@foreseal/gate
Drop-in Foreseal Gate. Put it in front of any data endpoint and instantly get:
- USDC pay-per-call over x402 — unpaid request → HTTP 402 challenge; on payment, proxy upstream and return the bytes.
- A verify-before-act receipt — an EIP-712
PayloadAttestation over the exact bytes served, emitted as the X-BYTE-Attestation header. A buyer recomputes the hash and recovers the signer before acting.
- Delivery telemetry — latency / status / bytes / uptime per call.
- Optional discovery — a Bazaar discovery-extension + manifest entry so the wrapped endpoint surfaces in the x402 ecosystem under your name.
It generalizes the production PayPerByte x402 gateway. No new contracts. The receipt it emits is verifiable by the existing PayPerByte verifiers (the MCP server's verify, the SDK's verify, and the on-chain DataStreamLib) — same BYTE Library EIP-712 PayloadAttestation format.
Phase 1 scope. This is the telemetry pipe + a basic uptime/latency score. A calibrated quality SLA is gated on DQI and is not advertised here. There is no escrow, no staking/slashing, no on-chain fee splitter — those are out of scope by design.
Install
npm i @foreseal/gate express
Quick start (Express)
import express from "express";
import { trustMiddleware } from "@foreseal/gate";
const app = express();
app.use(express.json());
app.use(
"/quote",
trustMiddleware({
upstream: "https://my-api.internal/quote",
price: { perCallUsdc: "0.01" },
payTo: "0xYourUSDCAddress",
network: "base-sepolia",
facilitatorUrl: "https://x402.org/facilitator",
}),
);
app.listen(3000);
Set the delivery attester key in the environment:
export X402_MIDDLEWARE_ATTESTATION_KEY=0x<32-byte-hex>
That's it. GET /quote now returns 402 until paid, proxies your upstream on payment, and stamps every paid 200 with X-BYTE-Attestation — copy-paste this and it settles end-to-end on Base Sepolia with no facilitator keys and no mainnet funds.
Going to mainnet
The quickstart above intentionally pins Base Sepolia so it works with zero setup. To take a paid route live on Base mainnet with real USDC:
- Set
network: "base" (or drop network entirely — eip155:8453 / Base mainnet is the library's default).
- Point
facilitatorUrl at a mainnet-capable facilitator — the public https://x402.org/facilitator above only settles Base Sepolia and will fail closed (503) on mainnet. Options: Coinbase's CDP facilitator (facilitatorAuth: "cdp", needs npm i @coinbase/x402 + CDP_API_KEY_* env — see TrustMiddlewareConfig.facilitatorAuth), or your own self-hosted x402 facilitator.
payTo must be an address that can receive real USDC on Base, and the buyer's wallet needs real Base USDC to pay with.
Config (TrustMiddlewareConfig)
interface TrustMiddlewareConfig {
upstream: string;
price: { perCallUsdc: string }
| { perKBUsdc: string; floorUsdc: string };
payTo: Hex;
network?: "base" | string;
facilitatorUrl?: string;
attestation?: "delivery" | "provenance" | "off";
providerSigner?: Signer;
discovery?: { list: boolean; name: string; description: string; category: string };
schema?: object;
}
The two receipt tiers — and why the distinction matters
| delivery-integrity (default) | the middleware's own attestation key (attestationKey / X402_MIDDLEWARE_ATTESTATION_KEY) — set by whoever deploys this middleware | "The holder of this key signed exactly these bytes — tamper-evident, valid until a signer-chosen deadline." The recovered signer is whoever configured the key, not automatically PayPerByte. |
| provenance (opt-in) | your key (providerSigner) | "This provider vouches these bytes are theirs." The recovered signer is you. |
Both tiers are signed by whoever deploys the middleware — by default that's the same operator running the endpoint, so out of the box neither tier is an independent third-party attestation. The distinction is which key you point the middleware at: attestation: 'delivery' (default) signs with the middleware's configured key; attestation: 'provenance' signs with your own production key (providerSigner), so the recovered signer is unambiguously the data's producer. What neither tier proves: that the data is correct, or when the bytes were served — the receipt's only temporal field is deadline (a signer-chosen expiry, default 300s from signing time; see DEFAULT_TTL_S), not a record of when the payload was observed or delivered. Never market delivery-integrity as provenance, as independent third-party attestation, or as a correctness guarantee.
import { privateKeyToAccount } from "viem/accounts";
trustMiddleware({
upstream,
price: { perCallUsdc: "0.05" },
payTo,
attestation: "provenance",
providerSigner: privateKeyToAccount("0x<your-key>"),
});
How a buyer verifies a receipt
The buyer reads the X-BYTE-Attestation header, recomputes keccak256(body), and recovers the EIP-712 signer — then asserts the signer is the attester they trust:
import { verifyReceipt, parseReceiptHeader } from "@foreseal/gate";
const res = await fetch(url, { });
const body = new Uint8Array(await res.arrayBuffer());
const receipt = parseReceiptHeader(res.headers.get("x-byte-attestation") ?? "");
if (!receipt) throw new Error("missing or malformed receipt — do not act");
const verdict = await verifyReceipt(body, receipt, EXPECTED_ATTESTER);
if (!verdict.verified) throw new Error(`do not act: ${verdict.reason}`);
This is the same two-leg check the deployed PayPerByte verifiers perform (keccak256(body) === payloadHash and recoverTypedDataAddress(...) === publisher), against the consensus-critical BYTE Library EIP-712 domain — so any existing PayPerByte verifier checks this receipt identically.
Two different questions: "safe to act now?" vs. "did this key sign this, ever?"
verifyReceipt answers one question by default: is this receipt safe to act on right now? That requires the receipt to be unexpired — five minutes after issuance (the default TTL), the answer becomes verified:false, correctly, because acting on stale data is not what "verified" means for a live call.
Months later, an auditor asking about a receipt already collected as evidence has a different, timeless question: did this key sign these exact bytes? That question doesn't care about the deadline — provenance doesn't expire even though freshness does. 0.2.0 had no way to ask it; folding !expired into verified made every archived receipt permanently unverifiable.
0.3.0 adds an explicit, opt-in archival mode that asks exactly that second question — and nothing else changes:
import { verifyReceipt, verifyProvenance } from "@foreseal/gate";
const live = await verifyReceipt(body, receipt, EXPECTED_ATTESTER);
const archived = await verifyReceipt(body, receipt, EXPECTED_ATTESTER, { mode: "archival" });
Archival mode sets aside only the expiry check. Hash match, signature recovery under the consensus domain, domain-divergence rejection, and the pinned-attester check (attesterMatch === true) are all still fail-closed exactly as in the default mode — a tampered payload, a wrong signer, a forked domain, or an omitted attester still returns verified:false in archival mode too.
There is no way to reach the archival answer by accident: it requires literally passing { mode: "archival" } (any other value, a typo, or an omitted opts falls back to the default), or calling the separately-named verifyProvenance(...). And the result never hides that it took the shortcut — every verdict carries:
mode: "act" | "archival" — which question was asked.
assertion: "safe-to-act-now" | "provenance-only-timeless" | "not-verified" — what verified actually means here. Only "safe-to-act-now" means it's safe to act.
expired: boolean — surfaced independently, so an archival pass on a receipt past its deadline still shows expired: true right next to verified: true. It is never silently dropped or folded away.
reason — on an archival pass, the text says provenance holds and explicitly states it is not a freshness or safety-to-act claim.
What archival mode still cannot prove: when the bytes were observed. The receipt's only temporal field is deadline — a signer-chosen expiry set at signing time — not a signed record of when the payload existed. There is no signed timestamp in this format. If you need to prove a receipt existed before some external date (an existence-in-time claim, not just an authenticity claim), you need an independent anchor outside this package — e.g. publishing the receipt hash to a public timestamping service or an append-only log — this package only ever answers "who signed these bytes," never "and at what point in time did they do it."
Fail-closed by design
- Facilitator unreachable → 503. Paid routes fail closed and are never served free while the x402 facilitator is down. The middleware retries the facilitator handshake in the background; until it's ready, every paid request gets a 503.
- Upstream error or schema violation → 502, no receipt. A paid buyer gets an honest error, never a signed payload that violates the advertised
schema.
Telemetry & score
const handler = trustMiddleware({ });
app.use("/quote", handler);
const score = handler.getScore();
Point telemetrySink at your own store (Postgres, the PPB indexer) to feed PQS/DQI. The score's note always states it is uncalibrated delivery telemetry, not a quality SLA.
Discovery
const handler = trustMiddleware({
upstream, price: { perCallUsdc: "0.01" }, payTo,
discovery: { list: true, name: "Acme Quotes", description: "Real-time quotes", category: "financial" },
});
app.get("/.well-known/x402.json", (_req, res) => {
res.json({ x402Version: 1, resources: [handler.manifestEntry("https://acme.example/quote")] });
});
Changelog
0.3.0
- New, non-breaking: opt-in
archival mode on verifyReceipt and verifyEmbeddedAttestation ({ mode: "archival" }), plus a separately-named verifyProvenance(...) convenience export. Answers the timeless provenance question ("did this key sign these exact bytes, ever?") instead of the default freshness-gated question ("is this safe to act on now?") — see Two different questions above. Default behavior (no opts, or any mode other than the literal "archival") is byte-identical to 0.2.0 — every 0.2.0 test still passes unchanged. ReceiptVerdict gained mode and assertion fields so a verdict is self-describing and can't be misread later as the other question's answer.
0.2.0
- Breaking:
verifyReceipt is fail-closed by default. Omitting expectedAttester now returns verified:false (previously it verified with a softened reason). Pin the attester you trust as the third argument to get verified:true.
- New:
verifyEmbeddedAttestation(receivedBody, attestationHeader, expectedAttester?) — parses the X-BYTE-Attestation header and verifies it in one call; fails closed (never throws) on a missing/unparseable header.
- Quickstart fix: the README/example now settle on Base Sepolia against the public discoverable facilitator out of the box (no 503, no keys). See "Going to mainnet" for the real-USDC swap.
Framework-agnostic core
Non-Express hosts can use the core directly: instantiate TrustEngine, gate it behind any x402 payment check, and call engine.fulfill() once payment is verified.
import { TrustEngine } from "@foreseal/gate/core";
Want the pre-wired, deploy-ready kit? Server + example upstream + offline verify demo + deploy guide — the integration afternoon, done. The packages here stay free MIT; the kit is the assembly + walkthrough. $39 → https://payperbyte.gumroad.com/l/nszyv
License
MIT © BYTEDev Inc.