Inspiration

Rewards apps count points. They don't subtract annual fees or interest.

Jordan, 24, has three cards. Last year he earned $552.69 in rewards and $80.00 in perks, and paid $325.00 in annual fees and $734.72 in interest on a $2,800 balance at 26.24% APR. Net, his cards cost him $427.03.

What it does

Monte reads a year of purchases and computes each card's net: rewards + perks used − fees − interest.

  • Receipt: that net, per card and in total.
  • Where it went: spending by store and category, month by month.
  • Leaks: interest, fees that don't pay for themselves, recurring charges, each with its fix.
  • Replay: the same year on the best card already in the wallet. Jordan gets $104.14 back.
  • Every purchase: the card used vs. the right card, for all 576 of Jordan's purchases.
  • Tap: the best card at a store, counting this quarter's caps. Never a card carrying a balance.
  • Pay this first: which balance to pay down first, and how long it takes.
  • Keep or cancel: each annual fee against the value the card adds over the next-best card.
  • Apply: the next card, ranked on your spending, not on referral fees.
  • Next year ×1,000: each plan run over 1,000 simulated years.
  • Wrapped: the year in eight cards.

How we built it

No language model touches the math. The engine is deterministic, and every dollar traces to a card rule and the purchases behind it.

  • Card catalog: 29 cards. Each term has the issuer's source URL and the date we checked it (17 Oct 2026).
  • Rules engine: earn rates, spending caps, shared cap pools, pick-a-category cards, quarterly rotations, statement credits, cash vs. travel point values.
  • Monte Carlo: resamples your own months into 1,000 years and reruns the engine on each, so caps reset every year. Seeded. 19–313 ms.
  • Solver check: a linear program assigns the whole year at once. It proves the card-by-card routing optimal on all three test wallets (gap $0.00), and on a hand-built cap trap it finds $33.00 where card-by-card gets $30.00.
  • Provenance: click any dollar to see its rule, the arithmetic (purchase × earn rate × point value), the source and the date.
  • Input: statement CSVs (Chase, Amex, Capital One), parsed in the browser; Plaid sandbox for transactions, APR and balances.
  • Backend (branch m/backend, commit 6c50418; built and tested, not yet deployed): Supabase Postgres with row-level security on every table, and no card numbers stored. Seeded with the 29 cards and their sources, plus Jordan's 576 purchases and cap state. /api/sync walks your history once and caches how much of every card's spending cap is used; /api/tap reads that cache instead of replaying the year. Same card and same reward to the cent on every test purchase, in about 3 microseconds instead of 3 milliseconds (scripts/cap-cache-check.ts). A review pass caught the new endpoints trusting a user id from the request; they now take the user from the signed-in session token.
  • One model, one job (on m/backend): Claude Haiku 5.5 cleans messy bank merchant strings and assigns one of our 20 categories ("SQ *BLUE BOTTLE COF 0417 LOS ANGELES CA" → Blue Bottle Coffee, dining; "COSTCO WHSE #0479" → Costco, wholesale clubs). 8 of 8 correct in a live test. It never computes a dollar.
  • Speed: a year of 576 purchases routes in about 1 ms; 1,000 simulated years in about 58 ms.
  • Stack: Next.js 16, TypeScript, React, Tailwind, D3, Supabase/Postgres, Claude Haiku 5.5 (merchant names only), javascript-lp-solver, Plaid (sandbox), Claude Haiku 5.5 (merchant names only), Vercel. 36 tests, including hand-computed cases.

Adrian built the interface and brand. Michael built the backend: Plaid, the solver, the Monte Carlo engine and the tests.

Challenges we ran into

  • Caps. Citi Custom Cash shares one $500 cap a billing cycle across categories, BofA Customized Cash one combined $2,500 a quarter, and Blue Cash Everyday has a separate $6,000 a year per category. Getting the pool keys right, and making the engine, the solver and the Monte Carlo use the same keys, took several passes.
  • Caps broke our first Monte Carlo. Resampling monthly reward totals overstates capped cards, because a month is worth less once earlier months have used the cap. We switched to rerunning the engine on every simulated year, and wrote a script that measures the difference.
  • Greedy can waste a cap on an early small purchase. We needed to know whether that cost anyone money, which is why the solver exists.
  • Interest overrides rewards. A card with a balance gets no new spending, and Monte never suggests a new card while you carry one.

What we learned

Interest decides most of the answer: Jordan's best move is paying down his balance, not a better rewards card. And when a system has limits like caps, you have to simulate the system, not the totals it produced. A cheap greedy is fine as long as you can prove it.

Real vs. simulated

  • Real: the card terms and sources, the engine, Monte Carlo, the solver check, every number on the receipt, CSV upload, the Plaid code.
  • Simulated: the five demo people (Jordan, Maya, the Parks, Sam, Rosa) and their purchases. Tap is a web demo, not NFC. Plaid runs in sandbox.
  • Built, not deployed: the Supabase backend and the merchant-name model, on m/backend. The live site runs the engine in the browser.
  • Flagged: 11 of the 29 cards are marked unverified and badged in the app. Jordan's cards are all verified.

What's next for Monte

  • Merge and deploy the backend, so Tap runs on real synced data and merchant cleanup puts more purchases in the right category.
  • Production Plaid, and a background job that updates the cap cache as new purchases post.
  • Confirm the 11 unverified cards and add published rotating categories for other quarters.
  • Payoff planning: which balance to pay first, and when it is safe to open a new card.

Built With

Share this project:

Updates

Submission history