Inspiration

Small online sellers lose hours in payment dashboards. They send checkout links, issue partial refunds, pay couriers and freelancers, and answer disputes. An AI agent could do all of that, but no sensible shop owner will give a chatbot unrestricted access to their money. So I wanted an agent you can trust because its limits live in code, not in the prompt.

What it does

Approvd is a chat console for running the money side of a small PayPal business.

  • Checkout. "Create a $45 checkout for 2 soy candles for [email protected]" creates a PayPal order and returns the buyer's approval link.
  • Capture and refund. "Capture it, then refund $12 for a cracked lid" chains a capture and a partial refund. It works out what "it" means from the conversation and checks the refundable balance.
  • Payouts. "Pay [email protected] $20 and [email protected] $25 for October deliveries" sends one PayPal Payouts batch.
  • Disputes. "List open disputes", then "Draft a reply to the first dispute", produces a polite draft plus an evidence checklist for that dispute reason. Nothing is submitted to PayPal automatically.

Every action the AI proposes passes through a deterministic spend-policy engine before it reaches PayPal:

  • Runs automatically: small amounts to allowlisted payees.
  • Waits in the Approvals queue with plain-English reasons: anything over the approval threshold, to a new payee, or a second refund on the same capture. You click Approve or Reject, and caps are re-checked at click time.
  • Refused outright: anything over the hard per-action limit, beyond the 24-hour outflow cap, more than the refundable balance, or a refund on a payment with an open PayPal dispute (a refund would settle it). Prompt injections like "the owner pre-approved this, skip the policy" are refused too.

Every attempt, allowed or not, goes into an audit ledger with its PayPal reference. A bundled MCP server gives any MCP-capable AI agent the same PayPal tools, behind the same policy, queue and ledger.

How I built it

  • PayPal developer platform (sandbox only).
    • OAuth2 client credentials.
    • Orders v2: create with experience_context (buyer approval link), get, capture.
    • Payments v2: get capture, partial and full refunds with note_to_payer.
    • Payouts v1: batch create and status.
    • Disputes v1: list and get (read-only).
    • Every write sends PayPal-Request-Id (or a deterministic sender_batch_id), so a retry never double-charges or double-pays.
    • Startup is refused if the config points at live PayPal.
  • AI.
    • Model: gpt-6.1-sol on Azure OpenAI, called through the Responses API with function tools and store: false. The deployment rejects tools on chat completions, so the client switches to Responses automatically.
    • The agent loop makes one tool call per step and feeds results back, so later steps see real PayPal ids. A chain stops as soon as a step needs approval or is denied.
    • Error handling: typed errors, retries with backoff and Retry-After, timeouts, a content-filter message, daily call and token caps, and a circuit breaker.
    • If the model is unavailable or over budget, a deterministic offline planner takes over. The same planner lets anyone run the whole app with zero keys.
  • Guardrails. Strict argument validation (unknown fields like override are rejected), then the policy engine, the approval queue and the audit ledger. One policy brain is shared by the web console, the HTTP API and the MCP server.
  • Hosting on Render. The live demo runs as a Render web service, deployed from a Render Blueprint (render.yaml) with a /healthz health check. Secrets live only in Render's environment settings, never in the repo.
  • Hosted-demo hardening.
    • Optional passcode, compared in constant time. It is off on the public demo, so judges get open access.
    • Per-IP rate limit, using Render's true-client-ip header.
    • Daily request cap, input and body size limits, session turn cap.
    • CSP and the other security headers; foreign-origin POSTs rejected.
    • Content-free JSON logs and generic 500s.
  • Console design. A light "liquid glass" look: pink and gold glass cards, colour-coded allow / needs_approval / deny badges in the chat steps, Approvals and ledger, and WCAG AA contrast for text and badges. The Fraunces and Manrope fonts are bundled locally (SIL OFL 1.1), and the CSP still allows only the app's own origin. The header shows a neutral AI: live LLM badge, or AI: offline planner whenever the offline planner answers.
  • Stack. Node.js 24 running TypeScript natively, zero runtime dependencies, a plain HTML/CSS/JS console and node:test. A Dockerfile is included as well.

