Shipping Receipt Binding — Why Method, Path, and Expiry Matter

August 9, 2026 · Engineering · 4 min read

A developer named Swapnoneel Saha left a comment on our x402 tutorial with three words that changed how we think about receipts: "bind to method, path, and expiry." He was right. Here's what we shipped.

The Gap We Missed

Our receipt system already had HMAC-SHA256 signing over canonical receipt fields. Every receipt carried a cryptographic signature that could be verified independently. Replay protection existed at the database level — duplicate receipt IDs were rejected.

But there was a subtle gap: a receipt generated for GET /x402/token-security?address=0xABC would pass verification if replayed against GET /x402/gas. The HMAC matched because the signing string didn't include which endpoint the receipt was for.

In other words: we were signing what happened (amount, timestamp) but not where it happened (method, path).

What We Added

Three new fields in the canonical signing string:

FieldPurposeExample
methodHTTP verb — prevents cross-method replayGET, POST
pathRequest path — binds receipt to specific endpoint/x402/x402-gas
expiresShort TTL — rejects stale receipts2026-08-09T12:41:57Z (5 min)

The updated canonical function now looks like this:

func canonical(r *store.Receipt) string {
    return fmt.Sprintf("id:%s\ntype:%s\nservice_id:%s\nmethod:%s\npath:%s\nagent:%s\namount_cents:%.0f\ncurrency:%s\ntx_hash:%s\ntimestamp:%s\nexpires:%s",
        r.ID, r.Type, r.ServiceID, r.Method, r.Path, r.Agent,
        r.AmountCents, r.Currency, r.TxHash, r.Timestamp, r.Expires)
}

And VerifyReceipt() now enforces expiry:

func VerifyReceipt(r *store.Receipt) bool {
    if r.Hmac == "" { return false }
    // Reject stale receipts
    if r.Expires != "" {
        exp, err := time.Parse(time.RFC3339, r.Expires)
        if err == nil && time.Now().UTC().After(exp) {
            return false
        }
    }
    expected := signReceipt(r)
    return hmac.Equal([]byte(expected), []byte(r.Hmac))
}

Live Receipt Example

Here's a real receipt from a trial call to /x402/time, showing the new fields in production:

idrcpt_mslsemr0_c3eaa698
typetrial
service_idx402-time
method newGET
path new/x402/proxy/x402-time
expires new2026-08-09T12:41:57.132Z
hmacce42e4eb3c853fae... (verified ✓)

The Failure Table That Comes Next

Method/path/expiry binding is defense-in-depth against receipt replay. But Swapnoneel's comment pointed to something bigger: a failure table that models every state an agent payment can be in, and what the system does about it.

StateTriggerSystem Action
AWAITING_SETTLEMENTPayment submitted, tx pendingPoll facilitator, timeout → refund
SETTLED_NOT_DELIVEREDPaid but response never returnedRetry with same receipt → idempotent replay
DUPLICATE_RETRYAgent retries with same proofReturn cached response, don't charge again
RECEIPT_STALEReceipt timestamp > TTLReject, force fresh payment

The principle: an agent should never need to reason about "did my payment go through?" The server guarantees exactly-once delivery semantics, and the receipt is the cryptographic proof. This is the difference between "agents can pay" and "agents can pay safely."

Why This Matters Beyond Security

Every x402 discussion starts and ends with "the 402 response has an invoice." But the invoice is only step 1. Steps 2 through 10 — settlement, verification, receipt binding, expiry, failure recovery, dispute resolution — are where production readiness actually lives.

The Cloudflare x402 Foundation, the Coinbase Agent Payment Stack, the OSL AgentPay — all of these are building the payment rails. The receipt and accountability layer is the track that keeps trains from colliding on those rails.

If you're building on x402, here's the minimum you should add to your receipt system today:

  1. Bind to method + path — prevents cross-endpoint replay
  2. Short expiry (5 min) — rejects stale receipts at verification time
  3. DB-level uniqueness — catches duplicate receipt IDs even if HMAC matches

These three checks together mean an attacker needs to: forge the HMAC and match the method and match the path and use a fresh receipt — all within 5 minutes. That's not impossible, but it raises the bar from "trivially replayable" to "cryptographically infeasible."

Thanks to Swapnoneel Saha for the detailed technical feedback that prompted these improvements. The best security insights come from developers who read your code with fresh eyes.

minia2a is an open marketplace where AI agents discover, call, and pay for APIs in USDC. 326 services, 15 free trials per endpoint. minia2a.uk