No commissioned security audit. The
v1.xline commits to a stable public API and wire contract; it makes no assurance claim about the payment path. There is no third-party audit report to point you at, for any release. Weigh that before putting production funds through it.
Please report security issues privately - do not open a public issue.
- Use GitHub's Report a vulnerability (Security → Advisories) on this repository: https://github.com/devlab-group/agent-commerce/security/advisories/new. That is the only private reporting channel; this project publishes no maintainer email address.
- Include the affected version or commit, a description, reproduction steps, and the impact you believe it has.
- You will get an acknowledgement within 5 working days, and a status update at least every 10 working days until it is resolved.
- Please give us 90 days before public disclosure, or less by agreement if a fix ships sooner.
We credit reporters in the release notes unless you would rather we did not.
- The deliberately public Anvil development keys and the local demo chain.
- The demo merchant API's failure-injection routes (
/api/slow,/api/fail), which exist only to test the gateway. - Anything in what the gateway does not protect against, which is documented rather than overlooked.
Agent Commerce Gateway is not a payment processor, wallet, exchange or custodian.
- The gateway never accepts, stores, derives or requires a merchant or buyer production private key or seed phrase.
- The merchant settlement destination is plain configuration
(
payments.x402.payTo: ${MERCHANT_WALLET}), an address the merchant controls. It is never a gateway-owned wallet. - Funds move buyer → merchant destination through the payment protocol
itself. With x402
exact/EVM this is an EIP-3009transferWithAuthorization: the buyer signs an authorisation that names the merchant as recipient, so a facilitator that broadcasts it cannot redirect the money. - The money path end to end:
docs/payment-flow.md.
- No real private key is ever committed to this repository.
- The only keys present are Anvil's well-known deterministic development
accounts, used exclusively on the local demo chain. Every occurrence is
labelled
LOCAL DEVELOPMENT ONLY - DO NOT FUND. Anyone can spend from them; they are public knowledge. Never send real assets to those addresses. - The buyer key used by the demo is Anvil well-known account #2, defined in
src/payments/x402/local-chain/accounts.ts(LOCAL_BUYER_ACCOUNT) and written into.deploy/local.jsonbynpm run chain:deploy; the demo agent reads it from that manifest. The gateway never reads it and never signs with it. Key locations in this document are stated literally: if one moves, this bullet is wrong until someone updates it. - The local facilitator signer pays gas on the dev chain only. Production deployments point at an external facilitator instead.
- Fail-closed paid resources. A paid resource is delivered only after a
successful
verify, a successful replay reservation, and a successfulsettle. Missing, malformed, expired, replayed, wrong-amount, wrong-recipient, wrong-network and wrong-asset payments all fail closed, and each has a test. - Replay defence at the gateway, not only on-chain. EIP-3009's
authorizationStateprevents a double spend, but a replayed authorisation could otherwise unlock a second delivery before the first settles. The payment provider derives areplayKeyfrom the authorisation (payer, nonce, asset, network) and the pipeline reserves it under aUNIQUEconstraint before settling. A duplicate isPAYMENT_REPLAYED. - Purchase authorisation, where a resource requires it. With AP2 enabled, a
paid resource can demand a signed Checkout Mandate proving the human behind
the agent approved that exact purchase. It is verified and reserved before
settlement, spendable once, and never a substitute for payment. Trust is
static public keys in configuration, with no key discovery of any kind. See
docs/ap2.md. - Bounded backend calls. Every merchant backend call has an explicit timeout and a 1 MB cap on the response body: a timeout bounds a call by time, not by bytes. There is no unbounded outbound HTTP request.
- Host-header (DNS-rebinding) validation. Every request, browser or not, is
checked against the configured host allow-list before routing. A rebinding
attacker's page reaches the gateway with its own hostname in
Host, never a configured one, so the request is refused before it can drive the operator routes from inside the victim's network. Seesrc/gateway/access-control.ts. - Configuration validated before startup. Invalid configuration fails the process rather than starting a half-configured gateway.
- Secrets kept out of logs and storage. The request serializer emits only
method, sanitised URL, host and remote address, so request headers never
reach a log line in the first place. Behind that, the logger redacts every
header that carries a credential or a proof -
Authorization,PAYMENT-SIGNATUREandAgent-Authorization- and theprivateKey,signerPrivateKey,signature,seed,mnemonic,secret,apiKey,adminTokenandtokenfields at the top level and one level deep. Pino's wildcards are single-level, so a secret nested two deep is not covered by the logger and must not be handed to it; every call site funnels caught errors throughdescribeError, which extracts only{message, name}. Receipts and events persist no secrets, no raw payment proofs and no mandates. - Input validation on resource inputs, path parameters, body size, content type, payment metadata and configuration.
None of the agent-facing routes, by design. An agent that can pay is a
customer, not an intruder, so POST /api/resources/:id/invoke,
GET /api/resources, GET /health, GET /ready,
GET /.well-known/agent-commerce and the /mcp and A2A mounts are open. Paid
resources are protected by payment, not by authentication.
ACP is the exception among agent routes. When protocols.acp is enabled,
every checkout route under its mount requires
Authorization: Bearer <protocols.acp.auth.token>, compared in constant time;
ACP's own discovery document at /.well-known/acp.json stays public and
contains no configuration at all. Request Signature verification is not
implemented and a Signature header never substitutes for the bearer token, so
ACP must be deployed behind TLS. See docs/security.md.
The operator routes are different. GET /api/receipts, GET /api/events
and GET /api/events/stream expose the merchant's commerce ledger: payer and
payee addresses, amounts, settlement transaction hashes, resource ids and
timings. That is revenue history and customer on-chain identity, not public
data. They require server.adminToken, and if no token is configured they
return 404 rather than serving openly.
The dashboard needs that same token to read those routes, via
VITE_ADMIN_TOKEN. Because Vite inlines every VITE_-prefixed variable into
the JavaScript it serves, that token is not a server-side secret once it
reaches the dashboard: it is readable by anyone who can load the dashboard's
page, not merely by anyone who can reach the gateway. The demo stack accepts
this because the dashboard is loopback-only and ships a non-secret placeholder.
A real deployment must not point a real server.adminToken at this variable.
The dashboard's port is a different trust boundary from the gateway's, and
there is no server-side proxy yet that would keep the token off the client
(post-alpha).
Browser access is governed by server.allowedOrigins, an explicit allowlist
that defaults to empty. Agent traffic is not browser traffic and receives no
CORS headers at all.
A browser EventSource cannot send custom headers, so the dashboard's SSE
connection to /api/events/stream cannot carry the admin token. It receives a
401 when a token is configured, and a 404 when one is not, because the operator
routes are closed by default. There is no posture in which a browser can read
the stream. A header-capable client still can; the dashboard polls
GET /api/events instead, which is its intended path rather than a degraded
mode.
Accepting the token as a ?adminToken= query parameter on that one route would
make the stream work in a browser. It is deliberately not supported. The
cost - credentials leaking through Referer, browser history and intermediary
logs - buys a convenience nothing needs, because the dashboard polls. Do not
add it without a client that genuinely requires it and a reason that outweighs
putting a credential in a URL.
Be clear-eyed about this. Running this gateway does not make your agent, your backend or your business secure.
- It does not secure your merchant backend. Authentication, authorisation, rate limiting and data protection in your API remain entirely yours.
- It does not vet the buyer. Any party able to produce a valid payment gets the resource. There is no KYC, sanctions screening, fraud scoring or dispute mechanism. An AP2 mandate proves a human approved the purchase; it says nothing about who that human is.
- It does not make payments reversible. On-chain settlement is final. There are no refunds, chargebacks or escrow.
- It does not protect against SSRF beyond configuration discipline. The gateway calls the backend URLs an administrator configured, and does not follow redirects. But if you configure an internal URL, the gateway will call it. Agent- or user-controlled backend URLs are forbidden, and there is no allowlist enforcement.
- It does not audit the payment protocol or its SDKs. x402, the MCP SDK and their transitive dependencies are third-party code.
- It does not provide multi-tenancy, RBAC or policy controls.
- It does not defend against a compromised host. SQLite receipts and process memory are as safe as the machine the gateway runs on.
- It does not guarantee delivery after settlement. A backend failure after a successful payment is possible; it is recorded as an event and a payment attempt, and reconciliation is the merchant's responsibility.
- It cannot always tell you whether a payment settled. If the settlement
transaction is broadcast but its receipt cannot be confirmed, through an RPC
timeout or a dropped connection, the outcome is genuinely unknown. The
gateway records the attempt as
settlement-uncertainwith the broadcast transaction hash, and does not deliver the resource. Resolving it is the merchant's job: check the recorded hash withgetTransactionReceipt. The gateway will not report this as a failure, because it does not know that it was one. - It prioritises delivery over bookkeeping. If a resource is delivered but persisting the receipt fails, the delivery still happens and the failure is logged. A missing receipt therefore does not prove a resource was not delivered.
- A reserved payment authorisation is never released. If settlement fails, that authorisation cannot be reused at this gateway even when nothing moved on-chain. This is deliberate, since releasing it would reopen a replay window, but a buyer hit by a transient error must sign a fresh authorisation. An AP2 mandate is handed back in the narrower case where settlement provably moved no money, and kept otherwise.
- A rejected request can still have been charged for. A few request-shape
errors are only detectable when the backend call is assembled, which happens
after settlement. The gateway hoists the checks it can - empty,
.and..path parameters, and input keys colliding with an operator-configured query parameter are all rejected before any payment is taken. But settlement is final and there are no refunds, so if you configure a resource whose inputs can fail late, your buyers can pay for a request that is never delivered. The attempt is recorded assettledwith abackend.failedevent sharing the samerequestId, so reconciliation is possible. - It does not rate limit anything. Free resources are an unauthenticated proxy to your backend at whatever rate a caller chooses. Rate limiting, quotas and abuse controls belong in your API or your edge.