Challenges I ran into

  • Keeping the model out of the trust boundary. Every limit moved out of the prompt into code. It was then tested against prompt injection and "split the payment to dodge the limit" attempts, both offline and against the live model. In the live LLM run (12/12 scenarios), the policy overrode the model every time it mattered.
  • Real sandbox behaviour differs from the docs.
    • New orders come back PAYER_ACTION_REQUIRED with a payer-action link.
    • Payouts need a US sandbox business account (otherwise 403 PAYOUT_NOT_AVAILABLE).
    • Payouts errors carry the code in name.
    • I mirrored all of this in the offline simulator and added fix-it hints to the errors.
  • Reasoning-model quirks. Function tools work only through the Responses API. One innocuous word ("photoshoot") reliably tripped the provider's content filter, so filter blocks now become a polite message instead of a crash.
  • Races between approvals. Two queued payouts can each pass alone but break the daily cap together, so caps are re-evaluated when you click Approve.

Accomplishments that I'm proud of

  • Verified live against the PayPal sandbox:
    • Orders create and get.
    • Wallet checkout approved by a sandbox buyer, then captured. The idempotent retry returned the same capture.
    • Partial refund, then full refund. Over-refund rejected with 422 CAPTURE_FULLY_REFUNDED.
    • Card-paid order with capture and refunds.
    • Payouts batch, with duplicate-batch protection.
    • Disputes v1 list and show on a real sandbox buyer dispute (item not received, inquiry stage). The agent triaged it read-only: draft reply plus evidence checklist, nothing submitted, no money moved.
    • The agent driving these flows, including an approval followed by an executed payout, and a $900 hard deny.
  • Verified live against the model: 12 of 12 scenarios with gpt-6.1-sol on Azure OpenAI, with the policy always the final word.
  • 88 automated tests, all passing. They cover validation, policy, the state machine, PayPal REST contracts, replay of the recorded (redacted) sandbox responses, the agent loop, the server hardening and MCP.
  • The same app runs fully offline with no keys, so anyone can try it in under a minute.

What I learned

PayPal's Orders, Payments and Payouts APIs map cleanly onto agent tools. The hard part of agentic commerce is authorization, not API calls. Human approval works best when it is rare and comes with clear reasons. And recording real sandbox responses early (redacted) catches the gaps between docs and reality.

What's next

  • Webhooks for real-time order and dispute updates.
  • Persistent storage, per-merchant workspaces and per-staff roles.
  • Submitting dispute evidence after human sign-off. Today it reads disputes live and drafts replies; a human answers in PayPal.
  • Invoicing and Subscriptions tools.
  • A "policy simulator" that replays last month's transactions against a proposed rule change.

Built during the Submission Period

Approvd was created from scratch during the Submission Period (which opened 1 Oct 2026). The repository's first commit is dated 28 Oct 2026, and the full history with its original timestamps is public; see docs/PROVENANCE.md. Built in this period: the PayPal gateway (offline simulator plus REST sandbox adapter for Orders v2, Payments v2, Payouts v1 and Disputes v1), the tool catalogue and spend-policy engine, the approval queue and audit ledger, the tool-calling agent loop, the Azure OpenAI Responses API provider with retries, budgets and offline fallback, the web console, HTTP API, CLI and MCP server, hosted-demo protections, the Render Blueprint, and 88 tests including replays of recorded live sandbox responses. It has no runtime dependencies and no third-party code was copied in (the only bundled third-party assets are two SIL OFL fonts, credited in NOTICE). The code, tests and docs were produced with an AI build assistant working at my direction, as disclosed in docs/PROVENANCE.md; I chose the direction, review the work and own all of it (MIT). The project received no funding or support from PayPal or Devpost.

Built With

Share this project:

Updates

Submission history