DEV Community

Cover image for Verify before your agent acts: a trust check for x402 data feeds (in one npm install)
Mark Zhurbin
Mark Zhurbin

Posted on Edited on

Verify before your agent acts: a trust check for x402 data feeds (in one npm install)

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Version matters: the full hash and signer check (verify /
verifyAttestation /
verifyFromGatewayResponse) ships in 0.1.2+ — the earliest 0.1.0/0.1.1
only export the
hash-only verifyPayload. 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
  }
Enter fullscreen mode Exit fullscreen mode

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
attester 0x77c86a…C472 is 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 its retiredAt as 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
  }
Enter fullscreen mode Exit fullscreen mode

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"}'
Enter fullscreen mode Exit fullscreen mode

(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)

Collapse
 
alexshev profile image
Alex Shev

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.

Collapse
 
0rkz profile image
Mark Zhurbin

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?

Collapse
 
alexshev profile image
Alex Shev

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.