Agentic Payments
Software is starting to buy things. Not "a script hits your API" — that has always happened — but an assistant that a person told to sort out the workshop tickets, deciding on its own what to buy, from whom, and when.
That breaks an assumption every payment API was built on: that a human is present, and their click is the authorisation. Four protocols have grown up to replace it, and they answer four different questions.
| Protocol | Question it answers | Where it runs |
|---|---|---|
| MCP | How does an agent reach your payment tools? | Between the agent and your systems |
| AP2 | How do you prove a human authorised this? | In the request, as signed mandates |
| ACP | How does a checkout run inside someone else's chat? | Between a shopping platform and your endpoints |
| A2A | How does one agent hire another? | Between agents |
They compose rather than compete: an ACP checkout can carry an AP2 mandate, and an A2A task can be what one agent sends another to start it.
The SDK implements the merchant side of all four.
MCP — giving an agent your payment counter
The platform ships an MCP server for merchants. It is not on a public registry, so it installs from its repository:
npm install -g --install-links "git+ssh://git@git.gaml.store/heth_project/4pay-payments-mcp.git"
--install-links is not optional: without it npm links its own temporary clone during a global
install and then deletes it, leaving a broken symlink and a cheerful added N packages.
// .mcp.json
{
"mcpServers": {
"fourpay": {
"command": "4pay-payments-mcp",
"env": {
"FOURPAY_API_URL": "https://sandbox.4pay.online",
"FOURPAY_API_KEY": "${FOURPAY_API_KEY}",
"FOURPAY_ORGANIZATION_ID": "${FOURPAY_ORGANIZATION_ID}",
"FOURPAY_ENV": "test"
}
}
}
}
Ten tools: create_payment, create_payout, get_payment, list_payments, refund_payment,
cancel_payment, create_payment_link, list_payment_links, list_saved_cards,
generate_order_id.
There is a second MCP server, @4pay.online/mcp-server, for the people who run the platform:
terminals, routing rules, limits, FX. It is a different program, not a mode.
An agent with routing tools in reach will, sooner or later, try to "fix" live routing while
working through a declined payment. No system prompt reliably prevents that; a tool that is not in
the list does. Merchants get @4pay.online/payments-mcp.
One server, one environment. env is not a tool argument — a model that could choose it could
be talked into choosing wrong. Run a second server if you need both.
Without MCP
An agent does not need a protocol to take payments. The SDK exports the same operations as function-calling definitions for any model:
import { paymentTools, executePaymentTool } from "@4pay/sdk/agentic";
const policy = {
env: "test",
currency: "EUR",
maxAmount: "50000", // 500.00 — enforced here, not in the prompt
allowRefunds: false, // refunds move money outwards; that is a human's decision
};
const tools = paymentTools(policy); // hand these to the model
const result = await executePaymentTool(pay, policy, name, args); // run what it picks
Amounts are minor-unit strings and a decimal is refused before it reaches the platform: models are
fluent, not exact, and 40.00 is not a number to let one calculate with.
AP2 — proving the human agreed
An AP2 mandate is a signed statement of what a person authorised. Two of them matter to a merchant:
- an Intent Mandate — standing permission, signed while the user was present: buy these tickets if they drop below 120 EUR;
- a Cart Mandate — a signed, itemised cart: this merchant, these lines, this total, until this moment.
Verify before charging:
import { ap2 } from "@4pay/sdk/agentic";
const cart = ap2.verifyCartMandate(jws, { keys: issuerKeys, merchantId: "merchant-1" });
ap2.assertWithinIntent(cart, intent); // did the human ever allow a purchase like this?
const transaction = await pay.createPayment(
ap2.cartMandateToPayment(cart, { env: "prod", jws, modality: "human_absent" }),
);
verifyCartMandate checks the signature, that the cart is addressed to you, that it has not
expired — and that the total equals the sum of its lines. That last one catches the attack
nobody expects: a cart with three items worth 300 and a total of 1. Both are signed; only one is
true.
The mandate then travels with the payment in merchant_meta.ap2, so the authority behind it stays
attached in reporting and disputes.
The platform does not forward a Payment Mandate to card networks as an AP2 credential — no rail we route to accepts one today. If a scheme starts requiring it, that is a platform change, not an SDK change.
Delegated tokens — letting the agent actually pay
Verifying a mandate proves the human agreed. It does not, by itself, move money: the agent still needs something to pay with, and handing it a card number would defeat the point.
A delegated token is that something. The organization issues it against one of its own instruments — a wallet, an issued account, an issued card — and what the agent receives is not a credential but a set of limits: no more than this amount, in this currency, at this merchant, until this moment, this many times. The instrument is never named in the token, and cannot be recovered from it.
You charge it like any other payment method:
POST /api/v1/transactions
{
"params": {
"type": "payment",
"amount": "12000",
"currency": "EUR",
"txid": "order-12345",
"env": "prod",
"money_storage": {
"type": "delegated_token",
"token": "dpt_…",
"mandate": "eyJhbGciOiJFUzI1NiIs…"
}
}
}
mandate is the Cart Mandate JWS and is optional — an organization may issue a delegated token on
its own terms, without AP2 at all. When it is present, the platform verifies it server-side
before charging: signature against the organization's registered issuer keys, the merchant it is
addressed to, its expiry, and that the total equals the sum of the lines. The SDK's
verifyCartMandate remains useful for failing early in your own code, but it is no longer the
thing that stands between a forged cart and a charge.
Three checks run in order, and each refuses before anything is spent:
- the token itself — not expired, not revoked, uses remaining, amount within scope, and issued for your partner if it was scoped to one;
- the mandate, if you sent one;
- the organization's own limits, which are ordinary platform limits: the terminal's minimum and maximum for a single payment, and anti-fraud rules on that terminal for daily, weekly and monthly sums. Agentic payments route through their own terminal, so these apply to them and to nothing else.
The right is consumed at settlement, in the same database transaction as the ledger movement. A payment refused by routing, limits or anti-fraud costs the holder nothing: the permission a person gave is not spent on someone else's rejection.
A payment that clears these three and then exceeds a terminal or anti-fraud limit is refused the
ordinary way — 201 Created with "status": "rejected" — because by then the transaction exists.
Read status, as everywhere else.
One consequence worth planning for: the right is consumed against the amount that actually leaves the instrument, not the amount you sent. Where the organization's terminal adds its fee on top of the purchase, those are two different numbers, and a charge that fits inside the token at create time can still be refused at settlement for exceeding it. Ask your operator which way fees are configured before you size the tokens you request.
Refusals arrive before a transaction exists, so — unlike routing failures — the HTTP status tells you the truth:
| Status | Body error | Meaning |
|---|---|---|
403 | module_not_active | The organization has not activated agentic commerce |
404 | delegated_token_invalid | No such token |
409 | spent, expired, revoked | The right existed and no longer applies |
422 | amount_exceeds_scope, currency_mismatch, partner_mismatch | The charge is outside what was granted |
422 | human_confirmation_required | The organization requires a person to be present, and the mandate says otherwise |
422 | instrument_inactive, instrument_currency_mismatch | The card or account behind the right cannot pay this |
422 | instrument_not_owned | The right names an instrument its holder does not own |
The transaction that results records who initiated it — a person, an agent with the human present, or an agent acting alone — together with the agent and the mandate behind it. That record is set by the platform, never taken from the request: a limit on agentic payments that trusted the caller's own word about being an agent would only ever restrain the honest.
Delegated tokens are issued by the organization, not by you. Before building against them, confirm that agentic commerce is switched on for the organization you integrate with and that a terminal exists for the instrument you intend to charge — a wallet, an account and a card are routed separately, and each needs its own.
ACP — selling inside someone else's chat
With ACP, the buyer never reaches your site. An agent reads a product feed, builds a cart, settles shipping and tax, and only then pays.
The platform hosts that conversation for you. You publish products; the platform serves the feed and holds the checkout state:
GET /api/v1/acp/feed what you sell
POST /api/v1/acp/checkout_sessions start a checkout
GET /api/v1/acp/checkout_sessions/:id where it stands
POST /api/v1/acp/checkout_sessions/:id change the cart or the buyer
POST /api/v1/acp/checkout_sessions/:id/cancel abandon it
The agent platform calls these with your API key, and the seller is whoever that key belongs to — never a party named in the body.
The platform prices the cart, not the agent
The agent sends SKUs and quantities. What they cost comes from your feed, on every request, including repricing after a change. This is not politeness — it is the only place the substitution is visible: an AP2 mandate signed over an agent-supplied price is perfectly valid, because the issuer signed what it was handed.
Only products you have published appear and only they can be bought. feed_enabled defaults to
off: a catalogue row is not a shop window until you say so.
Idempotency
Send Idempotency-Key on session creation. Agents retry on timeout — that is normal protocol
traffic, not a fault — and a repeat returns the same session instead of a second order.
Signed requests
If your agent platform signs its requests, send x-agent-signature (base64 of the signature over
the raw body) and x-agent-key-id. The key is verified against the same trust registry that holds
your AP2 issuer keys, and the algorithm comes from the registry, not from the request.
Signing is optional — the API key is the primary credential — but a signature that does not verify
is refused with 401. A checked signature that could be wrong would be worse than none.
What comes back
A session carries its status (not_ready_for_payment → ready_for_payment → completed, or
canceled), the priced items, the totals, and messages — protocol-level explanations of what is
missing or unavailable, meant to be shown to the buyer verbatim.
Payment itself is an ordinary transaction on the platform: a delegated token, a payment link, or a saved card. There is no second money path for ACP, and that is deliberate — fees and the ledger are computed in exactly one place.
Order updates
Pass order_update_url (https only) when creating the session, and the platform posts order events
back to the agent platform with the same delivery guarantees as any other webhook: retries with
backoff and a dead-letter queue. Nothing about your order's fate depends on that platform being
reachable at the moment the money moves.
ACP's delegated-payments flow can have the agent hand you a single-use token minted by the buyer's own payment provider. The platform does not accept third-party vault tokens today. What it does accept is a delegated token it issued itself — see above — which covers in-chat completion for instruments the organization already holds.
A2A — one agent hiring another
A2A is agent-to-agent work assignment: your agent publishes a card saying what it can do, another sends it a task, and polls until it is finished. It carries no money itself — it carries the request.
import { a2a } from "@4pay/sdk/agentic";
const agent = new a2a.A2APaymentAgent({
client: pay,
env: "prod",
url: "https://agent.example/a2a",
maxAmount: "50000", // a ceiling, because whoever has the URL can send tasks
});
app.get("/.well-known/agent-card.json", (_, res) => res.json(agent.agentCard));
app.post("/a2a", async (req, res) => res.json(await agent.handle(req.body)));
Three skills: create_payment, payment_status, refund_payment.
Put authentication in front of it. An A2A endpoint is reachable by anyone holding its URL, and its tools move money. The SDK enforces an amount ceiling; the rest is your agent mesh's job.
Rules that hold across all four
Idempotency is not optional. Agents retry — on a timeout, on a dropped connection, on their
own second thoughts. Every payment carries a txid, and repeating it returns the original rather
than charging twice. The SDK uses the cart id, the checkout session id or your order id
accordingly.
A ceiling belongs in code. Prompts are suggestions, and anything a customer types is untrusted
input that will eventually try. maxAmount is checked before the request leaves.
Refunds are off by default. They move money outwards, and that decision belongs to a person.
charged is the only settled status. Anything else — including a status the SDK has never
seen — is not money in your account. Agents are optimistic readers; do not let one tell a customer
their order is placed because the HTTP call succeeded.
Examples
| Example | Protocol |
|---|---|
agent-tools/ | Function calling, no protocol |
acp-merchant/ | ACP checkout endpoints |
a2a-agent/ | A2A agent card and JSON-RPC |
ap2-checkout/ | AP2 mandate verification |
All four ship in the SDK archive and run against the sandbox.