Inspiration

Agents that can recommend a product are easy to build. Agents that can pay are scary. The hard part of letting an assistant shop for you is not finding products — it is trusting it with money: How much may it spend? What is off limits? Can I undo it? We built Leash around those three questions, so the screen's main characters are not the shopping cart but the mandate, the guardrail trace, and the PayPal API log.

What it does

You write a request in plain English — "Pasta dinner for 4 under $25". Leash:

  1. Understands it — a language model turns the sentence into a structured shopping intent (budget, items, exclusions).
  2. Searches a catalog and lets the model pick among the candidates the code found.
  3. Enforces the mandate in code — monthly budget, per-order cap, per-item cap, max quantity, blocked categories (alcohol and gift cards by default), merchant allow-list, price-drift tolerance. Blocked items are swapped or removed; an over-budget cart is repaired (cheaper swap first, then optional extras dropped); if it cannot fit, the agent refuses to check out. Every step is shown in a trace.
  4. Asks you once. On confirm, the server re-quotes every price, re-runs the whole mandate against today's ledger, holds the budget, and creates a PayPal order.
  5. PayPal collects the approval — the buyer approves on PayPal's page, not in our app.
  6. Verifies, then captures. On return, the server re-reads the PayPal order and checks status, amount and ownership. Only then does it capture.
  7. Undo is a first-class action. "refund the parmesan" finds the line, asks you to confirm, and refunds exactly that item through PayPal inside the merchant's refund window. The budget is credited back.

The model never touches money: every price comes from the catalog, every total is integer cents, and every PayPal amount is built by code.

PayPal APIs used

Step Endpoint
Auth POST /v1/oauth2/token (client credentials)
Create order POST /v2/checkout/orders — intent: CAPTURE, itemized purchase_units with an item_total breakdown, payment_source.paypal.experience_context (return_url, cancel_url, user_action: PAY_NOW), PayPal-Request-Id for idempotency
Verify GET /v2/checkout/orders/{id} — must be APPROVED, with the amount and custom_id of the confirmed cart
Capture POST /v2/checkout/orders/{id}/capture (idempotent)
Refund POST /v2/payments/captures/{capture_id}/refund — one line (partial) or the whole order

All calls go to the PayPal sandbox; Leash refuses to start against a non-sandbox host.

AI: tools and how they are used

Tool Role
Meta Muse Spark (muse-spark-1.3, Meta Model API) Runtime model. Called through an OpenAI-compatible /chat/completions endpoint for two jobs only: (1) read the request into a JSON shopping intent, (2) choose among catalog candidates per item.
Claude Code (Anthropic) Used to write the code and tests of this project during the submission period.
PayPal Developer Platform (sandbox) Orders v2 and Payments v2, described above.

The model's output is treated as untrusted input: intents are validated and clamped (quantities, list sizes, budget), a SKU the catalog did not return is rejected, prices are never read from the model, and if the model errors or times out Leash falls back to a built-in rule parser and says so in the trace. Any OpenAI-compatible endpoint works (LLM_BASE_URL, LLM_MODEL, optional LLM_API_KEY).

