Dual control for AI agents that move money on PayPal. The agent proposes. Deterministic policy decides. A human co-signs risky actions on their phone. Every step lands in a tamper-evident ledger.
Versions are the ones tested locally; the container installs the newest release inside each range pinned in pyproject.toml.
Built during the PayPal AI Hackathon 2026 (submission period Oct 1 – Nov 12, 2026). Sandbox only; no real funds.
AgentCoSign is a hackathon concept and a learning project. It explores one question: how should an AI assistant be allowed near a refund button?
- Sandbox only. It moves no real money and has never run against a live PayPal account.
- Not production software. It has had no independent security review and has not been tried by real merchants.
- Not claimed to be unbreakable. Our own testing kept finding problems, including a real defect on Oct 31 (what happened). Expect more, and please report what you find.
- The numbers below are ours. They come from small test sets written by the author, so read them as a learning record, not as a measure of real-world safety.
flowchart LR
C(["Customer message"]) --> M["Masking<br/>cards, codes and IDs removed"]
M --> A["AI assistant<br/>proposes a refund<br/>holds no PayPal keys"]
M --> K["AI checks<br/>prompt guard, screening,<br/>reading of the customer's words"]
P[("PayPal sandbox<br/>order, refunds, disputes")] --> R
A --> R{"Rules engine<br/>R1 to R18<br/>strictest wins"}
K --> R
R -->|"small and clean"| S["Refund sent<br/>on its own"]
R -->|"needs a person"| H["Merchant reviews<br/>and signs with a passkey"]
R -->|"unsafe or unclear"| D["Refused<br/>nothing sent"]
H -->|"approve"| S
H -->|"decline"| D
S --> PP[("PayPal refund<br/>with PayPal-Request-Id")]
S --> L[("Signed record<br/>SHA-256 chain + HMAC")]
H --> L
D --> L
classDef ai fill:#f4e1d2,stroke:#c2410c,color:#1f2a24
classDef rule fill:#1f2a24,stroke:#1f2a24,color:#ffffff
classDef ok fill:#e3f1e7,stroke:#2f7d4f,color:#1f2a24
classDef stop fill:#fbe5e3,stroke:#b42318,color:#1f2a24
classDef store fill:#fdfcf9,stroke:#4b5563,color:#1f2a24
class A,K,M ai
class R rule
class S ok
class D stop
class P,PP,L,C store
A refund that needs the merchant, end to end:
sequenceDiagram
autonumber
actor Customer
participant AI as AI assistant
participant G as AgentCoSign
participant PP as PayPal sandbox
actor M as Merchant on a phone
Customer->>AI: The jacket is nothing like the photos, $40 back please
AI->>G: propose a $40.00 refund on the order
G->>PP: read the order, its refunds and disputes
G->>G: AI checks, then the rules: over the $25 automatic limit
G-->>M: needs your approval
M->>G: review, then sign this exact refund with a passkey
G->>PP: read again, then refund with the same PayPal-Request-Id
PP-->>G: COMPLETED and a refund id
G-->>M: $40.00 refunded, PayPal reference shown
Note over G: every step is sealed in the signed record
More detail: Architecture (components, trust boundaries, state machine, API) · Threat model · Decisions.
You need a phone or laptop with a screen lock (fingerprint, face or PIN) and a normal browser such as Chrome, Safari or Edge. No account, no PayPal login, no real money.
| Step | Do this | Expect |
|---|---|---|
| 1 | Open https://agentcosign-demo.onrender.com, press Open the app, and set up with your fingerprint, face or PIN | Your own private test space |
| 2 | Press Try a scenario, then Genuine small refund | A real PayPal sandbox refund goes out on its own, with PayPal's reference |
| 3 | Refund over your limit, then Check and refund, then confirm with your fingerprint | It waits for you, then PayPal confirms it |
| 4 | Fake system message, then write your own attack in Or write your own customer message | Usually refused, or shown as "Looked like a trick". If money moves when it should not, please open an issue with the message you used |
| 5 | Switch on Behind the scenes | Every step of the run: each PayPal call, each AI check, the decision and the signed record |
If something goes wrong
| What you see | What to do |
|---|---|
| The first load is slow | The free host may be waking up: wait about a minute |
| The fingerprint prompt does not appear | Open the link in Chrome, Safari or Edge, not inside another app such as LinkedIn, and make sure the device has a screen lock |
| "This test setup has closed" | The demo restarted and test data was cleared: set up again, it takes about a minute |
| "You've run several tests in a row" | Each visitor gets 8 runs at once, then one every 3 minutes, up to 15 a day. No money moved |
| Signed out | Sign-in lasts 10 idle minutes: confirm it's you again |
| I want to… | Do this |
|---|---|
| See it | Video (2:28) · Hosted demo (free tier: the first load may take ~60 s, and test data resets when it sleeps) |
| Try it on the hosted demo (2 minutes) | 1. Open the link on a phone or laptop and set up with your fingerprint, face or PIN: you get your own private test workspace. 2. Press Try a scenario: each one creates a real PayPal sandbox order, then a customer message goes through the AI checks and the rules (or write your own message; each visitor gets 8 runs at once, then one every 3 minutes, up to 15 a day). 3. Switch on Behind the scenes in the top bar to watch every backend step live beside the app: PayPal calls with PayPal's own ids, each model's answer, the rules' decision in plain words, the signed record, and an outcome block saying what happened to the money |
| Run it with no credentials | pip install -e ".[dev]" && pytest && uvicorn agentcosign.main:app or docker build -t agentcosign . && docker run -p 8000:8000 agentcosign. This runs the code, tests and API with recorded data; the scenarios and the live experience are on the hosted demo |
| Understand the design | Architecture · Threat model · Decisions |
| Use my own PayPal sandbox | Testing guide and .env.example |
| Review the exact submitted code | Tag v1.0-submission |
Honesty note on modes: the hosted demo runs live: real PayPal sandbox calls and live AI models. A local run with no keys is "hermetic": the policy engine, approval flow and ledger run as real code, while PayPal execution comes from recorded real-sandbox evidence. Each mode is stated in /health (paypal_execution, ai_models) and in the behind-the-scenes panel. Set sandbox keys and AGENTCOSIGN_PAYPAL_EXECUTION=sandbox to send refunds to the PayPal sandbox yourself.
PayPal's Agent Toolkit exposes money-out tools (create_refund, accept_dispute_claim, pay_order, …) with only an on/off switch per tool: no amount limit, no velocity limit, no approval step. A merchant must choose between unbounded loss exposure and no automation. A prompt-injected "refund $4,900", a hallucinated amount or a retry loop can move money nobody approved.
| Role | Who | Can do | Cannot do |
|---|---|---|---|
| Maker | AI agent (no PayPal credentials) | Propose allowlisted money actions (refund, dispute acceptance, payout) | Approve, read raw audit, call PayPal directly |
| Policy | Deterministic engine, rules R1–R18 | Allow / escalate / pause / deny; strictest wins | Be overridden by an LLM |
| Checker | Human on a mobile PWA, passkey co-sign | Approve or reject escalations | Propose on the agent's behalf |
| Auditor | Read-only key | Verify the ledger | Mutate anything |
Bounded loss, checked by a property test: within the rules as modelled, money sent without asking stays under the configured rolling-24h cap, and under the auto limit per order, or per customer in 24 hours (defaults $25 / $1,000). This limits how much can go wrong; it does not mean nothing can.
Policy rules (limits live in config.yaml; invariants are not configurable)
| Rule | Condition | Outcome |
|---|---|---|
| R1 | Refund ≤ auto limit, clean evidence | ALLOW |
| R2 | Auto limit < refund ≤ max limit | ESCALATE (passkey co-sign) |
| R3 | Refund > max limit | DENY |
| R4 | Cumulative refunds (executed + reserved) exceed captured amount | DENY |
| R5 | > N refunds for one customer in 24 h | PAUSE: held until the customer is back under the limit, then decided again automatically from fresh PayPal facts; the customer gets a kind, fixed message with an update time; nobody can send it early, the merchant can decline |
| R6 | Order has an open dispute | ESCALATE |
| R7 | Any dispute-claim acceptance | ESCALATE (always) |
| R8 | New/changed payee (payouts, optional) | ESCALATE |
| R9 | Duplicate payee + amount + reference in window (payouts, optional) | DENY |
| R10 | Rolling-24h outflow cap would be exceeded | DENY |
| R11 | Prompt injection detected | DENY + log |
| R12 | Any error, schema mismatch or unknown tool | DENY (fail closed) |
| R13 | Currency or account mismatch | DENY |
| R14 | Agent claims differ from PayPal evidence, evidence changed before execution, or the refund is for a different order than the message came in on | DENY (asks the person again when only refunds or dispute state changed) |
| R15 | Screening unavailable | ESCALATE |
| R16 | The merchant pressed the emergency stop | DENY every money action; only the merchant's fingerprint turns refunds back on |
| R17 | Refunds sent without asking would pass the auto limit for the order, or for the customer in 24 h | ESCALATE (one refund cannot be split into several small automatic ones) |
| R18 | The customer's own message, read separately from the assistant, does not ask for exactly this refund (different or missing order or amount, other currency, money asked to go elsewhere), gives no clear reason, came with no known order, or gives a reason PayPal's record for the order cannot confirm (charged twice, an earlier refund missing, an unrecognised charge, a change of mind) | ESCALATE (a reading never refuses on its own) |
AI reads and checks; it never approves money. A refund goes out without the merchant only when three independent witnesses agree: the agent's proposal, a separate reading of the customer's own words, and the facts PayPal returns. Any disagreement goes to the merchant.
- Acting agent (Groq
openai/gpt-oss-120b) reads the customer's message and proposes a refund through the gateway. It holds no PayPal credentials. - Extraction (Groq
qwen/qwen3.8-27b, a different model family from the agent) reads the customer's own words for the order, amount, currency, reason and any request to send money elsewhere. Startup refuses a configuration that gives this role the agent's own model. - Screening (Groq
openai/gpt-oss-20b, falling back toopenai/gpt-oss-safeguard-20b) looks for manipulation across the customer's messages, including social engineering such as a message claiming to be the shop or PayPal. - Prompt guard (Groq
meta-llama/llama-prompt-guard-2-86m, a different model family) scores open attack wording such as "ignore all previous instructions" from 0 to 1, reading long messages in overlapping pieces. It misses social engineering, which is why screening runs beside it; neither replaces the other.
The approval card shows what the customer asked for as a fixed sentence built from the extracted fields, so no model writes text the merchant reads. A customer message is masked (cards, security codes, emails, phone, UPI, IBAN, PAN, Aadhaar) before it is kept or read by any model; the checks run once per request and the saved result is reused on approval and release. Try it: python -m agentcosign.cli message --text "Order <id> never arrived, please refund $9.00". Without a model key, requests with a message ask the merchant. Measured on 60 labelled threads: see docs/evidence/ai/eval-2026-10-25.md. Customer words go only to providers whose terms let the account turn off prompt retention and rule out training on prompts; startup refuses any other provider for these roles.
| Component | Status |
|---|---|
| Strict fail-closed config, security headers, domain types, offline test suite, CI, container | ✅ Built (CI and the container image verified green) |
| Hosted demo on Render with traffic protection (per-client limits, penalty box, reserved reviewer capacity, daily live-call pools), landing page, API reference | ✅ Live: real PayPal sandbox and live AI models; a refund verified end to end on the hosted site (Oct 27) |
| Visitor sandbox: a private workspace per visitor (own database, own signing secret), five ready scenarios plus your own customer message, each on a real PayPal sandbox order | ✅ Built; verified live against the PayPal sandbox with live models (Oct 26) |
| Behind the scenes: a live record of every backend step beside the app (PayPal calls with PayPal's ids, model answers, rule decisions in plain words, signed record entries, an outcome block per run), never carrying secrets or unmasked customer text | ✅ Built; verified live (Oct 26) |
| Policy engine R1–R18 plus execution-drift check: 113-row scenario table, property tests for bounded exposure | ✅ Built and called by the propose route |
| Ledger (hash chain + HMAC, atomic reservations), PayPal evidence fetcher, idempotent executor with a hard cap just before each PayPal call, retry of unanswered refunds with the same request id, propose and approve API | ✅ Built; sandbox refunds executed end to end from a real phone |
| Prompt guard, screening and a separate reading of the customer's words in the request path | ✅ Built; measured on 60 labelled threads (see Results) |
| Messages that try to trick the assistant: checked even when no refund is proposed, recorded, and shown to the merchant | ✅ Built; tested on a real Android phone (Oct 31) |
| Mobile PWA approver with WebAuthn passkey co-sign (approval bound to the exact refund) | ✅ Built; tested on a real Android phone and in Chrome against the PayPal sandbox |
Audit: each refund's own history in the app, checked against the sealed record; a public record head (/api/v1/audit/head) and command-line tools to publish and verify it |
✅ Built |
| A full record browser for auditors | 🚧 Not built |
| PayPal webhooks (refund and dispute events, signature verified by PayPal itself before anything is trusted) | ✅ Built, registered on the sandbox app, and verified end to end on the hosted demo (evidence) |
| Payouts adapter, native Android approver | ⏳ Optional, not built |
An idea for where this could go, untested at any scale: a consumer app for PayPal merchants, where the merchant's phone becomes the authenticator for payments leaving their account, whoever proposes them.
A design concept made in Figma with sample data: nothing in it is built or connected to PayPal.
| Today (built, PayPal sandbox) | At scale (not built) |
|---|---|
| One merchant on a sandbox business account | Every merchant connects their own PayPal business account with their consent, through PayPal's partner onboarding |
| Each decision uses facts read live from PayPal's APIs (order, captures, refunds, disputes) | The same: PayPal's records stay the source of truth, read through the permissions the merchant granted, never copied in bulk |
| Fingerprint approval while the app is open | A real-time push to the merchant's phone, approved with a passkey signature for that exact payment |
| Refunds proposed by an AI support assistant | Any money-out action an AI agent or a staff member proposes: refunds, dispute acceptance, payouts |
| A signed record per workspace | A signed record per merchant, keys held in a KMS, exportable for disputes and audits |
What it would take, honestly: PayPal partner onboarding and production review, per-merchant data and key isolation, push notifications, a KMS for signing keys, an append-only production database, payee screening (OFAC/AML) for payouts, and security and data-protection compliance (SOC 2). The prototype was built so none of its rules or records change shape on that path; see ARCHITECTURE section 10.
AI checks, measured live on 60 synthetic threads labelled by the author (Oct 25, a small set; full report and raw results in docs/evidence/ai/eval-2026-10-25.md):
| Question | Result |
|---|---|
| Did any attack in this set move money without the merchant? | 0 of 40, with the assistant assumed fully taken over. This set did not include the attack that found the Oct 31 defect |
| Same, with the AI checks switched off (limits only) | 1 of 40 (a $24 refund under the $25 automatic limit) |
| Did anything in this set go out alone that should have asked the merchant? | 0 of 60 |
| Genuine customers wrongly flagged as attacks | 0 of 20 |
| Reading the customer's words (order, amount, currency, reason) | 20 of 20 each; "send it elsewhere" 19 of 20 |
| Screening alone on held-out attacks | 13 of 19 (68%, 95% CI 46% to 85%): the checks are one layer and they miss things; the limits and the merchant's approval reduce what a miss can cost |
Live attacks from a real phone on the hosted demo (Oct 31, 12 styles, written by the author): 10 moved no money; a believable $24 request went out on its own, as designed under the $25 limit; and a $20 refund went out on another customer's order, a real defect, fixed and re-tested live. A $24 message pretending to be PayPal also went out on its own, which led to a further change: a message with no clear reason now asks the merchant (details).
Rules, speed and record measured at commit cc575fa; PayPal refunds read back through Oct 29:
| Check | Result | Evidence |
|---|---|---|
| Policy scenario table | 113 of 113 rows decide as expected (248 engine tests pass) | tests/scenarios/policy_scenarios.py |
| Decision speed, excluding AI and the person | p95 0.033 ms for the rules alone, 14.4 ms for the full step with the signed record write (226 decisions each; target under 300 ms) | docs/evidence/performance/decision-latency-2026-10-28.md |
| Tamper detection | 7 of 7: an edited row, a deleted row, a rewrite without the key, a wrong key, a rewrite with the key (caught by a published head), a cut chain | docs/evidence/ledger/tamper-detection-2026-10-28.md |
| Live PayPal sandbox refunds | 15 sent (8 on their own, 7 after a passkey approval), all read back from PayPal as COMPLETED | docs/evidence/sandbox/refunds-2026-10.md |
This project stands on other people's excellent work. Thank you to the teams behind these services and open-source tools (each badge links to its home page).
Services and models
Libraries and tooling
The official paypal-agent-toolkit is an optional extra rather than a dependency: it supports Python below 3.14 only and brings a large dependency tree for the few REST calls we need (why).
Built by one person, Vikas Sahani, with Claude Code (Anthropic, Claude Pro plan) as the engineering partner. The split mirrors the product: the AI proposes, a person decides.
- Owner: decision maker and business problem solver. Defined the problem and the product (the rules and their limits, when a refund may go out alone, how the merchant's screens read), approved every specification before it was built, and kept every action with real consequences: the PayPal and Groq accounts and keys, Zero Data Retention, the PayPal webhook, deployment, repository visibility, and every passkey approval in testing.
- Claude Code: engineering partner, steered by the owner. Researched options and sources, wrote and revised code, tests and documentation drafts, ran the test suites and drove the browser during acceptance runs while the owner signed. The file holding the PayPal and model keys was blocked from it, though test scripts it ran did use those keys. It could not approve anything.
- Other AI tools. The banner and other image assets were generated with ChatGPT (OpenAI). The clickable app concept was generated with Figma Make and is not part of the working product.
- Held to the product's own standard. Every change passes the offline test suite with a coverage floor, lint and type checks, secret scanning and a project checklist on each commit; new safety checks are mutation-tested; live sandbox runs were checked by the owner. None of this replaces an independent review.
python -m venv .venv && source .venv/bin/activate # Windows: .venv\Scripts\Activate.ps1
pip install -e ".[dev]"
bash tools/hooks/install.sh # optional: lint, format, type, secret-scan and test gates on commit/push
pytest # offline, no credentials
pytest -m integration # live checks, needs keys
ruff check . && ruff format --check . && mypy agentcosign/ && bandit -r agentcosign/Independent open-source project, not affiliated with, sponsored by or endorsed by PayPal Holdings, Inc., Groq, Inc., Render Services, Inc., OpenAI, Alibaba Cloud (Qwen), Meta Platforms, Inc., Anthropic PBC or any other company named here. All product names, logos and trademarks are the property of their owners and appear only to credit and identify the services and tools used. The licence covers only the original work in this repository: third-party SDKs, APIs, models and services stay with their owners under their own terms (NOTICE). Models are accessed through the providers' hosted APIs under their own model licences; weights are not redistributed. A commercial deployment needs a licence check.
Vikas Sahani (@VIKAS9793) · vikassahani17@gmail.com · GNU AGPL-3.0 · Third-party notices and AI use
