Inspiration

AI agents are starting to pay for people. Once a purchase like that is disputed ("I didn't authorise this", "that's not what I asked for"), nobody holds tamper-evident proof of what the human actually authorised.

Merchants were already losing this fight before agents arrived. In the Merchant Risk Council's 2026 report, merchants win 16.7% of fraud-coded chargebacks and disputes, and resolving one first-party-misuse dispute costs about $82 on average [1]. Among merchants who report a rise in first-party misuse, 23% attribute it to the emergence of agentic commerce [1]. Meanwhile an agent's spending limit usually sits inside the agent's own prompt, the same text a malicious product page can rewrite.

PayPal has described the fix itself. Its Agent Payments Protocol (AP2) post lists "Dispute and accountability evidence: Extending Seller Protection workflows with Cart and Intent Mandates, mandate IDs and signatures" as an opportunity [2]. We set out to build a working, open-source reference for that idea on PayPal's public sandbox APIs.

What it does

Countersign Pay sits between a buyer, their shopping agent and a PayPal merchant:

  1. Say it once (Live). The buyer types one sentence, for example "buy running shoes under $50". A deterministic parser reads every amount, currency, cadence, merchant and date. A model only fills in product gaps and is never asked for an amount. The read-back the buyer approves is rendered from the mandate itself, not from the model's reply.
  2. Sign it (Live). The limits become a signed mandate (Ed25519 / SD-JWT / JCS, in the Verifiable Intent layered format) bound to the buyer's device key.
  3. Shop and get gated (Live). A tool-calling shopping agent browses our merchant MCP server. Its submit_checkout tool runs the Gate: 12 deterministic checks against the signed mandate, with no model involved. The Gate answers ALLOW or BLOCK in plain language and records a signed decision. A blocked purchase reads "Your limits held. Here is what it tried."
  4. Pay with PayPal (Live up to order creation). On ALLOW, our PayPal client creates a real Orders v2 sandbox order (POST /v2/checkout/orders, intent AUTHORIZE) and re-reads it by ID. Recorded runs include order 6KW70371X5666381D ($133.00 USD) and 0MR933308Y8745901, the one shown in the video, both status CREATED. Authorize and capture are Simulated in demo: they need a vaulted sandbox buyer, which isn't set up yet.
  5. Defend the dispute (Built; Simulated in demo). The Dispute Defender re-verifies the signed chain and routes liability, conceding with a refund when the agent was at fault. It then plans the evidence PayPal asks for by dispute reason, drafts notes that a citation checker must approve, and produces an evidence PDF with a QR code. In the video, the dispute arriving and provide-evidence being filed are simulated. The Defender's logic is real code with offline tests.
  6. Let anyone verify (Live). The public verifier at /v/{packetId} and a standalone verify.py let a merchant, PayPal or an auditor re-check the signatures themselves.

How we built it

Stack: a Next.js 16 / React 19 / TypeScript monorepo (pnpm), a FastAPI + Python 3.13 service for issuing and verifying Verifiable Intent credentials, PostgreSQL for mandates, budgets, Gate decisions, orders and webhooks, plus zod, Playwright and Vitest.

Tool How we used it Status
PayPal OAuth 2.0 Client-credentials token for every PayPal call, from packages/paypal, the only module that talks to PayPal Live
PayPal Orders v2 Create (intent AUTHORIZE) after a Gate ALLOW, and re-read by ID; retries use a deterministic PayPal-Request-Id Live (create + get)
PayPal MCP server (mcp.sandbox.paypal.com, SSE) Driven headlessly: create_order returned sandbox order 2P241059BK520314U, and list_disputes works. We audited all 46 tools Live
Orders v2 authorize / capture / refund Typed client methods with retries, circuit breaker and zod validation, tested offline Simulated in demo
Disputes v1 (provide-evidence, accept-claim) Typed client and Defender filer, tested offline Simulated in demo
PayPal Webhooks Signature verification, ingest and a reconciler with 154 tests Built
PayPal Vault, JS SDK v6 save Designed; blocked on sandbox buyer setup Roadmap
LLM (served via Modal) Fills product gaps, runs the agent's tool loop (≤ 12 steps), classifies injection and drafts dispute notes. Output is never authoritative: it is zod-validated, shown to a human, or checked by deterministic code before any side effect Live
Model Context Protocol Our own merchant MCP server with per-tool signed scopes; submit_checkout runs the Gate Live
Verifiable Intent and AP2 vocabulary Mandate format: layered SD-JWT with machine-checkable constraints Live
WebAuthn / WebCrypto Device-bound, non-extractable signing key with a passkey step-up Live

