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

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

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

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.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"}'
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; 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)

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?