ZyloPaydocs

Agent payments

Let an AI agent pay for a ZyloPay card holder with an agent key, and accept those payments as a seller.

A ZyloPay card holder can connect an AI agent (a script, an assistant, a service acting for them) to one of their passes. The agent gets an agent key. The key cannot move money by itself. For each purchase, the agent asks ZyloPay for a single-use payment token and hands it to the seller. The seller confirms a payment intent with the token from its server, and ZyloPay settles it like a tap at a terminal, so every check of a card payment applies.

This guide is for two audiences:

  • Agent builders: you build an agent, or a platform that runs agents, that pays for a ZyloPay user.
  • Sellers: you sell an API, data or content to agents and want to accept ZyloPay.
sequenceDiagram
  participant A as Agent
  participant S as Seller
  participant Z as ZyloPay
  participant P as Card holder's phone
  A->>S: Request a paid resource
  S-->>A: 402 with price and merchant ID
  A->>Z: Mint a payment token (agent key)
  Z-->>A: zpt_ token, 2 minutes, this merchant, at most this amount
  A->>S: Retry with the token
  S->>Z: Create a payment intent, confirm it with the token (secret API key)
  opt Held for approval
    Z->>P: Approve this payment?
    P-->>Z: Approve
  end
  Z-->>S: Intent succeeded, settled on Monad
  S-->>A: 200 with the resource

What an agent key is

  • The card holder creates it in the ZyloPay payer app (pay.zylopay.com), under AI agents. They name the agent, pick a pass and set its limits.
  • Bound to one pass and one network. Mainnet keys start with zpa_live_, testnet keys with zpa_test_. A key only ever pays from its own pass, on its own network.
  • Shown once. The app shows the full key one time. ZyloPay stores only a hash of it and can never show it again. A lost key is switched off and replaced.
  • Up to 5 active keys per pass.
  • Mints tokens, nothing else. The key cannot settle a payment, read the pass or act for the card holder in any other way.

The card holder can switch a key off at any time in the app. It stops at once: the next mint is refused, and tokens it minted but nobody used stop working.

Limits

Every payment passes all of these. The tighter one wins.

LimitSet byWhat happens
Per-payment limitCard holder, per agentA mint above it is refused with AGENT_AMOUNT_TOO_HIGH. The token also carries the amount, so a seller cannot charge more.
Monthly budgetCard holder, per agentCounts settled and in-flight payments in the current UTC calendar month. A payment that would go over is refused: at mint with AGENT_MONTHLY_LIMIT, at settlement with amount_exceeds_approval on the seller's payment intent. The budget resets on the 1st at 00:00 UTC.
Ask me every timeCard holder, per agentEvery payment by this agent waits for the card holder's approval on their phone (reason AGENT_APPROVAL_REQUIRED).
ExpiryCard holder, per agent30, 90 or 365 days, or never (default 90). After it, the key is refused with AGENT_KEY_INVALID.
The pass's spending approvalCard holder, when they create the passThe on-chain SpendingApproval caps each payment. A mint above it is refused with AGENT_AMOUNT_TOO_HIGH.
Payer confirmationZyloPay risk rulesSome payments are held until the card holder approves them: above the network's confirmation threshold, the first payment to a merchant, and others. See Payer confirmation.

A frozen account, a frozen or revoked pass, an expired approval and a restricted account stop the agent too. Nothing the agent sends can raise a limit.

Mint a payment token

Mint one token per purchase, right before you pay. Send the agent key in x-agent-key and the key's network in X-Network.

curl -X POST https://zylopay-api.fly.dev/api/agent/payment-tokens \
  -H "x-agent-key: $ZYLOPAY_AGENT_KEY" \
  -H "X-Network: testnet" \
  -H "Content-Type: application/json" \
  -d '{"merchantId": 1042, "amount": "2500000"}'
FieldTypeMeaning
merchantIdintegerThe seller's on-chain merchant ID on this network (1000 or more). Sellers publish it, for example in extra.merchantId of an x402 answer.
amountstringThe most the token may pay, in USDC atoms (6 decimals): 2500000 is $2.50. See Amounts.

X-Network is mainnet or testnet and must match the key: a zpa_test_ key only works with testnet. A mismatch is refused like an unknown key.

{
  "success": true,
  "data": {
    "paymentToken": "zpt_9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
    "expiresAt": "2026-09-28T14:05:31.000Z",
    "merchantId": 1042,
    "maxAmount": "2500000",
    "network": "testnet"
  }
}

