Last week I posted about a family of MCP servers that let AI agents accept payments — Pix in Brazil, UPI in India, GCash in the Philippines, PromptPay in Thailand, one stateless server per country.
The first substantive reply came from someone who works on agent guardrails. Their point, paraphrased:
Trust boundaries around money custody are solved by your design (the server never holds funds). But there's a second boundary you haven't addressed: a well-formed request can still be a request the agent should never have made. Amount limits, allow-lists, human-approval thresholds — checked deterministically before anything is signed.
They were right, and the interesting part was figuring out how to do this statelessly — these servers have no database, no accounts, no config storage. Credentials ride on HTTP headers per request. Where does policy live?
The answer: policy rides the same channel as credentials
The MCP client config (where a human pastes API keys) is something the agent cannot touch. Model output never edits claude_desktop_config.json or a .mcp.json. So policy set there has a property no in-band instruction has: the model cannot relax its own limits.
Two headers, checked before any signature is computed:
x-agentpay-max-amount: 1000
x-agentpay-approval-above: 100
-
x-agentpay-max-amount— hard cap in the local currency's major unit. Anything above is refused with a readablePOLICY_BLOCKEDerror that tells the agent to ask the human, not to retry. -
x-agentpay-approval-above— softer: amounts above return an unsigned draft (all payment parameters,payment_url: null,approval_required: true) for the human to review. There is deliberately no bypass parameter — the only way to proceed is for the owner to change the header in their client config.
A weak local model can't be prompt-injected past this, because the check isn't in the prompt. It's a deterministic if before the crypto.
// before any PSP call or signature:
const gate = policy.enforce(headers, amountMajor);
if (gate.needsApproval) return policy.draftResult(field, amountMajor, description, gate.threshold);
From suggestion to 30 countries
The same ~40-line policy.js dropped into every server, because they all share one shape: validate → policy gate → sign/call → return hosted checkout URL. The rollout:
- Day 0: shipped to the 20 live countries (Asia + Latin America + US/UK/NL/SG/NG), behavior tests + full e2e regression.
- Day 1: baked into the country generator, so the next wave was born with it — Turkey (iyzico), Saudi Arabia & UAE (Tap / mada), Kenya (Flutterwave / M-Pesa), Ghana & South Africa (Paystack), Poland (Stripe / BLIK), Germany & Belgium (Mollie / Klarna / Bancontact), France. That's 30 countries, every one policy-gated from its first request.
Each server's e2e suite now asserts both branches with a fake key against the real gateway:
PASS policy cap -> POLICY_BLOCKED
PASS policy approval -> unsigned draft
And the daily fingerprint canary (a watchdog that hits every gateway with fake keys and asserts the error fingerprint hasn't changed) grew to 30 endpoints / 36 canaries.
Takeaways
- Policy for AI agents belongs outside the model. Headers set by a human in client config are a surprisingly good policy channel: zero storage, per-merchant, and structurally out of the agent's reach.
- Draft mode beats refusal for the human-in-the-loop case. Returning the parameters (but no live link) gives the human something to approve instead of a dead end.
- Community feedback compounds when your architecture is uniform. One suggestion became a family-wide feature in a day because all 30 servers share one skeleton.
The family hub with all 30 endpoints: mcp.wishpool.app — each country page's llms.txt documents the two policy headers. All open source (MIT), all on the official MCP Registry under app.wishpool/*.
If you're building agent guardrails and see a boundary I've missed — that's exactly the kind of comment that turned into this feature. 🙏
Top comments (0)