Core concepts

Security model

Trust boundaries, which routes are authenticated, and what the gateway deliberately does not defend.

Agent Commerce Gateway is not a payment processor, wallet, exchange or custodian. The payment flow never needs a merchant or buyer private key. Local facilitator mode does hold a separate key that pays gas, and experimental ERC-8409 quotes need a separate, unfunded signing key; never reuse a merchant or buyer key for either. Signing a quote moves no funds.
No commissioned security auditThe 2.x line commits to a stable public API and wire contract. It makes no assurance claim about the payment path, and there is no third-party audit report for any release. Weigh that before putting production funds through it.

Trust boundaries

text
       UNTRUSTED               GATEWAY PROCESS               TRUSTED
  ───────────────────    ─────────────────────────    ────────────────────────
  agent input      ────► validates agent input  ────► merchant backend
  payment proofs         may hold a local             administrator configured
  authorization proofs   facilitator gas key
  protocol traffic
                         configuration          ◄──── administrator
                         environment            ◄──── operator

Everything from an agent is untrusted and validated. Configuration and environment variables are trusted operator input — which is why only they may select backend URLs.

Which routes are authenticated

The gateway checks the Host header on every request to limit DNS rebinding. Requests with an Origin header must also match server.allowedOrigins.

RouteAudienceProtection
POST /api/resources/:id/invokeagentspayment, not authentication
/mcp, /a2aagentspayment, not authentication; only when enabled
GET /api/resources, /health, /.well-known/agent-commerceanyonenone — public by design
/.well-known/agent-card.json, /.well-known/acp.jsonanyonenone; only when A2A or ACP is enabled
GET /readyoperatorsnone, but a fixed vocabulary, never raw errors
GET /api/receipts, /api/events, /api/payment-attempts, /api/transactions; POST /api/payment-attempts/:id/resolveoperatorsserver.adminToken, constant-time
/acp/checkout_sessions…agentsprotocols.acp.auth.token, constant-time

A disallowed Origin gets 403 before the token check. Otherwise, operator routes return 404 when no server.adminToken is configured, or 401 for a missing or wrong token when one is configured. These checks precede ledger access. The token is accepted only in the Authorization header, never as a query parameter. It does not identify a person; resolutions record a caller-supplied actor. Browser access is governed by server.allowedOrigins, an explicit allowlist that defaults to empty; requests without an Origin receive no CORS headers at all.

ACP Signature and Timestamp verification are not implemented, and a Signature header never replaces the bearer token, so deploy ACP behind TLS. The adapter forwards both headers to the merchant unverified. It never forwards the bearer token, and a configured backend.headers entry wins over a caller header of the same name.

Secret handling

  • The default request log records the method, the URL with any query string replaced by ?[REDACTED], the host and the remote address and port, but no request headers.
  • The logger redacts the Authorization, PAYMENT-SIGNATURE and Agent-Authorization headers and known secret fields such as privateKey, signerPrivateKey, challengeSecret, adminToken and token, at the top level and one level deep — not deeper.
  • Receipt redaction is recursive, and the pipeline keeps raw payment and authorization proofs out of receipts.
  • A facilitator's rejection reason reaches clients and storage only after trimming and a 128-character [A-Za-z0-9_.-] check; anything else becomes invalid_payment or settlement_failed. The raw value, with any URL cut to its origin, appears only in logs: at debug level for a rejection, at warn level when the outcome is unknown.
  • Discovery publishes only each x402 offer's health status. The reason an offer fails, which can name an RPC host, is logged at debug level.
  • Environment-substitution errors name the variable and path, never the resolved value. Later validation errors may echo non-secret values such as URLs, so keep secrets in fields meant for credentials.

SSRF and backend calls

  • Backend URLs are administrator configuration only. Agent input only fills {param} placeholders in the path or query, URL-encoded; a placeholder in the scheme, host or port is refused at load.
  • Empty, . and .. path parameters are rejected as INPUT_INVALID. After substitution, the path must still start with the template's literal prefix.
  • Input cannot replace a query parameter already fixed in backend.url; a collision is INPUT_INVALID.
  • Redirects are not followed; a 3xx is a BACKEND_ERROR.
  • The built-in HTTP executor applies a timeout and a 1 MiB response cap to every call. A custom backend executor must enforce its own.
  • The backend's error body is never relayed to the caller, only its status code; ACP also relays a valid ACP error's type, safe code and param. At most 512 characters of it are logged at debug.
