Your agent paid for the data. That doesn't mean the bytes are real.
Autonomous agents are starting to pay for data — per call, in stablecoins, over
x402. That solves billing. It doesn't solve trust. A 200 OK
from a paid endpoint tells you the money moved. It tells you nothing about whether
the bytes you got back are the bytes the publisher actually signed — or whether a
proxy, a cache, or a man-in-the-middle rewrote them on the way to your agent.
If your agent is about to act on that data — release funds, open a position,
file a report, trigger a workflow — "I paid for it" is not the bar. "I can prove
it's intact and I know who stands behind it" is.
This is a 5-minute walkthrough of doing that check before your agent acts, using
an open MIT kit. No token, no signup, no API key.
### See it first — one command, no wallet
npx @foreseal/demo
This runs entirely offline. It walks an agent through a handful of payloads — a
genuine signed one, a byte-tampered one, a forged-key one, one with the receipt
stripped off, one with the domain rewritten — and prints ACT or REFUSE for
each. The point: a verifier that fails closed. Tampered, forged, or
missing-receipt → the agent refuses. No real money is involved; it's the
mechanism, distilled.
### How the receipt works
Every paid response from the PayPerByte gateway
carries an
X-BYTE-Attestation header: an EIP-712
PayloadAttestation.
The recipe is published, machine-readable, at /.well-known/agent.json:
keccak256(responseBody) === payloadHash AND recoverTypedDataAddress(domain,
{PayloadAttestation}, message, signature) === attester
Two legs. The hash leg proves the bytes weren't altered. The signer leg
proves the receipt was
issued by the key you expect — not self-asserted by whoever sent the response. You
check both, locally,
before acting. (One deliberate detail that looks like a bug but isn't: payments
settle in USDC on Base, but the attestation's EIP-712 domain is anchored on
Arbitrum, chainId 421614. Settlement rail and trust anchor are intentionally
separate.)
### Wire it into your agent
npm i @payperbyte/sdk@^0.3.0
Version matters: the full hash and signer check (
verify/
verifyAttestation/
verifyFromGatewayResponse) ships in 0.1.2+ — the earliest0.1.0/0.1.1
only export the
hash-onlyverifyPayload. Current is 0.3.x; pin^0.3.0.
The one-call path, for a response you just fetched from the gateway:
import { verifyFromGatewayResponse, ARBITRUM_SEPOLIA } from '@payperbyte/sdk';
// Pin this once from https://x402.payperbyte.io/.well-known/agent.json →
receipt.attester
const GATEWAY_ATTESTER = '0xB48CCc9e3ab67041e3b5D09700138E45cda6AeA8';
async function getVerifiedData(url: string, res: Response): Promise<Uint8Array> {
const body = new Uint8Array(await res.arrayBuffer());
const header = res.headers.get('X-BYTE-Attestation');
const verdict = await verifyFromGatewayResponse(
body,
header,
ARBITRUM_SEPOLIA, // the attestation domain's chain (421614)
GATEWAY_ATTESTER, // pin it — a self-asserted header can't prove itself
);
if (!verdict.verified) {
// fail-closed: tampered, forged, retired key, or no receipt at all
throw new Error(`refusing to act on ${url}: ${verdict.reason}`);
}
return body; // safe to act on
}
Update 2026-08-20: the gateway attester rotated on 2026-08-19 (planned
rotation following a confirmed key exposure — same wording as the public
disclosure). The constant above is current as of this update. The previous
attester0x77c86a…C472is retired; the gateway now publishes retired keys
machine-readably at/.well-known/agent.json → receipt.retiredAttesters, so treat
anything recovering to a retired address after itsretiredAtas untrusted. If
you archived receipts verified against the old address between 2026-07-03 and the
rotation, treat their provenance as unknown: a receipt signed by the exposed key
is cryptographically indistinguishable from a genuine one. Re-fetch anything you
still rely on. And take the lesson this update embodies: re-pin from the live
manifest rather than trusting a constant in a months-old article — including this
one.
That GATEWAY_ATTESTER argument is not optional ceremony. If you don't pin the
attester, the kit
fails closed on purpose — because the publisher field inside an attestation
header is attacker- controlled, so trusting it would let a forged response
self-certify. Pin the address from the published recipe; then a forged header has
nothing to stand on.
The Verdict you get back is explicit, so your logs tell you why something was
refused:
interface Verdict {
verified: boolean; // hashMatch && signerMatch === true — the one "safe to
act?" bool
hashMatch: boolean; // bytes weren't altered
signerMatch: boolean | null; // null = no attestation present → fail-closed
(never "pass on hash alone")
recovered: string | null; // the address the signature actually recovers to
expired: boolean; // ADVISORY ONLY — staleness is a freshness axis, not a
provenance verdict
reason: string; // human-readable, for post-mortems
}
Note expired does not flip verified to false. A once-minted now+300s
deadline makes every aged feed look "expired"; that's a freshness question for
your own policy, not a tamper verdict. Surface it, decide for yourself.
If you're working from the lower-level pieces (raw bytes + the attested fields, or
an on-chain event
rather than a gateway header), verify(input) takes the explicit struct, and
verifyPayload does the hash-only leg on its own. Same fail-closed contract: it
throws nothing and always returns a Verdict.
### Bonus: verify the counterparty, not just the payload
The same EIP-712 receipt pattern backs a live $0.10 counterparty screen — a
The same EIP-712 receipt pattern backs a live $0.10 counterparty screen — a
signed go/no-go on an address+domain before your agent releases funds. It's a
paid feed; the verdict comes back as a signed ALLOW/WARN/BLOCK you verify the same
way:
curl -i -X POST https://x402.payperbyte.io/feeds/address-reputation \
-H 'content-type: application/json' \
-d '{"domain":"payee-checkout-7x9q.example","address":"0xRecipient","amount":500
0000,"chain":"base"}'
(With no payment header you get the HTTP 402 challenge back — that's the
handshake, not an error. This is a counterparty screen — "is it safe to send
here?" — not a seller-reputation score.)
### Where this is honest about itself
This is early. It's dogfooded end-to-end, settlement volume is still small, and
external adoption is exactly the open question we're publishing this to answer.
What it does prove is narrow and real: authenticity and tamper-evidence, not
correctness — your agent learns "these are exactly the bytes that were signed, by
the key I expected," not "this data is true." It's MIT, USDC-only, no token, no
new contracts to deploy. The verifier is the whole pitch: a small amount of code,
hardened to fail closed, that you run before your agent acts.
- Demo:
npx @foreseal/demo - Kit:
npm i @payperbyte/sdk@^0.3.0· Gate (for publishers):@foreseal/gate - Recipe:
https://x402.payperbyte.io/.well-known/agent.json - MCP server (drive it from Claude/Cursor):
npx -y byte-mcp-server
Disclosure: I build PayPerByte (machine name "BYTE Library").
Top comments (3)
The useful part is moving the trust check into runtime. Agent workflows need to know not only what a feed says, but whether the source, signature, and payment boundary are still valid before the action happens.
Exactly — runtime, right before the action, is the whole point. A 200 OK at request time says nothing about the bytes by the time the agent acts on them.
Your three split cleanly. Source + signature are the two legs the verifier runs locally: keccak256(body) === payloadHash, and recoverTypedDataAddress(…) === the attester you pinned — a self-asserted header can't certify itself, so it fails closed. The "still valid" / payment-boundary piece I deliberately kept out of the pass/fail verdict: the receipt carries a deadline, surfaced as an advisory expired flag, because staleness is a freshness policy you should own — folding "old" into "forged" would make every aged-but-genuine feed look like an attack.
Honest scope: it proves the bytes are intact and came from the key you expected — authenticity, not that the data is true.
Curious what you're wiring the check into?
That scope separation is useful. Integrity and freshness are related, but they are not the same verdict. I like keeping the cryptographic check narrow and deterministic, then letting the application own the freshness policy based on its risk tolerance.