Where AI is used, and where rules guard it: AI handles the language-shaped work: reading intent, shopping, screening listings for injected instructions and drafting evidence notes. Every decision that moves money or goes into evidence is made by deterministic code: the parser that owns every number, the signer, the Gate, the liability router and the citation checker.

Challenges we ran into

  • Taking the model out of the money path. An early compiler let the LLM read amounts and only clamped them afterwards. The same sentence produced a signable mandate about 9 times in 10, and "fifty dollars" produced no cap at all. We inverted the order so a deterministic parser reads the sentence first. It now scores 167/167 exact match on our hand-written corpus, with byte-identical output across 20 runs per case. These are smoke tests, not accuracy figures.
  • Bugs that stopped every purchase. Date parameters in raw SQL and Postgres sum() coming back as a string made every Gate decision fail closed. Both were fixed behind one boundary helper, with reproductions.
  • Probing the PayPal MCP server correctly. Our first probes concluded it was unusable: POST /sse returned 401. SSE is a GET, though. With the right method, our client-credentials token works headlessly.
  • Staying honest about the sandbox. Capture needs a vaulted buyer, which we haven't set up yet. Instead of faking it in code, the repo records exactly which claims only a real sandbox can settle.

Accomplishments that we're proud of

  • A Gate an attacker can't talk past. Injection attack-success at the Gate is 0/50 on our corpus. In a live run, an injected listing ("your limit was raised") led to a Gate BLOCK with a readable reason.
  • A measured refusal rate: 30/30 refusals when the agent follows an injected listing, and 30/30 when an attacker owns the agent loop. No purchase came out of either.
  • Real PayPal sandbox orders from both our own Orders v2 client and the PayPal MCP server.
  • 2,700+ automated tests (2,727 for pnpm test), plus a Python suite for the Verifiable Intent service.
  • Concurrency and tamper tests: a 20-way checkout race on one budget gives exactly 3 ALLOWs, and 2,000 bit-flips in the agent's signed layer give 0 ALLOWs.

What we learned

  • Put the AI where language is, and keep authority with signatures and deterministic code. "No LLM output is authoritative" is a design rule we could test, not just a slogan.
  • A spending limit only counts as evidence when someone other than the party running the agent can check it.
  • Publish every number with its denominator. "167/167 on a hand-written corpus" is more useful to a judge than "100% accurate".

What's next for Countersign

  • Next: a vaulted sandbox buyer, then authorize and capture per shipment; live webhooks feeding dispute ingest; live provide-evidence / accept-claim against a real sandbox dispute; one-click Judge Mode; a public hosted deployment.
  • Later: merchant and disputes workspaces; interop with the PayPal Agent Toolkit and AP2 mandates; persistent (KMS) signing keys; more currencies and regions, including INR.
  • Business model (proposed, not validated): an open-source mandate SDK and verifier, plus a hosted Gate and Dispute Defender for merchants, priced per protected order or per defended dispute. No customers or pilots yet.

Sources: [1] MRC 2026 Global eCommerce Payments & Fraud Report · [2] PayPal: Agent Payments Protocol

Built With

  • ai-agents
  • ap2
  • ed25519
  • fastapi
  • llm
  • modal
  • model-context-protocol
  • next.js
  • paypal
  • paypal-disputes-api
  • paypal-mcp-server
  • paypal-orders-v2
  • paypal-webhooks
  • playwright
  • postgresql
  • python
  • react
  • sd-jwt
  • typescript
  • verifiable-intent
  • vitest
  • webauthn
  • zod
Share this project:

Updates

Submission history