In the demo the runtime model is Meta's Muse Spark through the Meta Model API's OpenAI-compatible endpoint (https://api.meta.ai/v1), with response_format: json_object and temperature: 0. No code change was needed to switch providers.

How we built it

  • Zero dependencies. Node.js 22+ with only built-ins (http, fetch, node:test). npm start runs it; no install step.
  • One PayPal client, two targets. The same HTTP client (OAuth, retries, idempotency keys, error parsing) runs against the PayPal sandbox or against a local stand-in of the PayPal REST API for offline development. The stand-in imitates request shapes and the checks the app relies on — it is not PayPal, and the UI says so on its approval page.
  • Budget holds. Confirming a cart reserves its amount, so two parallel orders cannot overspend; a cancelled or failed approval releases the hold; a capture turns it into spend; a refund credits it back.
  • Capture only after verification. Returning to the app is not proof of payment. The server re-reads the order from PayPal first; a direct hit on the return URL without approval is rejected and the hold is released (covered by a test).
  • Tests. 13 node:test tests (policy, rule parser, the LLM path against a fake model including a hallucinated SKU, full HTTP flow with approve → capture → partial refund → budget credited) plus a separate-process end-to-end driver.

Challenges we ran into

  • A refund request has to find "the parmesan" in an order whose cart the agent rewrote to fit the budget (aged parmesan became a grated cheese). We now carry the catalog tags on each order line and match on name plus tags.
  • Making guardrail behavior visible: a language model plans different carts than the rule parser, so the demo request names the items (including a bottle of red wine) and a $20 budget, which reliably shows the blocked-category removal and the cheaper-swap repair.
  • Model latency varies (a few seconds per call, sometimes more), so the LLM timeout is configurable and the fallback to the rule parser is explicit in the trace rather than silent.

Accomplishments that we're proud of

  • The limit is not a prompt. A model that wants to buy wine, six bags of chips or a $50 gift card cannot — the code removes or caps them and shows why.
  • The same cart is accepted or refused depending on the mandate at checkout time: tighten the per-order cap after the proposal and confirm fails with a 409 before any PayPal order exists.
  • A full agentic-commerce loop — request, cart, order, buyer approval, verified capture, refund — in a repo that runs with one command.

What we learned

  • For agent payments, the useful questions are "how much", "what is off limits" and "how do I undo it". Making them first-class objects (mandate, guardrail trace, refund) did more for trust than a better recommender would.
  • Idempotency keys and a re-read of the order before capture are what make "the model asked for it" safe to wire to real money movement.

What's next

  • Live product search through a commerce-data API instead of the bundled catalog (an adapter for Channel3 search is in the code; it has not been exercised against the live API).
  • Multi-merchant carts, per-merchant PayPal orders, and spend limits per category.
  • Mandates that expire or require a second human approval above a threshold.

Verification (what we ran)

  • npm test — 13 of 13 pass (mock mode).
  • npm run e2e with the real model (Muse Spark) against the PayPal stand-in — request → cart → order → approval → capture → "refund the parmesan" → partially refunded ($3.19): E2E OK.
  • Live PayPal sandbox, real model: "Pasta dinner for 4 under $25" → 4 items, $12.46 → order PAYER_ACTION_REQUIRED → buyer approved on PayPal (guest checkout with a sandbox test card) → server re-read and captured (201) → "refund the parmesan" → PARTIALLY_REFUNDED, $3.19 credited back to the budget (201).
  • The demo video is recorded against the live sandbox end to end (capture and refund are real sandbox calls; waits are fast-forwarded and labelled).

Honest limits

  • The product catalog (4 shops, 47 items) and its merchants are fictional; Leash acts as the single merchant in the PayPal sandbox.
  • No real money moves anywhere: sandbox only.
  • Single-user, single-process demo storage.

Try it

Node.js 22+, no accounts needed for the offline mode:

git clone https://github.com/kimdooo-a/leash-agentic-checkout leash
cd leash
npm start            # http://localhost:3070 — PayPal stand-in + rule-based AI
npm test
npm run e2e

To run against the real PayPal sandbox and a language model, copy .env.example to .env, add the sandbox app's Client ID and Secret (PAYPAL_MODE=sandbox), and optionally point LLM_BASE_URL / LLM_MODEL at any OpenAI-compatible endpoint. The README has the exact steps.

Built with

Node.js · PayPal REST APIs (Orders v2, Payments v2, sandbox) · Meta Muse Spark (Meta Model API) · OpenAI-compatible chat completions API · vanilla JavaScript · node:test · Claude Code

Demo video: https://youtu.be/1WjwEpfasWg · Code (MIT): https://github.com/kimdooo-a/leash-agentic-checkout

Built With

Share this project:

Updates

Submission history