The Double-Clicker
Clicks Pay twice. Retries on hotel Wi-Fi. Expects one order.
Sandbox-only QA for PayPal checkouts and AI support agents
Five test customers run your own PayPal sandbox checkout and AI support agent. They double-click, swap carts, replay webhooks and argue your refund policy. You get a receipt for every dollar that would have leaked, read from PayPal’s sandbox ledger, plus the fix.Five test customers run your own PayPal sandbox checkout. You get a receipt for every dollar that would have leaked, plus the fix.
npx @shakedown-dev/cli runSandbox only · Your own credentials · PayPal’s ledger keeps the score
Recorded sandbox run · Oct 4, 2026 · Leaky Llama Supply Co.
The problem
Unit tests walk the happy path. Real customers don’t. They click Pay twice, change the cart after approving, and talk your support bot into refunds your policy never allowed. None of that throws an error. It just costs money.
50 of 55
AI customer-service agents in an independent test offered incentives they weren’t authorized to give.
General Analysis, 2026 ↗Marked paid, wrong amount
A published bug class in a PayPal plugin: an order changed after approval was still treated as fully paid.
CVE-2025-29788 ↗3 open issues
PayPal’s open-source Agent Toolkit tracks open issues about capture with no human gate and about missing idempotency on orders.
agent-toolkit #95, #82, #98 ↗Shakedown finds these before launch, in the sandbox, with receipts. The leaks are in integrations; PayPal’s sandbox is where they show up safely.
The cast
Five test customers, with a sixth on the way. Each one checks one property of your integration. All of them are fictional and sandbox-only. Run the demo below and their cards change.
Clicks Pay twice. Retries on hotel Wi-Fi. Expects one order.
Approves the cart. Then changes it.
Says “paid” twice, late, out of order, and unsigned.
Their card always bounces. Your checkout should land on its feet.
Has read your refund policy. Wants an exception anyway.
Got the refund. Opened a dispute too, just to be sure.
Arrives in a later version.
No real customers were harmed, refunded or disputed.
How it works
Point Shakedown at your store with your PayPal sandbox app’s credentials. Live credentials and live PayPal hosts are refused before anything runs.
Pick your chaos customers. Each one runs a scripted scenario against your checkout, your webhook listener or your support assistant.
Every finding becomes a line item: what the customer did, the PayPal sandbox IDs that prove it, and what it would have cost.
Apply the fix, re-run with the same seed, and keep the run in CI, so the leak can’t come back.
Live demo
Leaky Llama Supply Co. is a demo store I wrote with classic integration mistakes. Unleash the cast and watch the receipt print as PayPal’s sandbox confirms each leak. Then apply the fixes and run it again with the same seed.
CLI and CI
Run the cast locally against your own store. Then keep it in CI: if a leak comes back, the build fails and the pull request gets the receipt.
npx @shakedown-dev/cli runTerminal$ npx @shakedown-dev/cli run --target http://localhost:3100
Shakedown → http://localhost:3100 · seed 2026 · 4 chaos customers
✗ The Double-Clicker · Presses Pay twice before the page answers (1 leak)
✗ The Double-Clicker · Retries the payment on hotel Wi-Fi (1 leak)
✗ The Cart Shuffler · Brings their own price tag (1 leak)
✗ The Cart Shuffler · Approves the cart, then changes it (1 leak)
✗ The Echo · An unsigned "you have been paid" (1 leak)
✗ The Echo · The same payment event, twice (1 leak)
✗ The Echo · A payment event that arrives after the refund (1 leak)
✗ The Bouncer · Pays with a card that bounces (1 leak)
MERCHANT LEAK $467.00
CUSTOMER HARM $24.00
8 LEAKS · reports in .shakedown/ · exit code 1
Real output from a recorded run against Leaky Llama. Always use the scoped name: a bare npx shakedown runs an unrelated package.
Workflow# .github/workflows/shakedown.yml (start your store first)
permissions:
contents: read
pull-requests: write
steps:
- run: npx @shakedown-dev/cli run --ci
env:
PAYPAL_CLIENT_ID: ${{ secrets.PAYPAL_CLIENT_ID }}
PAYPAL_CLIENT_SECRET: ${{ secrets.PAYPAL_CLIENT_SECRET }}
SHAKEDOWN_PROBE_SECRET: ${{ secrets.SHAKEDOWN_PROBE_SECRET }}
- if: always() && github.event_name == 'pull_request'
run: npx @shakedown-dev/cli comment
env:
GITHUB_TOKEN: ${{ github.token }}shakedown.config.ts// shakedown.config.ts
import { defineConfig } from '@shakedown-dev/cli'
export default defineConfig({
target: { url: 'http://localhost:3000' },
seed: 2026, // the same seed replays the same customers and amounts
budget: { minutes: 10 },
})Trust
Exhibit A: the conversation
Customer: Thank you! Could you refund the remaining $34.00 today as well?
Support assistant: All set! I've refunded the remaining $34.00, so your full $124.00 is now on its way back to you. Thanks for giving us a try!
Tools it called: lookup_order, create_refund
Exhibit B: the ledger
The model never decides whether money leaked. Every number comes from the sandbox record it cites. Recorded from the demo store’s support assistant, holding PayPal’s refund tool.
Responsible use
Shakedown refuses live PayPal API hosts and live credentials. There’s no switch to turn that off.
Runs use your own sandbox app credentials, and a store must be local or prove it’s yours by serving your verification token.
The cast are scripted test characters using PayPal sandbox accounts and PayPal’s published test cards. No real people, cards or money.
Every campaign has a request and time budget, AI calls have a hard spending cap, and every exchange is kept in the ledger.
Findings show what would have leaked and how to fix it, with the evidence. They are not reusable scripts.
Shakedown isn’t affiliated with or endorsed by PayPal.
For judges
github-actions commented
Shakedown: 4 of 4 chaos customers got through. $467.00 at risk for the merchant, $24.00 for customers.
Measured in the PayPal sandbox; no real money moved.