Feather
Capstone • AI Market Insights Platform
About this project
The problem
Two problems, and the second one turned out to matter more. Prediction tools usually print a confident number and hide the error rate, so we built an evaluation layer that shows the evidence behind every forecast. Then, auditing the running service, I found the API trusted whatever identity the caller claimed in a header, which meant anyone could read or write any account.
My role
Team of four, two terms, one full academic year, and still running past it. I own the API gateway that now fronts the backend, the authentication and rate limiting inside it, and the security review of the whole system. Earlier in the project I owned the model evaluation layer and the API contract between the Python service and the React client.
What I shipped
- Judged #1 out of 50+ capstone projects at the 2026 showcase, graded 97/100 in the final term, up from 94/100 at the prototype stage.
- Found and closed an authentication hole in the running service: the API trusted a client-supplied header as proof of identity, so any caller could read or write any account. I verified it against production before writing a line of the fix.
- Built the API gateway that now sits in front of the backend. It verifies Firebase ID tokens against Google's published signing keys, discards client-supplied identity, and rewrites user fields inside request bodies so a spoofed body cannot get past a verified header.
- Made the gateway an allowlist rather than a proxy: an unrecognised path returns 404 and never reaches the upstream, because a proxy that forwards anything is an open proxy wearing your domain and your TLS certificate.
- Implemented dual-window rate limiting, a short burst budget plus a longer sustained one, with counters in Postgres so every edge instance shares a single number instead of each keeping its own.
- Audited the database and found 12 tables with Row Level Security switched off, including user profiles holding emails and display names. Wrote the migration that enabled it and scoped the user tables to service role only.
- Repaired a CI pipeline that had been failing on every run for months, caused by an unresolvable ESLint config path and a missing test dependency, then cleared the 129 lint errors and 5 test failures hiding behind it.
- Wrote the security plan the team works from, with a reproduction command for every finding so nothing rests on my word alone.
Achievements
6 of 6 unlocked
First Place
Judged #1 out of 50+ projects at Sheridan's 2026 capstone showcase. Graded 97/100.
Found the Hole
The API trusted a header as proof of identity. Anyone could read any account. I proved it against production, then built the gateway that closed it.
Twelve Tables Later
Audited the database, found 12 tables with Row Level Security switched off, and wrote the migration that fixed it.
Evaluation Owner
Built the harness that scores every model against a naive baseline, so the UI can be honest.
Contract Driven
Defined the API between model and frontend so neither half of the team sat blocked.
Team Player
A year with a team of four: code review, sprint planning, and the boring coordination that makes it work.
Case study
Feather (Capstone, Team of 4)
Live: feathertrade.org
Result
Selected for the final showcase round and judged first overall out of more than fifty capstone projects at Sheridan's 2026 showcase. Graded 97/100 in the final term, up from 94/100 at the prototype stage.
It ran for a full academic year across two terms with four developers. It is the largest system I have worked on, and the only one where I have had to audit somebody else's security decisions and then argue for changing them.
The team
Built with Muazam Syed, Wajiha Siddiqui, and Katie Reeves.
What Feather actually is
A market insights platform, not a single prediction widget. The app covers:
- Market. Indices, sector leaders and laggards, and a heatmap treemap where each tile is sized by its S&P 500 weight, with area and candle charts.
- Portfolio. Build a what-if basket of tickers valued at live prices, with Feather's next-session forecast applied per holding and rolled up to a projected basket value.
- Sectors. Compare your basket's sector weights against a benchmark (S&P 500 weights, equal weight, or custom) with configurable drift margins that flag green, yellow, or red.
- Clarity. Context before decisions: where today's move ranks against the past year of daily moves, how the wider market and the ticker's sector are behaving, and what the news is saying.
- Context / News. Article sentiment scored and aggregated into an overall tone.
- Risk. SEC filing analysis, running as its own Cloud Run service.
- Interpretations. The heart of the product. This is where the prediction model and the MiroFish simulation API actually run: you pick a ticker and a set of events, the model returns a forecast, and a confidence score sits next to it rather than being buried. A MiroFish run takes several minutes because it is doing real work, not returning a cached guess.
Model confidence is surfaced deliberately, because the point of the evaluation work was to make the number auditable rather than impressive.
Where the model actually stands
The forecast horizon is currently about 30 minutes ahead. The goal is a full 24-hour, next-session prediction with a materially higher confidence score, and that is the work still in front of us.
The limit is not the code, it is the hardware. Training something that can hold a 24-hour horizon needs GPU time and storage the team does not have, so the honest position is a working pipeline with a short horizon rather than a long horizon we cannot defend. That is also why the evaluation layer matters: it keeps the interface truthful about a model that is still improving.
Architecture
browser ──/api/…──► Vercel edge gateway ──► Cloud Run "alpha-worker" ──► Neon / Alpha Vantage
(the part I own)
│
└──────────────► Firebase Auth (Google sign-in)
Three Cloud Run services sit behind the product: the main API, the SEC risk extractor, and the MiroFish simulation. The FastAPI service in the repository is the reference implementation we prototype policy against, not what serves production.
Roughly 29,000 lines of TypeScript across 148 frontend files, a 1,750-line gateway, and a 2,500-line reference backend, with twelve test files spanning all three.
What I own: the gateway and the security work
Partway through the year I audited the running service rather than the code, and the result changed what I spent the rest of the project doing.
The finding
The browser told the API who the user was with an X-User-Id header read out of
localStorage, and nothing verified it. A Firebase uid is a public identifier, not a secret.
Measured against production, not inferred:
curl -s -o /dev/null -w '%{http_code}\n' -H 'X-User-Id: anything' $API/api/watchlist # 200
curl -s -o /dev/null -w '%{http_code}\n' $API/api/users/prefs # 401
curl -s -o /dev/null -w '%{http_code}\n' -H 'Authorization: Bearer garbage' \
-H 'X-User-Id: anything' $API/api/users/prefs # 200
The 401 on the bare request is the tell. There was a gate, and it checked whether you had
sent a header rather than whether the header was true. The garbage bearer token proves the
Authorization header was not being validated either. Anyone could read or write any
account, and one endpoint keyed on email, which is guessable in a way a uid is not.
The fix
I built the API gateway that now fronts the backend, as Vercel edge functions.
Identity. It verifies the Firebase ID token against Google's published signing keys, throws away whatever the caller claimed, and forwards values derived from the verified token. It also overwrites the user fields inside request bodies, because verifying a header achieves nothing if the backend reads the body instead.
Allowlist, not proxy. Every upstream path has to be declared with its method, whether it needs authentication, and which rate-limit budget it belongs to. An unrecognised path is a 404 that never reaches the upstream. A reverse proxy that forwards whatever it is handed is an open proxy, and an open proxy on our domain would let anyone reach our backend while wearing our IP address and our TLS certificate.
Rate limiting. There was none at all: twenty-five concurrent requests all returned 200, and one script in a loop could exhaust the Alpha Vantage quota every user shares while keeping Cloud Run scaled up on our bill. Each tier is now enforced by two budgets at once, a short burst window and a longer sustained one, because either alone has an obvious bypass. The counters live in Postgres so all edge instances share one number rather than each keeping a private count.
Caching with a rule. Shared CDN caching is only ever set for responses identical for every visitor. Anything user-scoped stays at zero, because a per-user response in a shared cache is a data leak.
The database
Reading the Supabase advisor, I found 12 of 17 public tables had Row Level Security
switched off, including user_profiles with emails, display names, and avatars. Supabase
publishes every public table through PostgREST, and the default grants give the anonymous
role read and write access, so RLS is the only thing between that endpoint and the data.
It was not exploitable at that moment only because our anon key was not in the shipped bundle. That is a delay, not a control: the anon key is designed to be public, so the protection would have evaporated the first time anyone added the Supabase client to the frontend. I wrote the migration that enabled RLS, kept public reads on market data, and scoped the user tables to service role only.
Everything else
The same pass turned up a login endpoint that created an account for any unrecognised
email, a placeholder signing key left in config, error handlers interpolating stack
internals into responses, untrusted news URLs rendered straight into href where a
javascript: link would have run, every guest sharing one server-side account, and a single
401 from any endpoint hard-signing the user out mid-session.
I also found the CI pipeline had been failing on every run for months. The ESLint config extended a path ESLint cannot resolve, so it threw before checking a file and the test and build steps after it never ran, while the backend job died at collection on a missing dependency. Fixing the config surfaced 129 lint errors and 5 test failures that had been invisible behind it.
The honest part
The models are baselines, not a research contribution, and the forecast horizon is still 30 minutes rather than the next session we are aiming for. The evaluation layer exists precisely so the interface says that rather than dressing a short-horizon model in a confident number.
Some findings are still open, and the plan says so with the owner and the command to reproduce each one. Two of the three Cloud Run services sit outside the gateway and have not had the same review. The main API can still be called directly, bypassing everything the gateway enforces, until the token verification lands in the service itself. Writing that down honestly was more useful to the team than claiming the work was finished.
What a year teaches you
The evaluation layer was built after the models, which meant rework once we could see how weak some early results were. I would build the scoring harness first next time, the same way you write the test before the implementation.
The bigger lesson was that the most valuable thing I did all year was not a feature. It was checking an assumption everyone had already accepted, against the running system, and being willing to be the person who says the authentication does not work.
Notes
The repository is private. The team is still developing Feather and is considering taking it further than the capstone.