There is no IP/CIDR allowlist. If you configure http://169.254.169.254/…, the gateway will call it. Treat configuration as privileged.

Denial of service

LimitValue
request body256 KiB, on parsed routes and raw protocol mounts alike; a raw mount answers 413 when it still can
backend response1 MiB, plus the configured timeout
requests per mounted protocol adapter20 concurrent, checked before the body is read — then GATEWAY_BUSY with Retry-After: 1
concurrent MCP tool calls8, plus a queue of 64 — then GATEWAY_BUSY
receipts and events queriesa bounded list limit
Agent-Authorization header8192 bytes, checked before decoding
unpaid A2A x402-extension tasks256 unexpired tasks in the receipt database, each expiring with its requirement or within an hour; a full store fails new purchases with GATEWAY_BUSY and never evicts a waiting task
X-Request-Idlogged only when it matches [A-Za-z0-9._:-]{1,64}; the audit request id is always generated by the gateway
/ready and /.well-known/agent-commercecached for 2 seconds; concurrent requests share a probe, limiting repeated upstream checks
signed ERC-8409 quotesup to 1024 cached per provider, reused while a full payment window remains; each cache miss signs once per EVM offer, so rate-limit unpaid requests at the edge
resolve request body4 KiB, then 413
A2A message parts and task referencesRejects multiple parts or any task reference by array length (PROTOCOL_UNSUPPORTED), before validating elements
There is no rate limitingA free resource is an unauthenticated proxy to your backend at whatever rate a caller chooses. Quotas and abuse controls belong in your API or your edge.

Multi-network payment checks

A selection from the adversarial matrix for offers on several networks, methods and chain families. A test covers each row.

ScenarioResult
a proof whose accepted echo matches zero or several of the challenge's offersinvalid_payment_requirements; the facilitator is not called
a proof for an offer missing from the resource's own x402.accepts listrefused before the facilitator is called
an AP2 mandate approved for one offer, presented with a proof for anotherthe selected destination, network or asset fails mandate binding
an EIP-3009 authorization signed for one EVM chain, presented as another chain's offerinvalid_exact_evm_signature: the signature fails against the other chain's EIP-712 domain
a Permit2 proof naming another spender, token, amount or witness recipientinvalid_permit2_spender, permit2_token_mismatch, permit2_amount_mismatch or invalid_permit2_recipient_mismatch, before the facilitator is called
a Permit2 payment from a buyer without a Permit2 allowance412 with permit2_allowance_required in a fresh PAYMENT-REQUIRED
the same Solana transaction with other signature bytes, while the first copy is settling409 PAYMENT_REPLAYED: the key is the message hash, not the signatures
another NEAR delegate under the same sender, key and nonce, while the first is settling409 PAYMENT_REPLAYED: the key is the access-key nonce, not the delegate bytes
a settlement success naming another network, or no transaction in its family's formsettlement-uncertain, no delivery
an EVM token whose decimals differ from assetDecimalshealth fails, naming both values
x402 options on two networks sharing one RPC endpoint, or mixing testnet and mainnetCONFIG_INVALID at load

Buyer protection

Buyer wallets or agent providers must enforce budgets, per-task limits and kill switches. The merchant's gateway handles each purchase separately.

  • x402's default authorization flow settles only after backend success. upfront, the MPP default, settles first. A later backend failure leaves the buyer charged; the gateway reports settlement and attempts to record an undelivered receipt. See the fail-closed matrix.
  • Replay checks stop a second settlement with the same authorization; buying loops can use fresh authorizations. See replay.
  • A resource can require an experimental AP2 mandate bound to its exact input, price and settlement terms. Open mandates are rejected because their spending limits are not evaluated. See AP2 mandates.
  • Operators correlate receipts and events by requestId at /api/receipts and /api/events. These records are not dispute-grade evidence.

Payments settle directly to the merchant; the gateway holds no buyer funds and needs no merchant wallet key. Refunds come from the merchant's wallet. Use GET /api/receipts?undelivered=true to find paid purchases whose delivery failed.

