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.1.2
Version matters: the full hash and signer check (
verify/verifyAttestation/
verifyFromGatewayResponse) ships in 0.1.2+. Earlier0.1.0/0.1.1only export the hash-only
verifyPayload. Pin^0.1.2.
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 = '0x77c86a5367d941091a31BC97104609F2Db33C472';
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, expired-key, or no receipt at all
throw new Error(`refusing to act on ${url}: ${verdict.reason}`);
}
return body; // safe to act on
}
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.05 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":5000000,"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; the only mainnet settlements so far are our own self-tests, 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.1.2· 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 (2)
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?