Hardening an x402 API: What October Taught Us

Most of the traffic that touched our payment path in October was not buying. It was probing: forged signatures, recycled transfers, a paid scanner pointed back at our own network. Here is what we saw, what we changed, and the six checks we think every x402 seller should run.

November 3, 2026 7 min read By Petteri Leinonen, Founder, AsterPay

When you put a price on an HTTP endpoint, you publish a small, precise challenge to the whole internet: here is exactly what I will accept as payment. Honest agents read that challenge and pay it. Everyone else reads it as a specification to test against.

We have written before that most x402 volume is not independent commerce and that trust, not settlement, is the scarce layer. October made that concrete on our own rail. This post is the engineering side of the same argument. None of it is exotic. All of it is the kind of thing that is easy to skip when the happy path works.

1. Forged authorizations: verify the signature yourself

On Base, an x402 payment is usually an EIP-3009 transferWithAuthorization: the buyer signs an EIP-712 message saying "move this much USDC from me to you, valid in this window, with this nonce", and the seller or facilitator submits it on-chain.

At the end of October one client spent an afternoon working through variations: a zero transaction hash, a real transaction belonging to someone else, an authorization for less than the price, and authorizations with signatures that did not belong to the stated payer. Every one was rejected. But the forged signatures taught us something.

Our verifier used to rely on the USDC contract to reject a bad signature at settlement time. That is safe in the sense that the contract will never move funds on a forgery, but it means a forgery travels further into your pipeline than it should, and every attempt costs you a gas estimate. We now recover the signer from the EIP-712 typed data during verification and refuse the request before anything touches the chain.

A detail that will bite you: the EIP-712 domain name for USDC differs between deployments. Mainnets use "USD Coin", while Base Sepolia uses "USDC". If you hardcode one, valid testnet payments fail verification and you will be tempted to loosen the check. Read the name from the payment requirements' extra field and fall back to the known values.

2. Recycled transfers: bind every proof to its challenge

Some payment flows do not use a signed authorization. The buyer simply sends USDC and presents the transaction hash as proof. That is convenient, and it has an obvious weakness: every transfer ever made to your receiving address is public on a block explorer.

In mid-October someone did exactly that, submitting old transfers to our address as proof for new requests. On one of our payment paths the check was "did a USDC transfer to us with this hash happen, for at least this amount", and that was not enough. We closed it the same day. A transaction hash now only counts if it was mined after the 402 challenge it answers was issued, pays at least the route price in USDC to our receiving address, and has not been used before.

A payment proof that is not tied to a specific challenge is not a payment proof. It is a receipt someone found.

3. Invoices that do not match the quote

The same review covered the flows where we pay out over Lightning. The risk there runs the other way: a quote locked at one amount, and an invoice, or an invoice returned by the payee's Lightning-address server, that encodes a larger one. Paying the invoice as presented would drain the treasury while the database records the smaller figure. We now decode every invoice and refuse it unless the encoded amount equals the locked quote exactly. Zero-amount invoices, which let the payer choose the amount, are refused outright.

4. A paid scanner pointed inward

We sell an agent-readiness check: give it a URL and it fetches the site to see how well agents can discover and pay it. Any endpoint that fetches a URL on a caller's behalf is, if you are careless, a proxy into your own infrastructure. In October someone tried to use ours that way.

The fix is a guard applied on every hop, not just the first URL. Redirects are where this usually goes wrong: a public URL that answers with a redirect to an internal address. Our fetcher now requires HTTPS, resolves DNS itself, refuses private, loopback and link-local ranges, and re-checks all of that for each redirect it follows.

5. Going around the CDN

This one we found in our own review rather than in an attack. Our API sits behind a CDN that provides the WAF, IP blocks and the real client address that rate limits and spending limits are keyed on. But the origin server also answered requests sent to it directly, and it trusted forwarded-for headers. Anyone who found the origin could have sent a made-up client address and walked around every limit.

The origin now refuses traffic that did not come through the CDN, apart from a health check, and the client address is taken only from headers the hosting platform rewrites itself, never from what the caller claims. If your limits are keyed on a header, ask who is allowed to set it.

6. Do not charge for failures

The last lesson cost buyers money rather than us. In x402 the payment settles before your handler runs. When the upstream model provider retired the model behind several of our AI endpoints, those endpoints started returning errors after the payment had already gone through.

Every paid route that depends on an upstream service now runs a preflight check before the paywall settles anything. If the dependency is down, the caller gets a 503 that says, in so many words, "You were not charged. Retry after 120 seconds." Data endpoints serve a recent cached copy when their source does not answer, and a similar no-charge reply when there is no copy. A buyer that has been charged for an error will not come back, and an agent buyer is a script that will route around you without telling you why.

A payment alert is not a customer

One more observation, because it changes how you read your own dashboard. Several of the payments we received in October were real, on-chain and correctly signed, and still not customers. They came from monitors, directory indexers and readiness checkers that pay a few cents to confirm an endpoint works end to end. That is useful, and it is also not demand. We now count external buyers separately from probes and from our own test traffic, and that is the only number we use to make pricing decisions.

The six checks, in one place
  1. Recover and check the EIP-712 signer during verification. Do not wait for the token contract to reject a forgery.
  2. Tie every transaction-hash proof to its challenge: mined after the challenge was issued, covering the price, to your address, in the right asset, never reused.
  3. When you pay out against an invoice or a quote, make sure the amounts match exactly, and refuse open-amount invoices.
  4. Treat any paid endpoint that fetches URLs as a potential proxy: HTTPS only, resolve DNS yourself, block private ranges, re-check on every redirect.
  5. Make the origin unreachable except through your CDN, and key limits only on headers your platform sets.
  6. Check upstream dependencies before you settle, and tell the caller plainly when they were not charged.

None of this makes an API popular. It makes it safe to be paid by software you have never met, which is the precondition for everything else. The agent economy will be decided less by who settles fastest than by who can be trusted to take a payment correctly, and to refuse one correctly.

See the 402 for yourself

No sign-up, no API key. Hit a paid endpoint, read the challenge, retry with a signed payment. The first call is on us.

Try it API docs