An uncertain settlement leaves no receipt even though funds may have moved. Use GET /api/transactions?unresolved=true to find journal entries awaiting a payment or delivery outcome; GET /api/payment-attempts?uncertain=true also covers attempts written before the journal. agent-commerce reconcile checks EVM, Solana and NEAR evidence and resolves attempts with a definite outcome. Retry inconclusive results later or investigate on chain, using the recorded transaction ID when available. To record an operator verdict, use agent-commerce resolve or POST /api/payment-attempts/:id/resolve. doctor reports the count when it is nonzero.

Manual resolution refuses a reserved attempt younger than 15 minutes, and refuses rejected while the recorded expiry is in the future. Without a valid recorded expiry, as on Solana and NEAR, rejecting needs an explicit confirmation that the authorization can no longer land. Neither verdict is checked on chain.

The gateway tries to save the receipt before sending the response. If the connection then drops, a saved receipt for a successful backend call still counts as delivered and is excluded from ?undelivered=true. The gateway does not resend that response and rejects the settled proof on retry. The buyer can make another paid request or ask the merchant for a refund.

What the gateway does not protect against

  • It does not secure your backend — authentication and data protection stay yours.
  • It does not vet the buyer: no KYC, sanctions screening, fraud scoring or disputes. An AP2 mandate proves a human approved the purchase, not who that human is.
  • It does not make payments reversible: no refunds, chargebacks or escrow.
  • It does not audit x402, the MCP SDK or their dependencies, which are third-party code.
  • It does not provide multi-tenancy, RBAC or policy controls.
  • It does not defend against a compromised host.
  • Payment and delivery can diverge. Under the upfront flow, a backend failure after settlement leaves the payment settled; the gateway records the attempt and an event for reconciliation and issues no refund. Under the authorization flow, the x402 default, a backend failure prevents settlement, but a refused or uncertain settlement withholds the response although the backend ran. Use upfront, the MPP default, when backend side effects must follow payment.
  • It cannot always tell whether a payment settled: a settlement with no verdict is recorded as settlement-uncertain, with its transaction hash when there is one, and the resource is not delivered. The caller gets PAYMENT_SETTLEMENT_FAILED, which does not mean the funds stayed with the buyer. Operators can list these attempts at /api/payment-attempts and check them with reconcile. Inconclusive evidence calls for a later retry or operator investigation.
  • A rejected attempt releases its payment authorization: the backend failed before settlement under authorization, or the provider confirmed that no funds moved. x402 buyers can retry the same authorization; it is verified again, and on-chain replay checks prevent double settlement. MPP buyers need a new challenge because credentials are single-use. A settlement-uncertain attempt keeps its authorization reserved, even if no funds moved, to prevent replay; any further payment needs a fresh authorization. AP2 mandates are also released on rejection and held on uncertainty.
  • It does not confirm a remote facilitator's report on chain. The facilitator, with the Solana fee payer or NEAR relayer that pays for broadcasting, is trusted with liveness and with its settlement report, not with the payment terms: the buyer's signature fixes the token, amount and recipient, but the facilitator can delay or drop a broadcast until the authorization expires. A reported success counts once it names the offer's network and a transaction in that family's form.
  • Under the upfront flow, a rejected request can still have been paid for. Most request-shape checks run before payment, but an unparseable URL and a path that escapes the template's prefix are caught only at the backend call, which follows settlement in that flow. Under the authorization flow, a failure at this stage prevents settlement.
  • It prioritizes delivery over bookkeeping: if persisting a receipt, payment attempt or journal step fails after reservation, the delivery or original error stands. The failure is logged at error; delivered outcomes list failures known at that point in persistenceFailures.
  • After a restart, the journal shows the saved steps and unresolved entries. For reserved attempts past --min-age, reconcile can resolve payment from conclusive final chain evidence. Unknown delivery outcomes still need operator review.
  • Restoring a backup loses newer local records. A proof spent since the snapshot reaches the facilitator again: EVM verification rejects it; Solana and NEAR rely on chain rejection of processed transactions or nonces. Under the authorization flow, a replay can reach the backend before settlement fails. A mandate consumed since the snapshot can authorize another purchase until expiry; see Restore.

Report vulnerabilities privately through GitHub's Report a vulnerability, not in a public issue. You get an acknowledgment within 5 working days; the policy is in SECURITY.md. The full adversarial scenario matrix, with the test covering each case, is in docs/security.md.