Inspiration
In the stock market, nobody watches a price all day. You place a GTT ("good till triggered") or limit order, and the broker executes when the market meets your number:
$$ \text{buy} \iff p_{\text{market}} \le p_{\text{limit}} $$
Shopping has nothing like that. I'd wishlist something, get a 2 a.m. price alert, and miss the deal. Giving an AI agent my card wasn't the answer either: models hallucinate, misread variants, or get talked into things. So I built DropCart: a GTT for shopping, where an AI agent sets up the order but never decides or moves money.
What it does
- Browse the live Channel3 catalogue and like products.
- Tell the agent your price in plain English, e.g. "Track it for 2 weeks, max $338, ask me first". It fills in a mandate \( M = (p_{\max}, v, t_{\text{exp}}, \text{mode}) \): max price, variant, expiry, and auto-buy or ask first. You review and confirm it.
- Approve PayPal once with PayPal Vault. DropCart stores only a token id, never card or bank details.
- Stay inside a budget. Each tracker reserves its limit, and a new tracker is accepted only if \( p_{\max} \le \text{available} \).
- Auto-buy on the drop. Prices are re-checked every 6 hours, and DropCart charges PayPal only when the guard allows it, with a receipt explaining why it bought.
- Undo with one tap (a full PayPal refund). Every decision is logged with the rule that made it.
How we built it
- App: Next.js 16 + React 19 on Vercel (Singapore region), Supabase Postgres via Prisma, Supabase Auth, and Row Level Security on every table.
- Agent: Groq (
gpt-oss-120b) with a Gemini Flash fallback. Its only write tool fills in a form; it has no tool that can pay, refund or confirm. - The guard: a pure function with 12 ordered rules and 100% branch coverage. For a live price \( p \), stock \( s \) and resolved variant \( v' \):
$$ \text{BUY} \iff t < t_{\text{exp}} \;\wedge\; v' = v \;\wedge\; s > 0 \;\wedge\; p \le p_{\max} \;\wedge\; \text{mode} = \text{AUTO} $$
Otherwise it returns SKIP, ASK or EXPIRE. It runs twice: on the scheduled check, and again on a fresh live re-check right before payment.
- Budget, a spending cap rather than a wallet (no money is ever held):
$$ \text{available} = B - \sum \text{spent} - \sum \text{limits of open trackers} $$
- PayPal:
- Vault v3 (setup token → payment token), Orders v2 (merchant-initiated charge on the saved wallet), and Payments v2 refunds.
- Every call carries an idempotency key,
PayPal-Request-Id = trackerId:priceCheckId, so a retry can never charge twice. - The charge is refused before PayPal is even called if \( p > p_{\max} \).
- All of the PayPal code was written with APIMatic ContextMatic, which supplied the real SDK methods and models instead of guesses.
- Channel3: catalogue search, plus free product lookups with
selected_optionsto get the live price of the exact variant. Checking every tracker every 6 hours costs no search quota. - Scheduler: GitHub Actions calls a secret-protected
/api/cron/checkevery 6 hours. - Quality: 108 unit tests (Vitest), and Zod validation on every input.
Challenges we ran into
- Vault returned 403: PayPal Vault v3 isn't available to India-based business accounts, which PayPal developer support confirmed on Discord. A sandbox merchant account in a supported country fixed it.
- Checking generated code: ContextMatic's SDK methods and response paths were correct, but TypeScript caught two string literals where enums were required, and a card-only field placed on a PayPal-wallet credential.
- Substituted variants: if Channel3 resolves \( v' \ne v \) (say the black one sold out), the guard skips instead of buying the wrong colour.
- A late security catch: Supabase's public key could read every table until I enabled Row Level Security. Because the app only uses server-side Prisma, nothing broke.
- A bug found while recording the demo: the PayPal return page could load twice and wrongly show "couldn't connect" for a connection that had worked. It's now idempotent.
Accomplishments that we're proud of
- A real end-to-end money flow in the PayPal sandbox: vault consent, an unattended charge and a refund, all visible in the buyer's own PayPal activity.
- An AI agent that is genuinely useful but structurally unable to spend money.
- Every purchase decision explained in plain English, with the exact rule that made it.
- Deployed and live, with automated price checks, tests, and a 3-minute demo of the full flow.
What we learned
The safest place for an LLM in a payment flow is upstream of a deterministic guard: the agent improves the experience, while tested code moves the money. When software spends money unattended, idempotency isn't optional.
What's next for DropCart
- Sign-up for new users, so every shopper gets their own account and budget.
- Email notifications for purchases, approvals needed, and refunds.
- More currencies and stores, starting with INR for Indian merchants.
- Price-history charts and smarter limit suggestions from the agent, based on how a product's price has moved.
Built With
- ai-agents/llm
- apimatic
- apimatic-docs
- channel3
- channel3-docs
- claude-code
- contextmatic
- cron-jobs
- gemini
- github-actions
- groq
- next.js
- node.js
- paypal
- paypal-orders-api
- paypal-server-sdk
- paypal-vault
- postgresql
- prisma
- react
- supabase
- tailwindcss
- typescript
- vercel
- vitest