The token:

  • is single-use: one settlement, then it is spent;
  • is valid for 2 minutes (expiresAt);
  • pays only this merchant, on this network, at most maxAmount. Presented anywhere else it is refused (credential_invalid on the seller's payment intent), and nothing is charged.

Minting charges nothing. A token nobody settles simply expires. The route allows 60 mints per minute per key.

Mint errors

Errors use the ZyloPay envelope: { "success": false, "error": "…", "errorCode": "…" }. None of them is fixed by sending the same request again.

StatuserrorCodeMeaningWhat to do
403AGENT_KEY_INVALIDThe key is unknown, switched off or expired, or X-Network does not match it.Stop paying. Ask the card holder to check the agent in the app.
400AGENT_AMOUNT_TOO_HIGHThe amount is above the agent's per-payment limit or the pass's spending approval.Do not split the purchase to get around it. Ask the card holder.
403AGENT_MONTHLY_LIMITThe payment would go over the agent's monthly budget.Wait for next month, or ask the card holder to raise the budget.
403PAYER_FROZENThe card holder's ZyloPay account is frozen.Stop paying.
403ACCOUNT_RESTRICTEDZyloPay cannot accept payments from this account.Stop paying. ZyloPay does not give the reason.
403PASS_REVOKED, PASS_FROZENThe pass can no longer pay.Stop. The card holder connects the agent to another pass.
403AUTHORIZATION_REQUIRED, AUTHORIZATION_EXPIREDThe pass's spending approval is missing or expired.Ask the card holder to renew the pass's approval in the app.
400—The body failed validation (merchantId below 1000, amount not a positive integer).Fix the request.
429RATE_LIMITEDMore than 60 mints in a minute for this key.Wait for Retry-After, then mint again.

Pay with x402

x402 uses HTTP 402 to ask for payment. ZyloPay payments use the scheme zylopay. The payload is a ZyloPay payment token, not a signed transfer, so the agent never holds a wallet key and never pays gas.

1. The seller asks for payment. A request without payment gets HTTP 402:

{
  "x402Version": 1,
  "error": "X-PAYMENT header is required",
  "accepts": [
    {
      "scheme": "zylopay",
      "network": "monad-testnet",
      "maxAmountRequired": "2500000",
      "resource": "/premium/report?topic=monad",
      "description": "Deep report",
      "mimeType": "application/json",
      "payTo": "0x81616A7e1b92F9FDc6B4Ae1bD8905084fA6bcc67",
      "maxTimeoutSeconds": 120,
      "asset": "0x534b2f3A21130d7a60830c2Df862319e593943A3",
      "extra": { "merchantId": 1042 }
    }
  ]
}

network is monad-testnet for testnet and monad for mainnet. payTo is the ZyloPay settlement contract and asset is USDC on that network. The merchant is extra.merchantId.

2. The agent mints a token for extra.merchantId and maxAmountRequired, on the matching X-Network. Check the price against your own budget first.

3. The agent retries with the token in X-PAYMENT, base64-encoded JSON:

{
  "x402Version": 1,
  "scheme": "zylopay",
  "network": "monad-testnet",
  "payload": { "credential": "zpt_9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08" }
}
const header = Buffer.from(
  JSON.stringify({ x402Version: 1, scheme: 'zylopay', network: 'monad-testnet', payload: { credential: paymentToken } }),
).toString('base64');

const res = await fetch(url, { headers: { 'X-PAYMENT': header } });

4. The seller confirms a payment intent with payload.credential as agent_token, then answers with the resource. See Accept agent payments as a seller.

The same token can also be handed to a seller any other way, for example in a paid tool call. What matters is that the seller confirms it once.

Accept agent payments as a seller

A seller accepts agent tokens on its own server, with its secret API key and the public API. Create a payment intent for the purchase, then confirm it with the agent's token. ZyloPay settles it at the intent's terminal like a tap, with every check of a card payment.

You need:

  • a merchant on the network and its on-chain merchant ID (merchant dashboard). Agents need the ID to mint. Mainnet needs an approved business verification;
  • a secret API key with payments:write (zp_test_sk_… for testnet, zp_live_sk_… for mainnet). Keep it on your server. See Authentication;
  • a terminal for online sales on that network, for example named "Online / AI agents" (dashboard, Settings → POS Terminals → Add Terminal). Do not set up a POS device with its key. Its payments count toward that terminal's daily cap and show under its name.

Use a terminal no POS device uses

A POS device picks up the intents created for its terminal and shows them at the counter. While a device is presenting an intent, confirm answers 409 payment_intent_unexpected_state. Create agent intents only on your online terminal.

1. Create a payment intent when the agent's token arrives. The token is valid for 2 minutes, so do not create intents ahead of time. Idempotency-Key is required.

2. Confirm it once with the token, passed on unchanged. Idempotency-Key is required here too. The intent's amount must fit the token: same merchant, same network, at most its maxAmount.

import ZyloPay from '@zylopay/node';

const zp = new ZyloPay(process.env.ZYLOPAY_SECRET_KEY!);

// purchaseId: your own ID for this sale. agentToken: the agent's zpt_ token, unchanged.
async function chargeAgent(purchaseId: string, agentToken: string) {
  const intent = await zp.paymentIntents.create(
    { amount: 2500000, terminal: 'tml_ZP-7730-Q', description: 'Deep report' }, // 2.50 USDC
    { idempotencyKey: `intent-${purchaseId}` },
  );
  return zp.paymentIntents.confirm(
    intent.id,
    { agent_token: agentToken },
    { idempotencyKey: `confirm-${purchaseId}` },
  );
}

The API key chooses the network, so no X-Network header is needed. Give the confirm call up to 60 seconds: ZyloPay answers once the payment is on-chain. If the call times out, or answers 503 request_timeout, send it again with the same Idempotency-Key: it never runs twice, and you get its real outcome. See Idempotency.

3. Follow anything that is not final with the payment_intent.* webhooks, or GET /v1/payment_intents/{id}. Never confirm the same intent again to find out what happened.

Confirm accepts only an AI agent's token. A pass's NFC ID or wallet token is refused with 400 parameter_invalid (param: "agent_token"): those credentials are read by a terminal and never reach your servers. Confirm also refuses an intent that is not requires_payment_method (409 payment_intent_unexpected_state) and an intent whose terminal was deactivated (409 terminal_inactive). Reference: Confirm a payment intent.

Outcomes

The confirm answer is the payment intent. A decline is not an HTTP error: it is on the intent.

Intent statusMeaningSeller doesAgent does
succeededSettled. USDC moved on Monad.Deliver, for example 200 with an X-PAYMENT-RESPONSE receipt.Use the result. Record the transaction.
processingSent, outcome not confirmed yet. ZyloPay resolves it.Answer 202 with a way to poll. Wait for payment_intent.succeeded, or retrieve the intent until it is final.Poll. Never pay again for the same purchase.
requires_actionHeld for the card holder's approval on their phone. next_action.reason_code says why (AGENT_APPROVAL_REQUIRED when the agent is set to ask every time), next_action.expires_at when it expires.Answer 202 with a way to poll. Wait for payment_intent.succeeded or payment_intent.canceled. An approved payment settles with no further call.Poll. Never pay again for the same purchase.
requires_payment_method with last_payment_errorDeclined. Nothing was charged.Answer 402 and say it is final.Report it. Do not retry the same payment.

An agent payment gives the card holder 5 minutes to approve (60 seconds at a POS). If they decline or do not answer, the intent becomes canceled with cancellation_reason step_up_denied or step_up_expired, and nothing is charged. The agent may start a new purchase with a new token after that, never before. See Payer confirmation.

last_payment_error.code on a declined agent payment:

CodeMeaning
credential_invalidThe token is spent or expired, was minted for another merchant, network or a smaller amount, or the agent was switched off before the payment went through.
amount_exceeds_approvalThe payment would go over the agent's monthly budget, or above the pass's spending approval.
payer_frozen, account_restrictedThe card holder's account cannot pay. ZyloPay does not give the reason.
pass_revoked, pass_frozenThe pass can no longer pay.
authorization_required, authorization_expiredThe pass's spending approval is missing or expired.
terminal_daily_limitYour online terminal reached its daily volume cap.
risk_declinedZyloPay's risk rules declined the payment.

A decline also sends payment_intent.payment_failed. Every code is listed in payment decline codes.

Security

  • Treat the agent key as a secret. Keep it in a secrets manager or the agent platform's secret store, and read it only in the code that calls ZyloPay.
  • Never put the key in a prompt, a model's context, a tool result, logs or client code. A model does not need it: give the model a "pay" tool and keep the key behind it.
  • One key per agent. The card holder then sees and switches off each agent on its own.
  • Switch it off on suspicion. In the payer app, AI agents → Switch off. It stops at once. Then create a new key: keys are never shown again.
  • Tokens are short-lived by design. Mint right before you pay. Do not cache tokens or send them to anyone but the seller.
  • Sellers: keep the secret API key on your server, confirm each token once with a fixed Idempotency-Key, and never confirm again to follow a payment. Do not log tokens. See Security best practices.

Test on testnet

Testnet is the Monad testnet with test USDC. It runs the same checks as mainnet. See Testing your integration.

  1. In the payer app, switch the network to testnet in Settings.
  2. Get testnet USDC from faucet.circle.com: choose Monad Testnet. Get testnet MON from faucet.monad.xyz. MON is needed only for the one-time USDC approval in the app; agents and sellers never pay gas.
  3. Create a pass, then AI agents → Connect an agent on it. Store the zpa_test_ key.
  4. As a seller, create a testnet merchant, a test API key with payments:write and a testnet terminal for online sales. No POS device and no business verification are needed on testnet.
  5. Pay once from the agent and approve on the phone: the first payment to a merchant asks for approval.

Test the refusals too: an amount above the per-payment limit, a key you switched off, a token confirmed twice and a token presented to another merchant (credential_invalid).

On this page