-
-
Signed Studio: an AI ad agency that cannot spend a cent you did not sign for
-
Signed: the money is held in PayPal, the campaign is created PAUSED
-
Mandate: limits signed once; the assistant pays from the saved PayPal inside them
-
Approve and sign: the amount computed in code; the PayPal order carries the ad's hash
-
The assistant's run: a mandate_charge receipt, the vault charge, the order id
-
studio verify: chain OK; one changed byte and the chain is INVALID
-
Money page: every hold, capture, refund and mandate charge, reconciled with PayPal's Transaction Search
-
PayPal refuses the capture (sandbox TRANSACTION_REFUSED): reason and debug_id on screen, nothing taken
-
/trust: the proof page for anyone, no account: ledger key, chain head, the self-check (chain OK, P-V1…P-V6), test counts
-
/agentic: how an assistant works with Signed Studio; the A2A agent card at /.well-known/agent-card.json with the payment policy
Judge quick start (3 minutes): 1) Sign in at https://pay.udun.eu as judge (password in the private field). 2) Studio → Use the example brief → Draft → answer or strike the questions → Confirm and run. 3) On Approve and sign, pay with the sandbox card (Visa 4111 1111 1111 1111, any future expiry, CVV 123) → Approve and sign: the campaign is created PAUSED and the money is only held. 4) Tick Sandbox: simulate PayPal refusing the capture → Request activation and sign: PayPal's refusal and debug_id on screen, nothing taken. Untick → activate: the capture. 5) Money → Check with PayPal reconciles every move against PayPal's Transaction Search. What sets Signed Studio apart from the many "agent pays within limits" entries: every PayPal call is a signed, hash-chained receipt and studio verify rechecks six money rules offline (600/600 tampered exports rejected).
Try it: https://pay.udun.eu (judges: sign in as judge; the password is in the private field for judges). Code: https://github.com/medelin/signed-studio (Apache-2.0). Everything runs on the PayPal sandbox.
Inspiration
I run ads for my own one-person firm. An AI could do the evening of work a campaign takes, but I would never hand an AI my card or my ad account. The question became: can an agent do all the work while the money moves only on a human signature, and can anyone prove afterwards that it did?
What it does
Signed Studio turns a short brief into a Meta campaign: AI agents plan, write and draw, two reviewers check every ad against the facts the owner confirmed and the platform's rules. PayPal holds the budget and the owner's signature releases it:
- Buyer present. The approval screen computes the amount in code and creates a PayPal
order (Orders v2,
AUTHORIZE) whosecustom_idis the ad's content hash. Signing is disabled until PayPal reports the payer approved; the signature then covers the order too. Signing authorizes the money and creates the campaign PAUSED; a second signature turns it on and only that one captures. - Buyer not present (agentic commerce). The owner saves PayPal once (Vault v3) and signs a spending mandate in plain words: goals, countries, hosts, caps per campaign and per period, which assistant tokens. Their own AI assistant (Claude, ChatGPT or a script, over MCP or the HTTPS API) runs a brief and pays from the saved PayPal, only inside those limits. Outside them it gets a pay-and-sign link for the owner instead of money.
- PayPal's agent tools, behind the gate. The assistant reads the account's PayPal transactions through PayPal's Agent Toolkit read tools, limited to the account's own money, and can propose a refund that only the owner's signature executes.
- Unhappy paths on screen. A sandbox switch makes PayPal refuse the capture
(
PayPal-Mock-Response): the studio shows PayPal's reason anddebug_id, nothing is taken and the money stays held. - Money page. One AG Grid table of every hold, capture, void, refund and mandate charge. "Check with PayPal" reconciles each move against PayPal's Transaction Search (matched, pending while PayPal's search catches up, held, missing, amount mismatch); "Explain" has the AI treasurer explain a funding in plain words, every amount recomputed by code.
- Proof. Every PayPal call is a receipt in an Ed25519-signed, hash-chained ledger.
PayPal's webhooks are verified offline against PayPal's RSA signature and re-read from
PayPal's API.
studio verifyrechecks six money rules (P-V1…P-V6) on an exported ledger: no capture without the owner's activation signature, no vault charge outside a valid mandate, refunds only on their own signed approval, never the same charge twice, voids after revocation. One changed byte fails it.
How we built it
Python (FastAPI, SQLite), no front-end framework; WebCrypto Ed25519 keys that never leave
the owner's browser. PayPal: Orders v2 (create, authorize, capture, void, refund), Vault v3
(setup tokens, payment tokens, delete), Webhooks (offline RSA verification, pinned cert
chain), Transaction Search (reconciliation), sandbox negative testing
(PayPal-Mock-Response), and PayPal's Agent Toolkit run through the studio's own receipted
PayPal client. AI: Muse Spark 1.3 for planning, copy and review; Muse Image 1.0 for images; SAM 3.1
for product cut-outs; an open-weights second reviewer; an AI treasurer that explains a
funding's reconciliation but never sees a PayPal id and never moves money — code recomputes
every amount it states. MCP server (Streamable HTTP) and an OpenAPI for assistants.
Challenges we ran into
- Proving money rules offline, not just enforcing them online: the verifier has to reconstruct who signed what for which PayPal order from the ledger alone, including PayPal's "order approved" webhook arriving before the owner's signature and a buyer who approves an order the owner never signs.
- PayPal's Transaction Search lags behind real time, and a search window that starts after its last refresh is rejected as a whole. Reconciliation reaches back past the lag and calls very recent moves "pending", never "missing".
- Negative testing: the sandbox answers
INSTRUMENT_DECLINEDon an authorization capture with an empty 403, so the studio usesTRANSACTION_REFUSED, which returns PayPal's real error shape anddebug_id. - Giving an AI agent PayPal's tools safely: answers limited to the account's own PayPal ids, nothing hashed from PayPal's raw pages (the sandbox merchant is shared), write tools refused.
- Browser security meets payment redirects: the strict CSP and
SameSite=Strictsession cookie both broke the Vault return from PayPal at first; fixed without loosening either (a sandbox-onlyform-action, a one-time same-site bounce). - Webhooks as triggers only: the studio never trusts a webhook body, it re-reads the resource from PayPal and records what the API says.
Accomplishments that we're proud of
- 600/600 tampered ledger exports rejected by
studio verify; 25/25 clean exports accepted. - 3,271 automated tests pass, including 288 attack tests (28 of them PayPal money attacks: over-cap charges, charges after revocation, a charge replayed against another mandate, captures without an activation signature, webhooks for orders nobody signed).
- Everything in the video is a live sandbox run on the hosted demo: order, payer approval, authorization, a refused capture, the capture, an assistant paying under a signed mandate, an assistant's refund proposal signed by the owner, reconciliation against PayPal's own transaction list.
- One campaign run costs about $0.10 in model calls.
What we learned
Agentic payments are safest when the agent can ask but cannot decide: limits signed by a human, enforced by code, and checkable by anyone afterwards.
What's next
Per-client accounts for small agencies, more ad channels behind the same signature, and PayPal's Agent Ready delegated tokens in place of our own mandate once they are generally available (the same idea: a maximum amount, a currency and an expiry, signed by a human).
Tools (AI models and services, and how they are used)
| Tool | Used for | Limits we put on it |
|---|---|---|
Muse Spark 1.3 (muse-spark-1.3) |
Producer (brief → plan), copywriter, vision reviewer, AI treasurer (splits a budget, explains a funding's reconciliation) | Never sees a PayPal id, credential or vault reference; code recomputes every amount it states |
| Muse Spark 1.3 contributor tier | Copy for inputs with no personal data (the reason is in the receipt) | Chosen by a PII check in code |
| Muse Image 1.0, SAM 3.1 | Ad images, product cut-out check | Reviewed before any signature |
| Muse Voice Transcribe 1.0 | Spoken briefs | The owner confirms the transcript |
| Spark Open (open weights) | Second, independent reviewer | Disagreements are shown, not averaged |
| MCP (Streamable HTTP) + OpenAPI | The owner's own assistant (Claude, ChatGPT, scripts) drafts, runs and pays under a signed mandate | Bearer token scopes; money only inside the mandate |
PayPal Agent Toolkit (paypal_agent_toolkit/shared 1.11.0, vendored) |
Read tools for the assistant: transactions, order details, disputes | Own PayPal ids only, payer data stripped, one keyed-hash receipt per call; write tools refused |
PayPal MCP server (mcp.sandbox.paypal.com; optional, documented setup next to ours, not part of the studio) |
Read-only questions if the owner adds it | Untested by us; anything that moves money goes through our server |
| PayPal AI Toolkit plugin (Claude Code) | /paypal:doctor on this repo, 5 Nov 2026: 0 critical, 4 warnings, all fixed |
Used while building; not part of the running app |
| APIMatic Context Plugin for PayPal | Installed in the coding agent while building | Used while building; not part of the running app |
| AG Grid Community 34.3.1 | The /money page (holds, captures, voids, charges) |
Vendored, strict CSP |
| Postman | Collection for the assistant API and the PayPal sandbox calls (docs/postman/) |
Variables only, no secrets |
| PayPal Orders v2, Vault v3, Webhooks, Transaction Search (sandbox) | Holding, capturing, voiding and charging the budget | Every call is a signed, hash-chained receipt; studio verify rechecks P-V1…P-V6 offline |
Submission-period note
The base (agents, signed approvals, receipt ledger, Meta adapter, mandates) was written from
27 Oct 2026 for another hackathon and is tagged base-meta-final (8d5f854, 3 Nov 2026).
Everything PayPal was written from 3 Nov 2026; git log base-meta-final..main shows it.