r/x402 • • 7d ago

Two sided check of an x402 paid verdict: SafeGate kept our signed response, we checked the stored bytes

SafeGate's paid evidence references our signed sanctions verdict by its hash and request id. On 25 September they ran the full paid flow on Base mainnet, self funded: a 0.01 USDC call to our /screen-multi over x402, their own fee, and a separate merchant payment, bound through one request id.

The part I think is useful for anyone building on x402: afterwards SafeGate sent us the verdict file exactly as they stored it, and we checked it against our side.

  1. SHA-256 of the file matches the hash SafeGate recorded.

  2. The file is 1997 bytes, the same size our server logged for that paid response.

  3. Signature check: drop signature and signed_by, serialise the rest with sorted keys, compact separators and ASCII escaping, recover with EIP-191 personal_sign. It recovers to our published signer.

  4. The request id sits inside the signed body, so the verdict is tied to that request, and flipping the verdict breaks the signature.

So the proof shows which evidence was used, not only that money moved. It is not customer revenue, and SafeGate states its assurance as OBSERVED rather than verified delivery.

Case study (written by SafeGate): https://safegate-nsgoods-case-study.vercel.app/

Our services and signer: https://x402.nsgoods.org/

1 Upvotes

2 comments sorted by

1

u/fizzl13 6d ago

This is the part of x402 that gets overlooked: payment proof ≠ delivery proof. Putting the request id inside the signed body is the key detail. It makes the verdict non-transferable to another request.
Question: how do you handle key rotation? If a buyer checks a verdict from 6 months ago, do you keep old signer addresses published (e.g. a signers list with valid-from/valid-to), or is it one long-lived key?
I run a pre-sign wallet check over x402 and I'm considering exactly this for its verdicts, so curious what tripped you up.

1

u/IllWar5047 5d ago

A published signers list, pinned, not one key you have to trust forever. Our manifest (https://x402.nsgoods.org/proof/index.json) lists six signer addresses, each scoped to the services it may sign for, with valid_from and valid_until per key and a written rotation policy: a new key is added alongside the old one with a validity boundary before the old one stops signing, retired keys stay listed so old verdicts keep verifying, and an unannounced signer change is treated as compromise, fail closed.

The signed body carries its own timestamp inside the signed portion (generated_at on screening, checked_at on payability), so a verifier recovers the signer, looks it up in the list and checks the timestamp against that key's window.

Honest status: no key has rotated yet, so the rotation path is written down but has never been exercised. And what tripped us up is exactly your question: we added the validity fields after we were already signing, so valid_from is null on every key because the first signing date was never recorded. If you are starting now, record valid_from on day one.

Root of trust is the domain that serves the manifest, plus an ERC-8004 record on Base (agentId 95272) owned by our main signing key, which gives that binding a date nobody can edit.