DEV Community

minia2a
minia2a

Posted on Originally published at minia2a.uk

Your Agent's Trial Call Needs a Signature — and the Service ID Isn't What You Think

An agent registers a self-custody wallet, appends ?wallet=0x… to an endpoint the way the quickstart shows — and gets HTTP 402. Nothing is down. The call simply isn't signed, and an unsigned call never reaches the wallet's own trial bucket.

Everything below was measured against a live x402 gateway on August 19, 2026. The pass/fail marks are real responses, not documentation.

Two buckets, not one

  • Anonymous — 15 calls keyed to your egress IP, shared across the whole catalog.
  • Registered wallet — a further 15 calls keyed to the wallet itself, reachable only by signing each call.

They're genuinely independent. My measurements came from a host whose anonymous bucket was already at 0/15: unsigned calls returned 402, and signed calls from the same host in the same second returned 200. If your agents run behind shared cloud egress — Lambda, CI runners, a Workers deploy where thousands of tenants share an IP — that's the difference between "free tier exhausted before I started" and "my wallet carries its own allowance wherever it runs."

The message

minia2a trial:<wallet>:<serviceId>:<unixSeconds>
Enter fullscreen mode Exit fullscreen mode

Sign with EIP-191 (personal_sign), send X-Wallet-Signature + X-Trial-Timestamp, put the wallet in the query string. Timestamp is unix seconds, ±5 minutes.

The detail that costs an afternoon

You're standing at the URL https://minia2a.uk/x402/time. The obvious reading of serviceId is time. It isn't — it's the catalog id, x402-time.

Four candidates, same wallet, same endpoint, same minute:

Signed as Result
x402-time 200x-trial-mode: wallet, x-trial-remaining: 14
time 402
/x402/time 402
https://minia2a.uk/x402/time 402

Confirmed on a second endpoint: gas → 402, x402-gas → 200.

Here's why this is worse than an ordinary gotcha. A wrong service ID and a forged signature produce byte-identical responses — both a 402 with trial.mode:"none". An agent debugging this has no signal telling it whether its crypto is wrong, its clock is wrong, its registration failed, or it merely used the wrong noun. So it goes and rewrites the signing code, which was fine all along.

The general lesson, for anyone designing a payment challenge

A 402 body that names a variable — "sign wallet:service:ts" — is not machine-readable. The client has to guess what service expands to, and every wrong guess looks exactly like an auth failure. Emit the literal string, or name the field it comes from.

The cost of that ambiguity lands entirely on the caller, which in the agent economy means it lands on software that cannot file a bug report. Humans work around ambiguous errors by asking someone. Agents retry, exhaust a budget, and give up — and you never hear about it.

Python that works

import time, requests
from eth_account import Account
from eth_account.messages import encode_defunct

acct, sid = Account.from_key(PRIVATE_KEY), "x402-time"
ts = str(int(time.time()))
sig = acct.sign_message(
    encode_defunct(text=f"minia2a trial:{acct.address}:{sid}:{ts}")
).signature.hex()

r = requests.get(f"https://minia2a.uk/x402/time?wallet={acct.address}",
                 headers={"X-Wallet-Signature": sig, "X-Trial-Timestamp": ts})
print(r.status_code, r.headers.get("x-trial-mode"), r.headers.get("x-trial-remaining"))
Enter fullscreen mode Exit fullscreen mode

Don't infer which bucket paid — read the header

  • x-trial-mode: wallet — signature accepted.
  • x-trial-mode: ip — you're on the anonymous bucket; something in the signed message was wrong and the call quietly fell back.

That ip case is the quiet one: a 200 doesn't prove your signing works, it may just prove your IP still had calls left. Test from an exhausted IP, or watch the header. Those are the only two honest checks.


Full write-up with curl/CLI/MCP recipes: minia2a.uk/blog/x402-signed-trial-calls-august-2026

Top comments (0)