DEV Community

Quinn
Quinn

Posted on

We turned the 3 bugs that beat Pink's AI agent spending rules into an open test suite

None of the three bugs that beat our AI agent spending rules last week were AI attacks. One was a trailing space. One was a clock. One was half a cent. Pink Agentic AI Payments checks every AI agent payment request against company spending rules before a credential is issued, and this suite is how we test the inputs around those rules.

On 2026-10-10 we ran a public "Overspend Challenge": find an input that gets an AI agent's payment past spending rules it shouldn't pass. Outside testers found three gaps in one day. None of them were clever prompt injection or model jailbreaking. They were the kind of input-handling bugs that show up in any API that parses money, time, and strings: a currency code with a trailing space, a client supplying its own clock, an amount with one extra decimal place. So instead of writing three one-off bug reports and calling it done, we turned the pattern into an open, vendor-neutral test suite anyone can point at their own spending-rules API.

What the suite is

agent-spend-edge-cases is a set of edge-case inputs (23 in the current set) across six categories: currency strings, amounts, time, idempotency replays, payee-name matching, and concurrency. It's stdlib-only Python 3.9+, it doesn't know anything about a specific vendor, and it talks to whatever adapter you give it. For each mutated input, it compares the decision your API returns against the decision a clean, canonical version of the same request gets, and reports whether that mutated input was handled the same, handled more strictly, or handled more loosely. That last case is the actual failure mode: a rule silently getting bypassed because of how an edge value got parsed before the rule engine ever saw it.

Running it against Pink's own public sandbox:

python3 edgecases.py --target pink --base https://agentic-sandbox.pinkwallet.com
Enter fullscreen mode Exit fullscreen mode

That's the real command from the README, copy-pasteable, no account needed to point it at Pink's test-money-only sandbox.

The 23 cases

category cases what they probe
currency strings 6 trailing/leading space, lowercase, unknown code, empty string, number instead of string
amounts 9 sub-cent decimals, negative, zero, scientific notation (string and number), string-typed number, very large value, "NaN"
time (client-supplied clock) 2 a real payment passing its own local_hour or timestamp/created_at
idempotency replays 2 same key with a different amount, same key with an identical payload
payee name matching 2 ALL CAPS payee name, padded-whitespace payee name
concurrency (added 2026-10-13) 2 the same idempotency key fired 10 times in parallel; a refund racing a new payment near the cap

There are three outcomes the suite tracks, and only one of them is bad:

  • rejected - the mutated input got a 4xx. Fine, that's a clean reject.
  • evaluated the same as clean input - this is "safe" even though nothing was rejected, because the mutated value got priced or matched correctly and the decision didn't change.
  • evaluated looser than clean input - this is the failure. The mutated input got auto-allowed when the canonical request would have been blocked or held, or a credential ended up covering something different from what was actually evaluated.

Our own results, honestly

We ran it against our own sandbox (coffee-template workspace, a Purchasing AI agent with a $500 per-payment cap and a $4,000/month budget). 23 cases run: 20 safe, 2 known-gap, 1 not applicable, 0 new unsafe findings.

The 2 known-gap cases are A1 (500.0001, a sub-cent amount) and A2 (1500.5 JPY, a currency that allows 0 decimal places). Both reproduce a finding we'd already disclosed and credited from the Overspend Challenge: amounts with more decimal places than the currency allows aren't rejected yet, so a sub-cent amount still matches the same auto-allow rule a rounded amount would. The fix, rejecting excess-decimal amounts with HTTP 400 before any rule runs, is in progress, not shipped. We're leaving those two rows as known-gap on purpose instead of quietly excluding them, because the whole point of an honest results table is that it includes the cases you haven't fixed yet.

Everything else held up. Currency strings with casing or whitespace noise got priced correctly instead of exploited into a different threshold. Scientific notation and string-typed numbers evaluated identically to their plain equivalents. A client-supplied clock got ignored in favor of the server's real clock on an actual (non-dry-run) payment. Idempotency replays with a changed amount got rejected (409), and identical replays returned the original payment instead of re-evaluating. A padded payee name didn't get matched to an existing approved payee, it got treated as unrecognized and asked for approval, which is stricter than the canonical case, not looser, so it counts as safe even though it's worth knowing about if you're matching payees by name instead of ID.

The two concurrency cases were added on 2026-10-13 after dev.to readers @arhancanli and @mickyarun asked about them. Firing the same idempotency key 10 times in parallel produced exactly one payment in all 4 runs: all 10 calls returned the same payment ID, and the day's spend moved by one payment's worth, not ten. The refund race is marked not applicable, because Pink's sandbox has no refund operation to race. We checked the REST docs, the MCP tool list and the live API before writing that down rather than skipping the row.

Zero new findings means the suite didn't surface anything beyond what the Overspend Challenge already found. That's the expected outcome for a suite built directly from your own disclosed bugs; the interesting test is what happens when someone else's API hits it.

Writing an adapter for your own API

The suite ships a PaymentRulesAdapter interface in adapters/TEMPLATE.py. You implement four methods:

  • ensure_workspace() - create or reuse a test workspace with test money only, return workspace/payee info.
  • check(payload) - a dry run against your spending rules, no money moves, returns a decision-like field (would_allow / would_ask / would_block or your own equivalent).
  • pay(payload, idempotency_key=None) - a real call, used only where a dry run can't show the behavior (client-clock cases, idempotency replays).
  • get_payment(payment_id) - fetch a previously created payment or hold.

Copy the template to adapters/yourname.py, implement those against your API, register it in the ADAPTERS dict at the top of edgecases.py, and run:

python3 edgecases.py --target yourname --base <your base url>
Enter fullscreen mode Exit fullscreen mode

Most of the 23 cases only call check(). Keep real pay() calls rare, same as we did, since a few cases genuinely need a real call to show what a credential or a replay actually does, but there's no reason to make more real payments than that.

One hard rule carries over from the README and it's worth repeating here: never point an adapter at a service you don't have authorization to test. The suite ships exactly one working adapter, Pink's own public test-money sandbox, plus a template. No other vendor's API is referenced in the repo, authorized or not.

Run it, break it, send us what you find

If you're building spending controls for AI agents (budgets, approval thresholds, payee allowlists, anything that has to make a decision based on an amount, a currency, a time, or a name), this suite is free to point at your own stack. If you find a case that comes back looser than it should, or you think of an edge case we didn't cover, open an issue or a PR.

One thing to be upfront about: the $300 bounty from the Overspend Challenge is claimed. There's no more money on the table for this one, credit only, in the Hall of fame of the Pink Overspend Challenge repo. If that's still worth your time, we'd like to see what you find.

For more on how the original three bugs were found, there's a companion writeup here: The first person to beat our AI agent's spending rules did it with a space character.

Disclosure: I work on Pink Agentic AI Payments (PinkWallet).

Top comments (0)