Inspiration

AI agents can already find and buy things. Checkout, though, assumes a person is at the payment step, and an unattended agent doesn't have one. A company that hands an agent its PayPal account lets money move without anyone agreeing to what was bought, from which merchant, at what price, or against which budget.

Caps and allowlists help, until one cart crosses a line. At that point a company doesn't need a log entry after the charge. It needs a person's approval, bound to that exact cart, before any money moves. We built the layer that sits there.

What it does

Veto is the approval layer between AI agents and a company PayPal account.

  1. An agent proposes a purchase in one sentence, from Slack (/veto 2 packs of bamboo toilet paper), through an API key, or from a scheduled standing job.
  2. AI reads it, code prices it. DeepSeek works out what and how many and routes the request to the right agent. Channel3 finds real offers from real merchants and prices every line.
  3. The agent's policy decides. Plain code checks the cart against a monthly budget, per-order and per-item caps, merchant and category allowlists, and an approval threshold. Every rule that fires is stored with what it saw and where the line is.
  4. A person approves when a rule asks. The approval is a single-use consent that holds a SHA-256 fingerprint of the exact cart. Press Verify against this cart and your browser recomputes the hash itself.
  5. PayPal captures only against that approval. Approving runs PayPal Orders v2: consent signed, order created, authorized, captured. Each real sandbox id lands on the record as it happens.
  6. A refusal is a receipt too. A blocked cart shows the failing rule and its numbers, and proves that no consent and no PayPal object was ever created.

Each agent is its own spending identity with its own budget, scope, standing jobs and order history. Analytics counts only captured money as spend, and it reads the same rows as the inbox, so the numbers reconcile.

How we built it

  • PayPal (sandbox): OAuth 2.0 client credentials, Vault v3 to save a PayPal wallet once, and Orders v2 with intent: AUTHORIZE charged against that vaulted wallet as a merchant-initiated payment. Then Payments v2 captures the authorization. The consent's nonce becomes the PayPal-Request-Id, so a retried approval replays the same order instead of charging twice.
  • DeepSeek (deepseek-flash, JSON mode, temperature 0) handles three narrow jobs: extracting line items, routing a request to an agent, and picking which catalogue offer is the requested product. Each has a deterministic fallback, and the model never sets a price, a limit or a payee.
  • Channel3 is the catalogue. Each cart line is searched in parallel across merchants, and the results stream into the console as items resolve.
  • Backend: one Python standard-library HTTP server with SQLite and an append-only audit trail. It serves the JSON API, signed Slack commands, a server-sent-events feed for the live board, and a scheduler for standing jobs.
  • Console: React 19 + TypeScript + Vite. It has a live board, keyboard approvals (A / V), in-browser consent verification with WebCrypto, and a layout that works down to 390px.
  • Slack: six signed slash commands, plus Approve and Veto buttons that write the same store as the console.
  • Render: the demo runs on the free Docker plan from one render.yaml. Its disk is wiped on every restart, so on a cold start the server rebuilds a month of history through its own public API: real Channel3 prices, real policy checks, real sandbox captures.
  • 362 automated tests, plus read-only HTTP check suites that compare every number a screen shows against the database.

Challenges we ran into

  • A vaulted PayPal wallet behaves differently. For a merchant-initiated payment against a saved wallet, PayPal can return the order already authorized. Settlement reads what came back and only calls authorize when it's needed, so no step is skipped and none is repeated.
  • Making a retry safe. Approvals can be double-clicked, retried from Slack, or interrupted mid-flight. Each PayPal call gets an idempotency key derived from the single-use consent, so the same approval can never produce a second charge.
  • Keeping the model honest about products. A catalogue search for "coffee" also returns coffee pots, and "Pringles" returns T-shirts. The offer picker has strict rules ("a related object is not the product") and reads Channel3's own category data before choosing. When nothing matches, the request says so instead of buying the nearest thing.
  • Proving the consent in the browser. The browser's hash has to match the server's byte for byte, so the console rebuilds the exact canonical JSON the Python side signs.
  • A free host that forgets everything. The demo had to look lived-in after every restart without fake data. So it replays its history through the same pipeline a real agent uses, and keeps one cart waiting so every visitor has something to approve.

Accomplishments that we're proud of

  • Money moves only on approval, and you can see it. Approve a cart and watch consent, order, authorization and capture arrive with real PayPal sandbox ids.
  • A consent you can check yourself. The approval is a fingerprint of the cart, and the browser verifies it on screen.
  • Refusals are first-class records. Every "no" has a reason, numbers and proof that nothing was charged.
  • It's live and finished. One click opens a shared demo workspace with a cart waiting, so a judge can approve a real sandbox payment in under a minute.

What we learned

  • Keep AI at the edges of a payment, and keep the money path deterministic. The model is good at reading what someone wants. Prices, limits and payees belong to code that can be tested.
  • Authorize-then-capture is the right primitive for agents. It separates "this cart is allowed" from "money moved", and gives a clean point for a person to step in.
  • A refusal is product, not an error. Treating a blocked cart as a record with reasons made the whole system easier to trust and to audit.

What's next for Veto

  • Refunds and partial captures, so a person can correct a purchase after the fact.
  • More than one approver, with approval chains above set amounts.
  • Production PayPal credentials and per-workspace wallets.
  • Accounting export (CSV, then QuickBooks and Xero).
  • An MCP server, so any agent framework can propose a cart to Veto as a tool call.

Built with

PayPal Orders v2 · PayPal Payments v2 · PayPal Vault v3 (sandbox) · DeepSeek · Channel3 · Slack · Render · Python · SQLite · React · TypeScript · Vite

Built with AI assistance: Claude Code helped write code and documentation. Product and design decisions, and the final review, are ours.

Built With

Share this project:

Updates

Submission history