npm.io
0.12.5 • Published 3d agoCLI

twzrd-x402-gate

Licence
MIT
Version
0.12.5
Deps
0
Size
1.4 MB
Vulns
0
Weekly
0

twzrd-x402-gate

Wrap your agent's paying fetch. Refuse before its wallet signs when a quote is over your cap, its recipient is not allowed, or the free preflight blocks or cannot answer. Keep the reason in a local JSONL file.

Which entry point?

You are… Use
Starting a new integration createBuyerFetch (since 0.12.3; optional expectedAmount since 0.12.4)
Already using an official x402Client installTwzrdAutoGate(client)
Using PayKit (createPayKitClient) installTwzrdAutoGate("pay-kit")
Using PayAI createX402Client({ beforePayment }) installTwzrdAutoGate("x402-solana")
Using MPP (Mppx.create({ onChallenge })) installTwzrdAutoGate("mpp")
Using the @x402/mcp client twzrdOnPaymentRequested
Keeping an older integration createGuardedX402Fetch, withTwzrdGuard, wrapFetchWithTwzrdGate, or twzrd.safeFetch

installTwzrdAutoGate is one function with those adapters. twzrd.safeFetch is spendControlSafeFetch. The other export named safeFetch is the advisory CLI pipeline in src/safe-fetch.ts, not this row.

createBuyerFetch is the front door for a new buyer. createGuardedX402Fetch is legacy and stays for callers that already use it. They are not the same wrapper:

  • The hourly ledger in createBuyerFetch is a JSONL file (spendLedgerFile, default .twzrd/spend.jsonl) shared by every fetch that points at that path, including after a restart. createGuardedX402Fetch keeps its rolling hour in memory for the life of that returned fetch and drops it when the process ends.
  • createBuyerFetch requires maxPricePerCall and allowedRecipients, and it sets unsupportedNetworkMode: "strict". A network the scorer marks unscored is refused. That includes a name containing devnet, testnet, or sepolia, and Base Sepolia. A Solana devnet named only by its CAIP-2 genesis id is still treated as scored (network.ts) and is not refused by this rule.
  • createGuardedX402Fetch leaves caps and the allowlist optional. Its preflight uses the shared default unsupportedNetworkMode: "observe", so those same unscored networks are allowed through without a reputation score. If you did set a spend cap, a non-USDC asset is still refused locally.

Defaults by entry point

Read from the constructors, not from the prose below. "Wash flagged" is refuseWashFlagged. refuseWashUnknown is off on every row in 0.12.x unless that call opts in. TWZRD_AUTO_GATE=0 and TWZRD_GATE_ENABLED=0 are a different switch: only installTwzrdAutoGate reads them, and createBuyerFetch does not.

Entry point Preflight outage TWZRD_FAIL_OPEN Wash flagged Unscored network
createBuyerFetch fail-closed (failOpen: false is passed in) no on, and TWZRD_REFUSE_WASH_FLAGGED cannot turn it off strict: refuse
installTwzrdAutoGate(client), "pay-kit", "x402-solana" fail-closed yes on, unless TWZRD_REFUSE_WASH_FLAGGED is 0 or false observe: allow, then the wash check still runs
installTwzrdAutoGate("mpp") not this preflight. A thrown evaluation refuses and does not return a credential not read not this flag. No wash_flagged check in mpp-hook.ts not observe/strict. An unknown cluster is refused; devnet is a known cluster
twzrdOnPaymentRequested fail-closed when called with no config yes, inside the approval (resolveConfig()) on, unless that env opt-out, when no config is passed observe
createGuardedX402Fetch fail-closed. After the local cap check it calls createTwzrdPayKitBeforePaymentHook, which is evaluateBeforePaymentCreation. That is the same evaluator installTwzrdAutoGate("pay-kit") returns. The client you pass does not have to be PayKit yes on, unless that env opt-out observe. A set spend cap still refuses a non-USDC asset
withTwzrdGuard fail-closed yes on, unless that env opt-out observe
wrapFetchWithTwzrdGate() fail-closed when you pass no config, or pass resolveConfig() yes, unless the config you pass already set failOpen same as the config you pass; resolveConfig() is on unless that env opt-out observe, unless the config you pass says strict
twzrd.safeFetch none, unless you pass preflight. A throw from that callback propagates not read not applied no observe/strict mode. Optional allowNetworks can refuse network_not_allowed

Start here: a blocked payment, no wallet

createBuyerFetch and twzrd-buyer-demo ship from 0.12.3, and expectedAmount from 0.12.4. In an ESM project:

npm install twzrd-x402-gate@0.12.5 @x402/core @x402/fetch
npx twzrd-buyer-demo

Or from a clone of this repository:

npm ci
npm run build
npm run buyer-demo --workspace=twzrd-x402-gate

The demo runs a real @x402/fetch client against a synthetic 402, refuses the over-cap quote, and writes .twzrd/demo-decisions.jsonl under the directory it runs in. No keys, network access, or funds are used. Expected fields:

{"mode":"offline_fixture","decision":"block","reason":"price_cap_exceeded","signer_invocations":0,"paid_requests":0,"usdc_spent":0}

To test an unpublished build outside this workspace, pack from twzrd-x402-gate/ with npm pack --ignore-scripts and install the tarball in place of the registry version.

Put it on your paying call

Pass a dedicated official x402Client with your scheme and signer registered:

import { createBuyerFetch } from "twzrd-x402-gate";

const guardedFetch = createBuyerFetch({
  client,                         // your configured x402Client
  maxPricePerCall: "0.01",         // exact USDC decimal; required
  allowedRecipients: [sellerPayTo],// explicit payTo allowlist; required
  hourlyBudgetCap: "1.00",         // optional rolling cumulative cap
  decisionFile: ".twzrd/decisions.jsonl",
  spendLedgerFile: ".twzrd/spend.jsonl",
});

try {
  const response = await guardedFetch(resourceUrl); // ordinary fetch signature
} catch (error) {
  console.error(error.reason ?? error.message);
}

Caps and recipients are checked against the selected requirement at onBeforePaymentCreation. Supported payment assets are USDC on scored Solana mainnet and Base mainnet; unscored networks refuse. EVM recipient comparison ignores case; Solana comparison preserves case. An empty allowlist refuses all.

Bind the quote: expectedAmount (0.12.4)

maxPricePerCall is a ceiling: with a $1,000 cap, a $1,000 challenge passes it. To refuse any amount other than the one you were quoted, also pass expectedAmount:

const guardedFetch = createBuyerFetch({
  client, maxPricePerCall: "1.00", allowedRecipients: [sellerPayTo],
  expectedAmount: "0.01",          // exact USDC decimal the seller quoted
});

The atomic amount the signer would authorize, on the offer the client selected, must equal it exactly. A lower or higher amount refuses with expected_amount_mismatch before preflight or signing. Only the selected offer is checked, so a 402 that lists several offers can refuse even when another offer matches. The cap still applies and keeps price_cap_exceeded. A fixed expectedAmount above maxPricePerCall throws at install; values take up to 6 decimal places.

One wrapper can serve several resources with a function that returns the quote per payment: expectedAmount: ({ resource, payTo, network, signal }) => quotes.get(resource). It never sees the challenged amount. resource is the normalized request URL (new Request(url).url, with its query), and payTo is spelled as the challenge spells it, so key lookups the same way. When it returns no decimal string or throws, the call refuses with expected_amount_unavailable and the error keeps the cause. An abort while it is pending refuses with request_aborted.

Added in 0.12.4. 0.12.3 does not have this option: TypeScript rejects it there as an unknown property, and plain JavaScript ignores it without an error, so pin 0.12.4 or later before relying on it.

The free preflight and merchant-card checks run before signing. This entry point has no fail-open or disable switch: legacy TWZRD_FAIL_OPEN, TWZRD_AUTO_GATE, and TWZRD_GATE_ENABLED cannot bypass it. A warn can proceed within local caps and the preflight's policy. A check that cannot answer refuses.

Decisions are flushed locally before signing; no decision-upload request is made. Rows retain recipient, network, atomic amount and resource origin, not URL paths/queries, credentials, signatures or response bodies. The signer_invocations: 0 field describes the decision's pre-sign point, including an allow row; it does not claim an allowed call stayed unsigned or settled. Free preflight still sends the requested seller, resource URL and price to intel.

The spend ledger counts authorized signing attempts, including signer errors and uncertain payment outcomes. It does not claim confirmed settlement or refunds. A supplied hourly budget survives process restarts. Pending calls share reservations within one process. Use separate files per process or serialize externally; these ledgers are not a multi-process budget service. Local write failures refuse signing. HTTP redirects and pre-signed input headers are refused. Configure the client before wrapping it, use the returned fetch for payments, and do not add bypass/recovery hooks or mutate its signer path afterward. Core's own spend controls can impose additional restrictions.

No paid score or receipt is bought by this wrapper. /quick (0.001 USDC) is an optional unsigned Solana seller score; /trust (0.05 USDC) is an optional signed V7 receipt of that score. Neither proves a GPU or model completed work.

The offline demo proves a local refusal, not outside adoption. First outside use requires evidence from another agent using the installed wrapper on its own payment path.

Other integration APIs

The existing APIs below retain their own defaults and optional switches.

Commerce loop (directory → preflight → AutoGate → verify → evidence bundle): repo docs/COMMERCE-KIT.md. exportEvidenceBundle / npx twzrd-evidence-bundle writes twzrd.evidence_bundle.v1. This package is not Catena's Agent Commerce Kit.

Lower-level call (named export twzrd)
import { twzrd } from "twzrd-x402-gate";

const result = await twzrd.safeFetch(url, {
  maxSpend: "0.10",              // per-call cap AND cumulative budget
  allowNetworks: ["solana", "base"],
  requireOfferBinding: true,     // compose → verify hard bind → only then pay()
  composeBoundTransaction,       // build unsigned bound bytes (no keys)
  pay,                           // sign/submit only after bind-v1 hard verify
});
// result.verdict: "allow" | "warn" | "block" — blocks have signerInvocations === 0
// result.receipt: { strength: "hard"|"soft"|"refuse", leaf_hash, fact_type: "resource_bound" }

requireOfferBinding is fail-closed: missing composeBoundTransaction returns bind_required_no_compose with signerInvocations === 0 (pay is never called). The gate verifies the composed bytes, then calls pay() with that transactionBase64. The old post-pay check remains as verifyOfferBindingAfterPay only. This SDK never holds keys.

Full walkthrough: QUICKSTART.md · verify receipts yourself: REVIEW.md

Core product (buyer gate): after the client selects the exact payment requirement and before payment payload creation / wallet signing — free preflight + merchant_card wash refuse. Protects the payer from a risky merchant (payTo). Chain-neutral envelope; Solana mainnet and Base mainnet (eip155:8453) are scored. Other EVM networks do not run the scored preflight.

Default-on AutoGate (5 lines)
npm install twzrd-x402-gate@0.12.5 @x402/core @x402/fetch @x402/svm
import { x402Client } from "@x402/core/client";
import { installTwzrdAutoGate } from "twzrd-x402-gate";

const client = new x402Client();
// refuseWashFlagged defaults true; gateOnCanSpend stays false unless you opt in
installTwzrdAutoGate(client, { refuseWashFlagged: true });
// then register schemes + wrapFetchWithPayment as usual

Historical intercept proof (2026-07; 0 USDC, wash seller never reaches signer):

cd twzrd-x402-gate && npm run autogate-block-proof   # needs network: live intel preflight
# writes block-proof-<run_id>.json  (schema twzrd.autogate_block_proof.v1)
# public reason: TWZRD_TRUST_GATE_BLOCK: wash_flagged

gateOnCanSpend remains opt-in (false by default; set true or TWZRD_GATE_ON_CAN_SPEND=1 only when you want hard cap enforcement).

Optional (0.8.1): a resource-server settle hook so merchants can apply customer policy before they settle and serve (abuse, sanctions, bots, “don’t serve this payer”). Not an equal mirror of the buyer problem — settled USDC is final; wash resistance is mainly TWZRD scoring.

Seller settle guard (onBeforeSettle) — optional 0.8.1

Resource servers can screen the payer before they settle an inbound payment and serve the resource. Use for merchant policy (abuse / sanctions / bots / customer selection). TWZRD is not in the settlement path: advisory + fail-open by default. Attaches to official x402ResourceServer.onBeforeSettle (inherited by @x402/express|hono|next|fastify and Python x402).

Do not treat this as “protect merchant reputation by rejecting USDC” — anyone can still transfer on-chain. Wash/sybil edges are primarily discounted in TWZRD scoring, not forced revenue refusal.

npm install twzrd-x402-gate@0.12.5
import { x402ResourceServer } from "@x402/core/server";
import { createTwzrdSettleGuard, twzrdPayerScreen } from "twzrd-x402-gate";

const server = new x402ResourceServer(facilitator);
server.onBeforeSettle(
  createTwzrdSettleGuard({ screen: twzrdPayerScreen() }),
);

Defaults (fail-open / advisory):

Behavior Default
Abort settlement decision=block or wash_flagged=true
warn allowed (continues) unless you set abortOn.warn
Default screen free GET /v1/intel/merchant_card/{payer} via twzrdPayerScreen()
Screen/extract timeout timeoutMs: 3000 — on timeout, continue (fail-open)
Unresolved payer / null screen / screen error continue unless failOpen: false
exact-SVM payload, no @x402/svm peer installed reports twzrd_svm_peer_missing (warns once) and continues — without the peer, SVM payers are never screened; install it or set failOpen: false
Paid /v1/intel/trust not default — inject a custom screen if you want it

Payer identity prefers signed/encoded scheme fields (EIP-3009 authorization.from, Permit2 permit2Authorization.from, exact-SVM payload.transaction via optional peer @x402/svm) over client-supplied loose aliases — so a spoofed payload.payer cannot bypass screening.

Offline demo: npx tsx examples/seller-settle-guard.ts
Fixture-backed SVM extract tests live in test/seller-hook.test.ts + test/fixtures/exact-svm-transfer-checked.ts.

PayAI agentic-payments (the active PayAI SDK, not the dormant x402-solana): npx tsx examples/payai-agentic-onPaymentVerified.ts — wire onPaymentVerified → toPayaiVerifyResult to screen payers before serving. Fail-open by default.

Historical: demonstrable refuse mechanism (2026-07-16; free, reproducible)

Status (2026-07-16): mechanism proof — not adoption or demand.

Install the published gate and run against wash fixtures:

npm install twzrd-x402-gate@0.12.5
# from package root after install, or from a checkout:
npm run wash-dogfood

Verified live (2026-07-16):

Fixture preflight_id decision note
7G73PL… wash dogfood 378468 block wash_flagged=true
HuSiSpc… 378469 block wash_flagged=true, fleet≈98%
BJGds… alt wash 378470 warn wash_flagged=true (nuance — not all wash is hard-block)
4LkEF… clean control 378471 warn not wash

Gate dogfood: approved=false reason=twzrd_decision_block, USDC spent = 0, tx broadcast = none, ALL PASS.

Public transcript: https://gist.github.com/twzrd-sol/2882bddee912f89e99061f3bc1da8227

Accurate paste line:

Preflight returned decision=block on wash seller 7G73PL… / HuSiSpc… (preflight_id 378468 / 378469, wash_flagged=true). Gate approved=false reason=twzrd_decision_block. No USDC spent. No tx broadcast. Repro: npm i twzrd-x402-gate@0.12.5 && npm run wash-dogfood or gist above.

This is a reproducible demonstration that the free gate blocks known wash sellers with stamped preflight_ids and zero spend. It is not proof that external agents already default to this path at scale.

Historical: foreign-wallet block proof (2026-07-23; official @x402 stack, fresh key, zero history)

Verified 2026-07-23: the same pre-sign block with a brand-new wallet that has zero TWZRD/corpus history — proving the mechanism is wallet-independent, not a whitelisted internal path.

  • Wallet: 3GMuabSAATKEXTchSpyP1y5raBS7y6Kx8GShdAbiDLce (generated fresh; corpus pre-check: intel_score: 0, paid_calls: 0, facilitator footprint found: false)
  • Stack: official @x402/core client + @x402/fetch + @x402/svm ExactSvmScheme, TWZRD via installTwzrdX402ClientHook at onBeforePaymentCreation
  • Result: verdict: green_block · reason: twzrd_can_spend_false · sign_after_abort: false · decision_count: 1 · usdc_spent: 0 — payload creation aborted on a live mainnet 402 (payTo GFpLvo…, $0.001) before any signing

Repro with your own fresh key (no SOL/USDC needed — the block path never funds):

node --input-type=module -e "
import { randomBytes } from 'node:crypto';
import { writeFileSync } from 'node:fs';
import { createKeyPairSignerFromPrivateKeyBytes } from '@solana/kit';
import { base58 } from '@scure/base';
const seed = randomBytes(32);
const s = await createKeyPairSignerFromPrivateKeyBytes(seed);
writeFileSync('fresh-wallet.json', JSON.stringify({ privateKey: base58.encode(seed), address: s.address }), { mode: 0o600 });
console.log(s.address);
"
SVM_KEYPAIR_PATH=./fresh-wallet.json npm run official-dogfood -- --block

Honest scope: run by the TWZRD team on TWZRD infra, so it is wallet-independence proof, not an external adoption datapoint — that requires a non-internal operator running this same command with their own attribution flags.

Canonical integration (official x402 client)

import { x402Client } from "@x402/core/client";
import { wrapFetchWithPayment } from "@x402/fetch";
import { ExactSvmScheme } from "@x402/svm/exact/client";
import { installTwzrdX402ClientHook } from "twzrd-x402-gate";

const client = new x402Client();
client.register("solana:*", new ExactSvmScheme(svmSigner));
// Optional: client.register("eip155:*", new ExactEvmScheme(evmSigner));

installTwzrdX402ClientHook(client, {
  gateOnCanSpend: false, // decision-only default (warn allowed)
  refuseWashFlagged: true,
});
// Strict opt-in: gateOnCanSpend: true — also block when can_spend=false
// → onBeforePaymentCreation: scores selectedRequirements, abort if policy denies

const fetchWithPayment = wrapFetchWithPayment(fetch, client);
await fetchWithPayment("https://merchant.example/paid");
official x402 client receives 402
  → selects exact requirement
  → onBeforePaymentCreation
  → TWZRD (network + payTo + amount + resource)
  → local policy allow | warn | block
  → agent-owned wallet signs same requirement

No AgentCash. No marketplace. No second probe. No TWZRD custody.

Note on @x402/core Defaults: @x402/core >= 2.23.0 evaluates default spend controls before invoking TWZRD hooks. Calls breaching core defaults or referencing unrecognized assets abort before the gate preflight or callbacks execute. Use setSpendControls(false) or x402Client.fromConfig if core spend limits should be deferred entirely to TWZRD.

PayKit (@solana/pay-kit)

Foundation pay-kit#303 exposes onBeforeX402PaymentCreation on createPayKitClient and registers it on the internal x402Client. Pass TWZRD as that option — no TWZRD branding inside pay-kit itself. This package does not hard-depend on unpublished @solana/pay-kit.

import { createPayKitClient } from "@solana/pay-kit";
import { createTwzrdPayKitBeforePaymentHook } from "twzrd-x402-gate";

const client = await createPayKitClient({
  accept: ["x402"],
  onBeforeX402PaymentCreation: createTwzrdPayKitBeforePaymentHook({
    refuseWashFlagged: true,
  }),
  rpcUrl,
  signer,
});

Equivalent: onBeforeX402PaymentCreation: installTwzrdAutoGate("pay-kit", { refuseWashFlagged: true }). Same official @x402/core context hook as Path E. Abort returns { abort: true, reason }; PayKit throws before signTransactions.

MCP (@x402/mcp)

Wire twzrdOnPaymentRequested / prefer onPaymentRequired + onBeforePayment per lifecycle hooks. Same policy core.

Raw-fetch composition (injectible pay client only)
import { installTwzrdAutoGate } from "twzrd-x402-gate";
import { wrapFetchWithPayment } from "@x402/fetch";

// Guard RAW fetch, then hand to a client that still surfaces 402 to the guard layer
// — OR installTwzrdAutoGate(x402Client) (canonical) / installTwzrdX402ClientHook alias.
const payingFetch = installTwzrdAutoGate((guarded) =>
  wrapFetchWithPayment(guarded, client),
);

Buyer flow — locked sequence

Canonical path for every agent that spends USDC on Solana x402:

Step Call Cost What gates pay
1 POST /v1/intel/preflight free decision=block → refuse (twzrd_decision_block). Score floor / optional can_spend also deny.
2 GET /v1/intel/merchant_card/{payTo} free wash_flagged: true → refuse by default (twzrd_wash_flagged). Only tightens step 1.
3 First paid hop quickCheck $0.001 On an approved warn with x402Fetch wired: GET /v1/intel/quick/{payTo} only. That hop does not request GET /v1/intel/trust/{payTo}. /trust at $0.05 is a separate allow-path receipt, not the warn hop.
4 Pay (or refuse) resource price Only if steps 1–2 approved (and any opt-in paid escalate did not block).

Fail-open (no invent):

  • Preflight HTTP/network error → default fail-closed (TWZRD_FAIL_OPEN=true restores legacy allow-on-outage).
  • Merchant card outage on a reputation-scored path (5xx, 429, thrown fetch, non-JSON 200) → same failOpen switch; default refuses with twzrd_card_unreachable_fail_closed. This is tighten-only: an already-blocked payment keeps its more specific reason.
  • Merchant card reachable with no wash_flagged (including 4xx, or 200 + insufficient_evidence) → washFlagged=null → do not refuse on wash.
  • Unscored-network observe: a card outage keeps the observe allow; wash_flagged: true still refuses.
  • Only a successful card with wash_flagged: true triggers wash refuse or soft cap.

Wash policy (exact):

  • Prior preflight deny → unchanged (wash never loosens a block).
  • refuseWashFlagged=false or wash not true → keep preflight approval.
  • wash_flagged=true + no cap → approved=false, reason=twzrd_wash_flagged, verdict=block.
  • wash_flagged=true + washMaxUsdc set + priceUsdc <= cap → allow, washCapped=true, reason twzrd_wash_capped_{price}_le_{cap}.
  • wash_flagged=true + price above cap (or price unknown) → refuse with twzrd_wash_flagged_above_cap_*.

Order note: onWarnUpsell (points at first paid hop GET /v1/intel/quick $0.001) fires on preflight warn before the merchant_card wash check. A wash-flagged seller that preflighted as warn may still get the upsell hook, then be refused on step 2. Optional V7 GET /v1/intel/trust $0.05 stays autoReceipt / requireReceipt / escalateOnWarn:false.

Dogfood (one public live proof path):

  • Free only (wash refuse): npm run wash-dogfood → examples/wash-refuse-dogfood.ts
  • Official client + Path E hook (live Solana ≤$0.001): npm run official-dogfood → examples/official-x402-dogfood.ts. Needs @x402/fetch @x402/svm @x402/core @solana/kit @scure/base and a funded Solana key (SVM_KEYPAIR_PATH or ~/.agentcash/solana-wallet.json). --block exercises hard gateOnCanSpend abort with $0 spend.
  • Multi-hook composition (amount cap then TWZRD, abort short-circuit) is proven offline against the real x402Client in test/x402-official-compat.test.ts.
  • Release identity: CLIENT_VERSION is read from package.json (single source of truth); npm test includes version-identity; npm run pack-smoke packs the tarball and checks the installed header.

Install

npm i twzrd-x402-gate

This doc describes 0.12.4. Check npm view twzrd-x402-gate version for the latest release.

CLI commands (package bin)

twzrd-safe-fetch, twzrd-gate-eval-refuse, twzrd-gate-doctor, twzrd-evidence-bundle, twzrd-payment-decision, twzrd-bounty-preflight, twzrd-cold-start (run with npx <name>).

Pay-path behavior (0.12.4)
  • Scored preflight runs on Solana mainnet and Base mainnet (eip155:8453). Other EVM networks do not run the scored preflight.
  • A missing payTo is never approved.
  • A buyer preflight outage does not sign unless TWZRD_FAIL_OPEN=true, except createBuyerFetch, which forces failOpen: false and does not read that variable.
  • An unevaluated seller (null_reason: unknown_subject) can be paid up to its card's recommended_cap_usdc; above the cap it does not sign. refuseUnevaluated: true refuses it outright (see Unevaluated sellers under Policy).
  • A price above recommended_cap_usdc does not sign.
  • Disagreeing v1/v2 price or recipient fields are refused.
  • An approved warn with a wired paying fetch requests /v1/intel/quick and does not request /v1/intel/trust.
  • createTwzrdBeforePaymentHook does not read TWZRD_AUTO_GATE or TWZRD_GATE_ENABLED; those switches apply only to installTwzrdAutoGate.
  • createTwzrdSettleGuard stays fail-open by default, so an unavailable gate does not abort.
  • The pay path is not uniformly fail-closed in any release: createTwzrdSettleGuard is fail-open by default, TWZRD_FAIL_OPEN=true lets a buyer preflight outage sign on entry points that do not force failOpen, and those entry points default unscored networks to observe (allow, wash check still runs). createBuyerFetch is the exception: the outage stays closed, and an unscored network is refused.

Optional settle guard (resource-server payer policy): see Seller settle guard (onBeforeSettle) — optional 0.8.1 above. Do not confuse with facilitator createOnBeforeSettleHook in @wzrd_sol/plugin-trustgate/facilitator — that screens the merchant/payTo on a brokered settle (buyer-side counterparty check at the rail).

Live card screen: npx tsx examples/seller-settle-guard.ts --live <payerWallet>

TWZRD Payment Control (protocol-neutral authorization core)

The gate now ships a protocol-neutral policy runtime underneath the x402 surface. Defining invariant: a signer path that calls assertIntentApproved will not sign an intent that differs from what TWZRD evaluated. (Honest scope: TWZRD does not own third-party wallets - the binding is enforceable exactly where the check runs before the signer, not as a claim over arbitrary wallet internals.)

import {
  evaluateIntent,
  assertIntentApproved,
  createLocalDecisionSigner,
  createDecisionRegistry,
  x402RequirementsToIntent, // or ap2CheckoutToIntent
} from "twzrd-x402-gate";

const signer = createLocalDecisionSigner();
const registry = createDecisionRegistry(); // consume-once

const intent = x402RequirementsToIntent(selectedRequirements, { resourceUrl });
const token = await evaluateIntent(intent, {
  signer,
  mandate,                    // user/company mandate (purpose, ceilings, resource scope)
  policy,                     // local hard controls (caps, lists, recurring checks)
  intelligence: twzrdIntel,   // optional remote counterparty intelligence
});

// Wallet-side, immediately before signing the EXACT intent:
assertIntentApproved(intentBeingSigned, token, {
  registry,                       // replay / consume-once
  publicKeyPem: signer.publicKeyPem, // signature verification
});
// throws MISSING_VERIFIER_KEY | BAD_SIGNATURE | INTENT_HASH_MISMATCH |
//        DECISION_EXPIRED | DECISION_NOT_ALLOW | DECISION_REPLAYED
//        -> the signer is never invoked
// There is no published way to match an intent without verifying the decision
// signature: the old "twzrd-x402-gate/unsafe" subpath was removed in 0.9.4 and
// its module in 0.11.2. Always verify with a key.
  • PaymentIntent v1 (frozen): protocol x402 | ap2 | ucp | mpp | direct + network/asset/amount/payTo + resource + facilitator + mandate + recurrence context, bound into one canonical tiv1: intent hash.
  • Decisions are signed, expiring DecisionTokens - "policy version X approved this exact transaction at this timestamp", auditable offline.
  • A block is a signed decision, not an exception - refusals audit the same way approvals do.
  • Local hard controls (mandate scope, ceilings, allow/blocklists, cumulative caps, recurring price checks) never depend on API availability; remote intelligence (wash/fleet, counterparty score) plugs in via a provider.
  • The category test lives in test/payment-control.test.ts: a mandate permits software under $100, a $12 checkout is approved, orchestration mutates payTo after approval, the wallet refuses on hash mismatch, signerInvocationCount === 0, and both the decision and the refusal verify from audit records alone.
On the client hook (opt-in)

installTwzrdAutoGate(client) (or alias installTwzrdX402ClientHook) wires the runtime into the official onBeforePaymentCreation seat. Pass paymentControl to build the canonical intent, run the policy runtime (with the hook's own preflight fed in as remote intelligence), and surface a signed, intent-bound PaymentDecision:

import { installTwzrdX402ClientHook, createLocalDecisionSigner } from "twzrd-x402-gate";

const signer = createLocalDecisionSigner();
installTwzrdX402ClientHook(client, {
  paymentControl: {
    signer,
    mandate,                       // optional user/company mandate
    policy: { maxAmountUsd: "50" }, // optional local hard controls
  },
  onDecision: ({ intent, decision }) => handOff(intent, decision), // → assertIntentApproved
});
  • Tighten-only composition: a paymentControl block aborts even when the legacy preflight allowed; it never loosens a legacy denial.
  • Opt-in: with paymentControl unset the hook behaves exactly as before.
  • x402 wire amounts (USDC micro units) are converted to the decimal USD the runtime expects by x402RequirementsToIntent (decimals defaults to 6; override via the intent context for other assets), so amount-based policies are not mis-scaled.
  • Hook binding test: test/intent-binding.test.ts.
On MPP (Machine Payments Protocol) — Solana charge only

createTwzrdMppOnChallenge guards Mppx.create({ onChallenge }). The mppx Solana method signs AND broadcasts the transaction inside createCredential(), so onChallenge is the last deterministic checkpoint before money moves - and mppx re-throws onChallenge errors, so a TWZRD block is an exception that means createCredential() never runs and nothing signs. On allow, the guard creates the credential for the exact challenge it evaluated; non-solana/charge challenges fail closed (allowUnevaluated: true to opt out).

import { Mppx } from "mppx/client";
import { client as solanaClient } from "mppx-solana";
import { createTwzrdMppOnChallenge, createLocalDecisionSigner } from "twzrd-x402-gate";

const mppx = Mppx.create({
  methods: [solanaClient({ signer: wallet })],
  onChallenge: createTwzrdMppOnChallenge({
    signer: createLocalDecisionSigner(),
    policy: { maxAmountUsd: "1.00" },
  }),
});

Scope limit (honest): the guard is authoritative only when no onChallengeReceived event handler supplies a credential. mppx resolves eventCredential ?? onChallenge(...), so an event handler returning a credential short-circuits the guard and pays ungated. Do not register both on one client.

What it refuses, and why. Each of these is a case where the transaction mppx would actually sign is not the transaction the decision covers - so the guard fails closed rather than approve a payment it cannot bind:

Case Code Reason
Sponsored charge SPONSORED_CHARGE sponsored / sponsorPath / feeTokenAmount make mppx-solana build a second transfer to the sponsor on top of the advertised amount. PaymentIntent v1 binds one amount to one payTo, so approving it would authorize strictly more value than the decision covers.
Non-USD-pegged asset UNPRICED_ASSET Policy ceilings are USD; the wire amount is base-unit tokens. Pricing SOL would store asset: solana:native beside a dollar amount and lose the token quantity actually transferred. (A 1.5 SOL charge of 1500000000 at 9 decimals would otherwise evaluate as "$1.50" and sail under a $5 ceiling while moving ~$270.) USDC/USDT only until an intent version carries token amount + quote.
Unknown cluster UNKNOWN_CLUSTER mppx-solana's resolveEndpoint returns an unrecognized cluster verbatim as the RPC endpoint URL, and a network string containing "solana" is otherwise scored as mainnet - so solana:https://seller-rpc.example would inherit mainnet reputation for a chain never observed. Known cluster names only.
Misdeclared decimals MALFORMED_CHALLENGE A known stablecoin declaring the wrong decimals is a discount attempt, not a rounding error.

The intent binds a digest of the entire normalized challenge, not just realm:id - swapping recipient or amount under the same challenge id changes the intent hash, so the decision no longer matches.

Verdicts: block throws. warn proceeds to pay by default - set treatWarnAsBlock: true to refuse on warn too.

  • mppChallengeToIntent binds the challenge id + realm into the intent (resource.operation), so the signed decision covers this exact challenge.
  • Cluster names stay honest: solana:devnet classifies recognized-but-unscored.
  • Proof: test/mpp-hook.test.ts - block means credential count 0 and signer count 0.
Experimental CLI: twzrd-safe-fetch (AgentCash advisory pre-check)

Not a challenge-bound firewall. Classification: advisory_precheck.

AgentCash CLI internalizes 402 handling. This tool can only decide whether to invoke AgentCash after scoring a probe challenge. AgentCash then makes a second request and may sign a different recipient/network/amount (TOCTOU). JSON output always sets requirementScoredMatchesRequirementSigned: false.

Secure integrations: installTwzrdAutoGate over raw fetch + injectible pay client, or twzrdOnPaymentRequested (MCP). Do not wrap AgentCash's paying fetch with withTwzrdGuard.

# Advisory: block AgentCash invocation when can_spend=false
npx twzrd-safe-fetch https://example/paid --gate-on-can-spend --payment-network solana --json

# Dry-run: preflight only, zero USDC
npx twzrd-safe-fetch 'https://intel.twzrd.xyz/v1/intel/quick/BJGdsDXJFy63eCAnX3UmGfShp8BuqbtkTfcamyRGr7VQ' --dry-run --json
probe request → TWZRD scores challenge A → (if allowed) AgentCash request → may sign challenge B
  • Exit 2 = policy blocked (AgentCash never started).
  • Exit 0 = passthrough / dry-run allowed / AgentCash returned success (binding unproven).
  • Base mainnet (eip155:8453) is scored, like Solana mainnet. Other EVM networks do not run the scored preflight (see Networks).
CLI: twzrd-bounty-preflight (worker-side refuse, zero spend)

For agents that work bounty boards (DeskCrew arena, ClawTasks). Before you pay an attempt fee or stake collateral, read the board, read the paid door's unpaid 402 for its payTo, gate that payTo, and decide each open row. One JSON transcript on stdout (schema: twzrd.bounty_preflight.v1, usdc_spent: 0, signer_invocation_count: 0). Exit 0 means at least one open row is eligible — pay only rows with proceed: true. Exit 1 means none are. Nothing signs, nothing spends.

# DeskCrew: ticket-only attempt cost; the row's entry fee is added separately
npx twzrd-bounty-preflight --board https://deskcrew.io/api/arena/contests \
  --attempt-cost-usd 0.02 --max-attempt-usd 0.25 --assumed-win-prob 0.2

# Any other board: name its paid door explicitly
npx twzrd-bounty-preflight --board https://clawtasks.com/api/bounties?status=open \
  --paid-endpoint https://clawtasks.com/api/paid --my-networks base --allow-unscored-payee

Refuse reasons, in order: gate_block / gate_wash_flagged (TWZRD refused the payee), gate_unscored (the payee's rail is outside the scored corpus — Solana mainnet and Base mainnet (eip155:8453) are scored; other rails are not; the gate's "allow" there is a policy pass-through, never a trust allow; opt in with --allow-unscored-payee, wash still refuses), payout_network_unsupported / payout_network_unknown, attempt_cost_over_max (attempt fee + board entry fee + stake vs the ceiling), below_break_even / assumed_win_prob_missing (your declared win probability vs at_risk / (agent_share * bounty)). The board's approval rate and contest size are reported as naive and contested EV for context only; they are never substituted for --assumed-win-prob. Self-serve transcript, not adoption proof.

Library: import { safeFetch } from "twzrd-x402-gate/safe-fetch".

Quickstart: installTwzrdAutoGate (default-on)

Canonical entry point (design: docs/strategy/install-autogate-design.md). One name, five adapters:

Call Adapter
installTwzrdAutoGate(payWrap, opts?) Fetch: guard raw fetch → pay client
installTwzrdAutoGate(x402Client, opts?) Official x402 onBeforePaymentCreation
installTwzrdAutoGate("x402-solana", opts) PayAI beforePayment on createX402Client
installTwzrdAutoGate("pay-kit", opts) PayKit onBeforeX402PaymentCreation (Foundation #303)
installTwzrdAutoGate("mpp", opts) MPP onChallenge (returns handler)

Aliases: installTwzrdX402ClientHook, createTwzrdMppOnChallenge remain; docs prefer AutoGate. Kill switch: TWZRD_GATE_ENABLED=false or TWZRD_AUTO_GATE=0 applies to installTwzrdAutoGate only. createTwzrdBeforePaymentHook does not read those variables. Uninstall x402 installs with uninstallTwzrdAutoGate(client). installTwzrdAutoGate reads that switch per call on the x402-client, x402-solana, pay-kit and MPP seats before it calls the factory, so flipping it moves an already-installed AutoGate seat in both directions. The fetch / payWrap seat is the exception: it resolves the switch once, when the fetch is composed, so a fetch built while the switch was off stays ungated after you clear it — rebuild the fetch. options.disabled: true is a permanent install-time opt-out everywhere and no env change revives it.

installTwzrdAutoGate is the one-liner form of "guard the raw fetch, then hand it to your x402 client." It takes a payWrap function — whatever composes your paying client on top of a fetch — and returns a fetch that's already gated: a blocked seller throws before your client ever gets a chance to sign.

import { installTwzrdAutoGate } from "twzrd-x402-gate";
import { wrapFetchWithPayment } from "@x402/fetch";

const payingFetch = installTwzrdAutoGate((guarded) => wrapFetchWithPayment(guarded, buyerWallet));

// Use payingFetch everywhere you'd call a paid resource:
const response = await payingFetch("https://api.exa.ai/search");

payWrap receives the guarded fetch (the guard has already run by the time your client sees a 402) — this is the only correct composition order. Building it the other way round (guarding an already-paying fetch) is a no-op; see Compatibility note. Any x402 client that composes over an underlying fetch works the same way — swap in whatever payWrap your client's API expects (agentcash, ClawRouter, PayAI, a custom @x402/svm scheme, etc.).

This installTwzrdAutoGate(payWrap) seat is default ON. Disable that seat with TWZRD_AUTO_GATE=0 or TWZRD_GATE_ENABLED=false, or with { disabled: true } (install option, for example in tests) — the raw fetch is handed straight to payWrap, unguarded. createTwzrdBeforePaymentHook does not read those variables. Setting them does not stop that factory from aborting a bad payment.

What happens on every HTTP 402 the raw fetch returns:

  1. Every distinct accepts[] entry has to pass. The guard does not keep only the Solana entry.
  2. Calls POST /v1/intel/preflight — free, no auth. decision=block (or score floor) throws — payWrap's client never signs.
  3. Calls GET /v1/intel/merchant_card/{payTo} — free, no auth. wash_flagged: true refuses by default. A reachable card with no wash finding is not a refusal unless refuseWashUnknown is on. A card outage on the scored path honours failOpen (default fail-closed).
  4. An unscored network (keyword devnet, Base Sepolia, other EVM) is allowed under the default observe mode. The wash check still runs. createBuyerFetch refuses that network instead.
  5. Otherwise returns the 402 to payWrap's client, which pays normally.

Non-402 responses pass through unchanged.

Legacy: createGuardedX402Fetch

Kept for compatibility. A new integration should use createBuyerFetch. The older wrapper's hourly budget is in memory only, its caps are optional, and unscored networks follow observe instead of createBuyerFetch's strict.

createGuardedX402Fetch combines a configured @x402/core client with the existing TWZRD pre-sign evaluator and local recipient / spend rules. Register the payment schemes and wallet signer on the client first; the helper returns the paying fetch, so the request can use the normal x402 402 flow:

import { x402Client } from "@x402/core/client";
import { createGuardedX402Fetch } from "twzrd-x402-gate";

const client = new x402Client();
// Register the x402 payment schemes and wallet signer on `client` here.

const guardedFetch = createGuardedX402Fetch({
  client,
  maxPricePerCall: "0.05",
  hourlyBudgetCap: "2.00",
  allowedRecipients: ["0x1234567890abcdef1234567890abcdef12345678"],
});

const response = await guardedFetch("https://api.example.com/paid-resource");

The local checks run against the x402 client's selected payment requirement, then TWZRD's existing @x402/core pre-sign evaluator runs before the client creates the payment signature. The caps are decimal USDC values compared with the selected requirement's six-decimal atomic amount. When a spend cap is enabled, the helper accepts only canonical USDC on Solana mainnet/devnet and Base mainnet/Sepolia; unsupported tokens and networks fail closed. EVM recipient matching is case-insensitive; Solana base58 matching is case-sensitive. Supplying an empty allowedRecipients array denies every recipient.

The hourly ledger is in memory for the lifetime of this returned fetch and is shared by concurrent calls through it. Spend is reserved while TWZRD evaluates the challenge and recorded before the pre-sign hook returns, so a later signer failure still consumes the budget. It resets when the process restarts. The existing TWZRD_AUTO_GATE=0 / TWZRD_GATE_ENABLED=false switches disable the remote TWZRD evaluation only; local caps and the recipient allowlist remain on. The helper expects a configured x402 client because a bare signer.pay() method does not define a standard x402 challenge-to-payment contract. A separate paid Path A fetch cannot be passed through twzrd.x402Fetch: it has a different signer and cannot share this fetch's local ledger.

Fulfillment record (opt-in, local only)

Pass onFulfillment to keep one row per paid call: who was paid, the settlement transaction, who the seller says served, and whether the seller says the job became ready. Nothing is sent anywhere, and it never changes what is allowed or refused.

import { createFileFulfillmentLog, createGuardedX402Fetch } from "twzrd-x402-gate";

const log = createFileFulfillmentLog("./fulfillment.jsonl");
const guardedFetch = createGuardedX402Fetch({ client, onFulfillment: log.record });

Each row (schema: "twzrd.fulfillment.v1") carries paid_to, network and amount_atomic from the requirement the client signed, tx and settled from the facilitator's PAYMENT-RESPONSE, and, from the seller's JSON body only, served_by (provider or model), requested_model, failover_hops, ready, job_status and job_id. The resource is kept as its origin; no other body field is copied. verdict is one of:

verdict meaning
match The seller reports ready: true, or it served the model that was requested (a dated snapshot of it counts). For a GPU job this is the seller's claim that capacity is ready, not evidence that the workload ran or its output is correct.
pending The seller reports ready: false with a non-terminal status such as provisioning. Readiness arrives later; it is not a failure.
mismatch One or more reasons: not_ready (terminal status without ready), model_substituted, tx_disagrees (the body's tx is not the PAYMENT-RESPONSE tx), settle_failed, paid_http_error.
not_reported The response carries none of these fields.

The row records the seller's own claims next to a transaction anyone can check on chain. It is not a delivery proof, and it is not a TWZRD receipt: the signed V7 receipt from paid /trust attests TWZRD's check of a seller, not that a GPU or a model was delivered. A GPU seller's provider is often a node label, so it is recorded but never compared with paid_to. To record a later job-status body (for example once the job is ready), call buildFulfillmentRecord with that body and append the row yourself. A proxy that pays internally and returns 200 never exposes a payment header, so no row is written for it.

Networks (Solana mainnet and Base mainnet scored)

The gate recognizes multi-chain 402s. Solana mainnet and Base mainnet (eip155:8453) run the scored preflight. Other EVM networks do not. eip155:137 does not run twzrdPreflight or the scored preflight.

Network Reputation scored? Default policy (unsupportedNetworkMode)
Solana mainnet Yes — free preflight + merchant_card allow/block from intel
Base mainnet (eip155:8453, base) Yes — free preflight + merchant_card allow/block from intel
Other EVM (eip155:* except 8453) No scored preflight observe (default): decision=unknown, policyAction=allow, telemetry unsupported_network_seen. Wash still runs — wash_flagged refuses before sign.
Other EVM in strict mode No policyAction=block before sign

An unscored network is never represented as a TWZRD trust allow. Set TWZRD_UNSUPPORTED_NETWORK_MODE=strict (or { unsupportedNetworkMode: "strict" }) to hard-block unscored networks.

requireReceipt (Path A) follows the same line: hard receipts apply to scored networks only — on an unscored network there is no receipt to buy, so the decision carries receiptSkipped: "unscored_network" and continues. Use strict mode to fail closed there.

requireLogInclusion sits one step further: a captured Path A receipt does not count as trust until its leaf is proven included in the Receipt Transparency log under a key you pinned (spec). A valid signature proves authorship and integrity; only the log proves the issuer showed everyone the same answer. The log is live (v0.1 domain, genesis 2026-09-03), and a paid response carries its proof inline as log_inclusion — the audit path plus the signed head it targets — once the leaf is merged, so the usual case verifies offline with nothing but your pinned key. The gate takes no dependency on the log verifier — you wire it; it receives the receipt and the whole paid response, and twzrd-log-verifier's results fit the verdict shape with no adapter:

import { verifyLogInclusion, verifyReceiptInLog } from "twzrd-log-verifier"; // not yet on npm — see its README

// Today the log serves a single signing key (the receipt issuer key); pin it out of band.
// When v0.2 ships a key directory, pass that here instead — same call.
const PIN = "Ak5SQwHpuQAqU7ty7ZWX7qgF39A9yi72c22KNn8sHzvS";

await evaluate_x402_resource(url, requirements, {
  requireReceipt: true,
  x402Fetch,
  requireLogInclusion: {
    verifier: async (_receipt, ctx) =>
      ctx.logInclusion
        ? verifyLogInclusion(ctx.response, PIN)                                  // offline: no round-trip
        : verifyReceiptInLog({ baseUrl: "https://intel.twzrd.xyz", receipt: ctx.response, trusted: PIN }), // fetch; 404 ⇒ pending
    // hard: true         — an unproven receipt denies spend (default)
    // onPending: "deny"  — a leaf not merged yet is unprovable at pay time; "allow"
    //                      tolerates the one-anchor-period merge window (default "deny")
    // refuseTofu: true   — never accept keys the log advertised about itself (default)
  },
});

The receipt is returned either way (result.receipt) — you paid for it. What changes is approved: a denial sets logInclusionDenied: true, policyAction: "block", and a reason of twzrd_log_inclusion_failed | _pending | _tofu_refused | _error; result.logInclusion carries the verdict (key_id, leaf_index, tree_size, pending, tofu). A verifier that throws denies under hard — a broken or unreachable verifier must not wave receipts through. So does a Path A attempt that yields no receipt at all (non-OK response, thrown fetch, x402Fetch not wired): an outage on the receipt endpoint must never produce a better outcome than an empty receipt body. Path A that your requireReceipt threshold never attempted is out of scope — this knob gates receipts, it does not override your threshold.

Two things to know today: a leaf is merged within one anchor period, so a receipt paid for just now may not yet carry log_inclusion — the fetch fallback above then reports pending, and onPending decides. And nothing is anchored on Solana yet (anchor_authority is null), so inclusion currently proves what the log committed to, not yet when — the anchor is what makes backdating detectable.

Dual-chain accepts still prefer the Solana entry for scoring (same as payment clients that prefer Solana when available).

Lower-level: withTwzrdGuard

installTwzrdAutoGate is built on withTwzrdGuard — the fetch wrapper itself, if you want to manage the raw/paying composition yourself:

import { withTwzrdGuard } from "twzrd-x402-gate";
import { wrapFetchWithPayment } from "@x402/fetch";

const raw = globalThis.fetch;               // MUST still surface HTTP 402
const guarded = withTwzrdGuard(raw);        // guard sits upstream
const safeFetch = wrapFetchWithPayment(guarded, buyerWallet);

const response = await safeFetch("https://api.exa.ai/search");

What the guard does on HTTP 402:

  1. Reads the Solana-network entry from accepts[] (falls back to first entry) to get the seller wallet.
  2. Free POST /v1/intel/preflight — decision=block / score floor deny.
  3. Free GET /v1/intel/merchant_card/{payTo} — wash_flagged:true refuses by default (reason: twzrd_wash_flagged). Reachable + no signal fails open (no invent). A card outage on the scored path honours failOpen (default fail-closed).
  4. If approved: returns the original 402 for the x402 client to pay.

Opt out of wash refuse: withTwzrdGuard(fetch, { refuseWashFlagged: false }) or TWZRD_REFUSE_WASH_FLAGGED=0. Soft cap instead of hard refuse: washMaxUsdc / TWZRD_WASH_MAX_USDC.

Non-402 responses pass through unchanged.

First paid hop on warn (revenue path)

Wiring x402Fetch turns on buyer defaults: a proceeding warn settles GET /v1/intel/quick/{payTo} at $0.001 (escalateOnWarn). That is the first paid hop.

Optional V7 — autoReceipt or material-allow requireReceipt — buys GET /v1/intel/trust/{payTo} at $0.05 for a signed credential. Opt out of the cheap hop with escalateOnWarn: false if the host explicitly wants /trust first.

const safeFetch = withTwzrdGuard(x402Fetch, {
  x402Fetch,           // warn → $0.001 /quick first
  autoReceipt: true,   // optional V7: also buy $0.05 /trust on allow (not after /quick)
  onReceipt: (receipt, tx) => {
    // receipt is a twzrd_receipt (V7 + ERC-8004 reputation_credential)
    console.log("Trust receipt captured:", tx);
  },
});

autoReceipt is off by default — it spends the buyer's USDC, so you opt in. When on and /quick did not already settle, warn/allow can buy the $0.05 V7 credential.

x402Fetch is yours to supply (this package is dependency-free). Wire the proven @x402/svm sponsored-feePayer client — the same one twzrd-mcp-server uses:

import { wrapFetchWithPayment } from "@x402/fetch";
const x402Fetch = wrapFetchWithPayment(fetch, buyerWallet); // settles 402 challenges

Gate it behind your own ROI policy (e.g. only auto-buy the receipt for payments above a threshold). Runnable, no-spend demo: examples/auto-receipt.ts (npm run autoreceipt-demo). A bundled/sponsored x402Fetch (so integrators need no wallet) is the next step.

Quick tier ($0.001) — cheap paid qualify

The reputation ladder has three rungs: free preflight (allow/warn/block), $0.001 quickCheck (tier + score, no receipt), $0.05 autoReceipt (full intel + signed V7 receipt). When an evaluated warn is inconclusive and you want a cheap paid confirmation before committing — without paying 50× for the portable receipt — use quickCheck:

import { quickCheck } from "twzrd-x402-gate";

const q = await quickCheck(sellerWallet, { x402Fetch }); // settles $0.001 to /v1/intel/quick
if (q.available && (q.tier === "Gold" || q.tier === "Platinum")) {
  // tier is high enough — proceed with the larger payment
}

quickCheck is fail-soft — it never throws; any gap (no x402Fetch, unreachable, settle failure) returns available: false, so a quick-tier hiccup can't break your flow. The hard allow/warn/block decision stays the free preflight's job.

Autonomous risk-escalation — escalateOnWarn (pay-to-confirm on warn)

An evaluated warn (no null_reason, score present) proceeds by default. A card with null_reason: unknown_subject is allowed only up to its recommended_cap_usdc (see Policy), and this hop never runs for it: there is no paid score for a seller intel has never seen either, so the $0.001 would buy nothing. escalateOnWarn closes the loop on a proceeding warn: the guard settles the cheap $0.001 GET /v1/intel/quick/{payTo} and re-decides on the paid score — below the floor the payment is blocked, at/above it proceeds. That hop does not request GET /v1/intel/trust/. The paid call fires from your agent's own risk policy (no human), and the paid signal actually gates the spend (unlike autoReceipt, which is upsell-only and never changes the decision).

const safeFetch = withTwzrdGuard(x402Fetch, {
  escalateOnWarn: {
    minSpendUsdc: 0.01,   // don't pay $0.001 to vet a sub-cent buy
    blockBelowScore: 40,  // block when the paid quick score is below this (default: preflightMinScore)
  },
  x402Fetch,              // settles the $0.001 quick charge
});
// warn + paid score < 40  -> throws "[twzrd-guard] payment blocked: twzrd_escalated_warn_block ..."
// warn + paid score >= 40 -> proceeds (result.escalated=true, result.escalatedScore set)

Opt-in, fail-soft (if the quick tier can't answer, the base warn is preserved), and it only tightens — a warn may become a block, but an allow or block is never changed. This is the autonomous demand loop: an uncertain counterparty is vetted with real paid intel, automatically, before your agent commits.

Sponsored payer — use the paid rungs with no wallet (prototype)

createSponsoredX402Fetch lets a sponsor settle the paid rungs on the agent's behalf, so an integrator can call quickCheck / autoReceipt with no wallet of their own:

import { createSponsoredX402Fetch, quickCheck } from "twzrd-x402-gate";

// `settle` = the funded backend (your @x402/svm fetch, or a TWZRD treasury sponsor endpoint).
const x402Fetch = createSponsoredX402Fetch({ settle });
const q = await quickCheck(seller, { x402Fetch }); // sponsor pays — caller holds no wallet

Two backends plug into settle: gas-sponsored (live via @x402/svm — agent pays USDC, the resource server's feePayer covers SOL gas, the model twzrd-mcp-server uses) and full-sponsor (a TWZRD treasury endpoint pays on the agent's behalf — the true no-wallet path). The full-sponsor endpoint + treasury is founder-gated (who funds it + per-agent budget caps); this ships the client seam + a dry-run so the wiring is ready. No-spend demo: examples/sponsored-payer.ts (npm run sponsored-demo).

evaluate_x402_resource — standalone preflight

Use when you already have the paymentRequirements object from a parsed 402 body:

import { evaluate_x402_resource } from "twzrd-x402-gate";

const result = await evaluate_x402_resource(
  "https://api.exa.ai/search",
  paymentRequirements, // X402PaymentRequirements from the 402 body
);

console.log(result.decision);    // "allow" | "warn" | "block"
console.log(result.trustScore);  // number | null
console.log(result.approved);    // boolean
console.log(result.receiptUrl);  // "https://intel.twzrd.xyz/v1/intel/trust/<payTo>"

if (!result.approved) throw new Error(`Blocked: ${result.reason}`);

With autoReceipt:

const result = await evaluate_x402_resource(url, requirements, {
  autoReceipt: true,
  x402Fetch: myPayingFetch,
  onReceipt: (receipt, tx) => storeCredential(receipt),
});
// result.receipt — twzrd_receipt (V7 + ERC-8004 reputation_credential)
// result.receiptTx — on-chain settlement tx
// result.receiptFeeCaptured — true when fee landed

Fee-payer preference (multi-facilitator accepts[])

When a seller lists more than one facilitator in its 402 accepts[] (e.g. Dexter and TWZRD), you can tell the gate which fee payer to settle through. It selects the matching entry the seller already offers — it never adds, rewrites, or forces an entry onto the seller's 402, and falls back to the normal Solana-mainnet preference when nothing matches.

Opt-in, off by default. Set once via env:

# prefer TWZRD's facilitator when a seller multi-lists (alias resolves to its feePayer)
export TWZRD_PREFER_FEE_PAYER=twzrd
# or any explicit base58 fee payer you want to route to
export TWZRD_PREFER_FEE_PAYER=4LkEFjJdXARkKx8FBx4LBFa2SvJNmjQpgGDLoJcypZUE

or programmatically:

import { pickRequirements, TWZRD_FEE_PAYER } from "twzrd-x402-gate";

const chosen = pickRequirements(body.accepts, { preferFeePayer: TWZRD_FEE_PAYER });

With no preference set, selection is unchanged (first Solana-mainnet entry).

Lower-level APIs

Direct approval call
import { createTwzrdGate } from "twzrd-x402-gate";

const gate = createTwzrdGate();
const { approved, reason, card } = await gate.approvePayment({
  payTo: "SELLER_WALLET_FROM_402",
  resourceUrl: "https://merchant.example/paid",
  priceUsdc: 0.003,
});
if (!approved) abort(reason);
@x402/mcp payment hook

The hook accepts the real @x402/mcp v2 PaymentRequestedContext ({ toolName, arguments, paymentRequired }) — wire it directly:

import { createx402MCPClient } from "@x402/mcp";
import { registerExactSvmScheme } from "@x402/svm/exact/client";
import { twzrdOnPaymentRequested } from "twzrd-x402-gate";

const client = createx402MCPClient({
  name: "my-agent",
  version: "1.0.0",
  schemes: [/* e.g. registered SVM scheme */],
  autoPayment: true,
  onPaymentRequested: (ctx) => twzrdOnPaymentRequested(ctx), // false = deny before signing
});

The legacy flat shape ({ accepts, context }) is still accepted. Prior to 0.6.1, only the flat shape was read — wired into the real @x402/mcp runtime the hook saw accepts: undefined and fail-closed-blocked every payment (safe, but a 100% false-block).

wrapFetchWithTwzrdGate
import { wrapFetchWithTwzrdGate, resolveConfig } from "twzrd-x402-gate";

// Alternative fetch wrapper — same interception logic, no autoReceipt.
const gatedFetch = wrapFetchWithTwzrdGate(fetch, resolveConfig());

withTwzrdGuard is preferred — it composes with autoReceipt and onReceipt. wrapFetchWithTwzrdGate remains for codebases that can't migrate.

Policy

A payment is blocked when:

  1. decision ∈ blockDecisions (default: ["block"])
  2. trust_score < preflightMinScore (default: 40), after an evaluated card. null_reason: unknown_subject (or score: null) is not a low score: see Unevaluated sellers below.
  3. can_spend === false — only when gateOnCanSpend: true (default false, opt-in)
  4. the price is above recommended_cap_usdc when the card was otherwise approved
  5. on Solana or Base, the requirement names an asset that is not USDC on that network (twzrd_non_usdc_asset, 0.11.1+). Every cap is in USDC, and amount is in the named asset's base units, so any other asset cannot be priced. A requirement that names no asset is read as USDC.

Unevaluated sellers (0.11.0+). Live intel answers a seller it has never evaluated with null_reason: unknown_subject, score: null, a floor trust_score of 45, decision: "warn" and a recommended_cap_usdc. For that floor score live intel's ceiling is $0.10, and the card reports the lower of $0.10 and the requested price (2026-09-28). The gate follows that card instead of refusing every new seller, so a never-seen seller can be paid up to $0.10 per call:

Case Result Reason
price at or under recommended_cap_usdc signs twzrd_unevaluated_within_cap_<price>_le_<cap>
price above the cap does not sign twzrd_unevaluated_over_cap_<price>_gt_<cap>
card carries no finite cap does not sign twzrd_unevaluated_no_cap_<null_reason>
price unknown does not sign twzrd_unevaluated_unknown_price_<null_reason>
decision other than allow / warn does not sign twzrd_unevaluated_subject_<null_reason>
refuseUnevaluated: true does not sign twzrd_unevaluated_subject_<null_reason>

An approval here carries unevaluated: true and score: null, never a trust score. The free wash check still runs after it, so a wash-flagged unevaluated seller does not sign. refuseUnevaluated: true (or TWZRD_REFUSE_UNEVALUATED=1) restores the 0.9.9–0.9.16 behaviour: null_reason: unknown_subject returns reason twzrd_unevaluated_subject_unknown_subject and nothing unevaluated signs.

A price above recommended_cap_usdc returns a reason that starts with twzrd_over_recommended_cap_. A direct createTwzrdBeforePaymentHook abort of a block card returns reason twzrd_decision_block. That reason string is not block and is not twzrd_fail_closed. A missing payTo returns reason twzrd_unidentifiable_payment_recipient from twzrdApprovePayment. That reason is not twzrd_missing_payTo. When the buyer preflight fetch throws and TWZRD_FAIL_OPEN is unset, twzrdApprovePayment returns reason twzrd_fail_closed. That reason is not twzrd_preflight_fetch_error.

An evaluated warn (no null_reason, score present) is allowed unless overridden. When the buyer preflight fetch throws and TWZRD_FAIL_OPEN is unset, twzrdApprovePayment returns approved: false with reason twzrd_fail_closed and the wallet does not sign. An omitted failOpen on createTwzrdSettleGuard is a different default: a thrown screen returns without abort. 0.12.4 is not uniformly fail-closed: createBuyerFetch refuses that outage and does not read TWZRD_FAIL_OPEN, the other buyer entry points honor the env, and createTwzrdSettleGuard stays fail-open when failOpen is omitted.

A 402 whose payment requirements yield no identifiable seller wallet (missing/empty payTo, or an unparseable accepts[]) is a different case from "unknown seller" — it always blocks with reason: twzrd_unidentifiable_payment_recipient, without ever calling the preflight network. The wallet does not sign. This is unconditional (not affected by buyer failOpen): that switch governs a buyer preflight outage, not a missing payTo. An omitted failOpen on createTwzrdSettleGuard is a different default: a thrown screen returns without abort.

can_spend note: the free preflight returns can_spend=false for most sellers not yet in the TWZRD corpus, including legitimate ones. can_spend: false alone does not block (gateOnCanSpend defaults false). That is separate from null_reason: unknown_subject, which signs only within its card's cap. Set gateOnCanSpend: true to also block on can_spend: false.

Refusals

Every reason the gate returns, by entry point. A refusal means the wallet was not asked to sign (signerInvocations = 0). test/readme-refusal-codes.test.ts fails if a reason string in the source is missing here, so this table cannot drift from the code. <...> marks a value filled in at runtime.

Buyer approval (twzrdApprovePayment, used by every buyer entry point)

On the x402 client hooks (createTwzrdBeforePaymentHook, installTwzrdAutoGate, createTwzrdPayKitBeforePaymentHook, createGuardedX402Fetch) the abort reason is [twzrd] <reason> payTo=<payTo> network=<network>.

Reason When Default
twzrd_unidentifiable_payment_recipient The requirement names no payTo. Intel is not called. always
twzrd_invalid_price A caller passed a priceUsdc that is negative or not finite. Intel is not called. always (0.11.2+)
twzrd_non_usdc_asset On Solana or Base, the requirement names an asset that is not USDC on that network. Intel is not called; failOpen does not apply. always (0.11.1+)
network_not_scored, network_missing The network is not scored (not Solana or Base mainnet). A name containing devnet, testnet, or sepolia, including Base Sepolia, is unscored. A Solana devnet named only by its CAIP-2 genesis id is still scored. refused when unsupportedNetworkMode is strict. The shared default is observe (allow). createBuyerFetch sets strict
twzrd_decision_<decision> Intel's card decision is in blockDecisions. twzrd_decision_block
twzrd_unevaluated_subject_<null_reason> Intel has never evaluated the seller, and refuseUnevaluated is set or the decision is not allow/warn. strict mode off
twzrd_unevaluated_no_cap_<null_reason> Never evaluated, and the card has no finite recommended_cap_usdc. always
twzrd_unevaluated_unknown_price_<null_reason> Never evaluated, and the price is unknown. always
twzrd_unevaluated_over_cap_<price>_gt_<cap> Never evaluated, and the price is above the card's cap (live: min($0.10, price)). always
twzrd_can_spend_false The card says can_spend: false. only with gateOnCanSpend: true
twzrd_score_<score>_below_<min> An evaluated seller scores below preflightMinScore. min 40
twzrd_over_recommended_cap_<price>_gt_<cap> An evaluated seller, and the price is above the card's recommended_cap_usdc. always
twzrd_wash_flagged The free merchant card says wash_flagged: true. on (refuseWashFlagged defaults true on every entry point)
twzrd_wash_flagged_above_cap_<price>_gt_<cap>, twzrd_wash_flagged_above_cap_unknown_price_max_<cap> Wash-flagged, washMaxUsdc is set, and the price is above it or unknown. only with washMaxUsdc
twzrd_wash_unknown The merchant card answered but has not fully answered the wash question: no card (including a 4xx), wash confidence other than full (for example partial_inbound_only, base_2cycle), ring_evaluated: false, or wash_stale: true. Never reported as an outage; failOpen does not open it. Scored networks only. opt-in (refuseWashUnknown: true; off by default in 0.12.x, on from 0.13.0; also needs refuseWashFlagged)
twzrd_fail_closed (<error>) The preflight did not answer (non-2xx, non-JSON, network error, or no answer within intelTimeoutMs). fail-closed unless failOpen
twzrd_card_unreachable_fail_closed (<error>) The merchant card did not answer (5xx, 429, non-JSON, network error, or no answer within intelTimeoutMs). A 4xx is an answer, not an outage. With failOpen the payment is allowed and the result carries cardUnreachable: true (the wash check did not run). fail-closed unless failOpen

Approval reasons, for onDecision consumers: twzrd_allow, twzrd_warn_allowed, twzrd_unevaluated_within_cap_<price>_le_<cap>, twzrd_wash_capped_<price>_le_<cap>, twzrd_fail_open (an outage allowed because failOpen is set).

x402 client hook only
Reason When
amount_field_conflict, payto_field_conflict The requirement's v1 and v2 price fields (maxAmountRequired / amount) or recipient fields (payTo / pay_to) disagree. Two spellings of the same 0x address are one recipient.
amount_malformed The amount is present but is not an ASCII base-unit integer (a sign, decimal point, exponent, whitespace or non-ASCII digit). Refused before intel on every entry point (0.11.2+); twzrd.safeFetch reports it as malformed_amount.
payment_control_unevaluable: missing <field> Payment Control is on and the requirement has no amount or payTo.
payment_control_block:<reason codes> Payment Control (paymentControl option) refused the intent.
twzrd_escalated_warn_block (paid quick score <score> < <floor>) escalateOnWarn bought the $0.001 /quick score and it is below the floor.
twzrd_receipt_required_missing_x402Fetch requireReceipt needs a paid receipt and no x402Fetch is wired.
twzrd_receipt_required_failed (HTTP <status>), twzrd_receipt_required_error (<error>) The required paid receipt could not be bought.
aborted_before_payment: signal already aborted The caller's abort signal fired before the hook ran.
Fetch wrappers and the MCP hook

withTwzrdGuard, wrapFetchWithTwzrdGate, installTwzrdAutoGate(payWrap) and twzrdOnPaymentRequested see the whole 402 but not which accepts[] entry the paying client will choose, so every distinct entry must pass the buyer approval (one free preflight each; paid hops run once, for the preferred entry). The first refusal is reported with that entry's payTo (0.11.2+).

Reason When
too_many_payment_options The 402 lists more than 8 distinct offers.
./cloudflare-base (edge Worker seat)

withTwzrdBasePreflight and createTwzrdCloudflareBaseApproval apply the same rules to the Base USDC entry, restated in the module so it imports nothing: twzrd_unidentifiable_payment_recipient, amount_field_conflict, amount_malformed, twzrd_non_usdc_asset, invalid_base_offer (before intel; failOpen never signs these), then twzrd_decision_block, twzrd_wash_flagged, twzrd_unevaluated_subject_*, twzrd_unevaluated_no_cap_*, twzrd_unevaluated_over_cap_*, twzrd_score_*_below_* and twzrd_over_recommended_cap_*. The price comes from the entry's own amount; the priceUsdc option is ignored for the decision. Before 0.11.2 this seat signed on any non-block verdict.

createBuyerFetch

Thrown as BuyerFetchBlockedError with reason; message [twzrd-buyer-fetch] <reason>. Refusals are flushed to the decision file before the error is thrown, except use_buyer_fetch_for_payment and decision_log_unavailable. Rows are in the order the checks run. The buyer approval above runs between hourly_budget_exceeded and invalid_preflight_decision, and its twzrd_* reasons also refuse here.

Reason When
presigned_payment_not_allowed The caller's request already carries a payment header.
use_buyer_fetch_for_payment The client's hook ran for a payment not made through the returned fetch.
amount_field_conflict, payto_field_conflict, amount_malformed As in the client hook table.
recipient_missing The selected offer names no recipient.
unauthorized_recipient payTo is not in allowedRecipients (an empty list refuses all).
unsupported_or_non_usdc_asset The asset is not USDC on that network.
amount_missing_or_malformed The amount is not a base-unit integer.
price_cap_exceeded The amount is above maxPricePerCall.
amount_out_of_range The amount is above 2^53-1 micro-USDC.
expected_amount_unavailable expectedAmount is a function that returned no decimal string or threw.
expected_amount_mismatch The amount is not exactly expectedAmount.
hourly_budget_exceeded The payment would take spend in the last hour above hourlyBudgetCap.
invalid_preflight_decision The preflight card's decision is neither allow nor warn.
request_aborted The request's signal aborted before signing.
decision_log_unavailable The decision file could not be written and flushed.
spend_ledger_unavailable The spend could not be recorded before signing.
payment_without_approval A payment header reached the transport without this wrapper's approval.
core_spend_controls The x402 client's own spend controls refused before this wrapper's hook ran.
payment_client_refused The x402 client failed after a 402 for another reason.
request_failed The request failed before any 402 (recorded as an error row, not a block).
createGuardedX402Fetch local caps

Abort reason [twzrd-guarded-fetch] <reason>; these run before the buyer approval above.

Reason When
price_cap_exceeded The price is above maxPricePerCall.
hourly_budget_exceeded The payment would take spend in the last hour above hourlyBudgetCap.
amount_field_conflict, payto_field_conflict As in the client hook table.
recipient_missing allowedRecipients is set and the requirement names no recipient.
unauthorized_recipient allowedRecipients is set and payTo is not in it.
amount_missing_or_malformed A spend rule is set and the amount is not a base-unit integer.
unsupported_or_non_usdc_asset A spend rule is set and the asset is not USDC on that network.
twzrd.safeFetch verdicts

{ verdict: "block", reason }:

Reason When
unparseable_402 The 402 has no readable payment requirements.
network_not_allowed No offer is on an allowNetworks network.
amount_field_conflict, payto_field_conflict As above.
no_payable_requirement No offer names both a recipient and an amount.
malformed_amount The amount is not a base-unit integer.
non_usdc_asset On Solana or Base, the offer names an asset that is not USDC (the budget counts micro-USDC).
over_max_spend, over_cumulative_spend The payment, or the running total, would pass maxSpend.
intel_block The injected preflight answered block.
decision_bind_required A signed decision is required and none was bound.
bind_required_no_compose, bind_required_no_settlement, bind_required_no_leaf_hash, bind_mismatch A resource bind was required and could not be built or did not match.

Config

Option Env Default Description
intelBase TWZRD_INTEL_BASE https://intel.twzrd.xyz Preflight API base
preflightMinScore TWZRD_PREFLIGHT_MIN_SCORE 40 Block below this score
blockDecisions TWZRD_BLOCK_DECISIONS block Decisions that throw
failOpen (buyer preflight only) TWZRD_FAIL_OPEN false Buyer outage only: default does not sign. createTwzrdSettleGuard omitted failOpen is the other default — a thrown screen returns without abort
gateOnCanSpend TWZRD_GATE_ON_CAN_SPEND false Also block when can_spend=false
refuseUnevaluated TWZRD_REFUSE_UNEVALUATED false Refuse every seller intel has not evaluated, instead of allowing it up to the card's recommended_cap_usdc. true, 1, "yes", "on" in any case turn it on.
refuseWashUnknown TWZRD_REFUSE_WASH_UNKNOWN false (0.12.x; true from 0.13.0) Refuse (twzrd_wash_unknown) when the merchant card is reachable but its wash coverage is missing, partial or stale. Also refuses an unevaluated seller within its cap when its card has no full coverage. true, 1, "yes", "on" turn it on.
intelTimeoutMs TWZRD_INTEL_TIMEOUT_MS 2000 Deadline for each free intel call (preflight, merchant card). A miss is an outage, decided by failOpen. Paid hops are not cut off mid-payment.
autoReceipt — false Optional V7: auto-buy $0.05 /trust when /quick did not already settle
x402Fetch — — x402-capable fetch for autoReceipt
onReceipt — — Callback after receipt is captured
disabled (installTwzrdAutoGate only) TWZRD_AUTO_GATE=0/false false Bypass the guard entirely — payWrap gets the raw, unguarded fetch
attribution TWZRD_ATTRIBUTION_INTEGRATION + TWZRD_ATTRIBUTION_RUN_ID — Opt-in run attribution (see below)

Gate adoption proof (no-spend harness)

Deterministic install→transcript path for operators (no wallet, no USDC):

npm run adoption-proof -- --integration demo-autogate-proof --run-id 00000000-0000-4000-8000-000000000001

Emits twzrd.gate_adoption_transcript.v1 JSON: block path aborts with signerInvocations: 0, allow path emits decision, attribution headers stamped on mocked preflight. Full acceptance criteria (what counts as EXTERNAL_RUN vs dogfood), and the harness's own limits: docs/strategy/gate-adoption-operator-proof.md in this repo — the same path the transcript's acceptanceDoc field cites.

Portable decision receipt: twzrd.payment_decision.v1

A frozen, closed public record of one decision — what the agent saw (challenge_hash), what it decided (allow | block | warn | unavailable), why (one code from a closed enum) and an evidence_id — signed with your existing decision signer and verifiable offline by anyone holding your public key. No TWZRD call in the verification path. unavailable is a first-class decision and is never written as block. The record carries no score, secret, raw authorization/payload, amount or resource URL; the verifier rejects any of them.

import { createLocalDecisionSigner, evaluateIntent } from "twzrd-x402-gate";
import { paymentDecisionRecordFromToken, verifyPaymentDecisionRecord } from "twzrd-x402-gate/payment-decision";

const signer = createLocalDecisionSigner({ keyId: "ops-2026-09" });
const token = await evaluateIntent(intent, { signer, policy });
const record = await paymentDecisionRecordFromToken(token, selectedAccepts, signer);

// relying party, anywhere, later:
const r = verifyPaymentDecisionRecord(record, { publicKeyPem, challenge: selectedAccepts });
r.ok && r.decision; // "block" — or null when rejected
npx twzrd-payment-decision --verify record.json --pubkey issuer.spki.pem [--challenge accepts-entry.json] [--json]

This record is the $0.001 clearance SKU (requirePaymentDecision on the before-payment hook). The hook will not proceed to sign without an unexpired allow or warn for this exact challenge_hash. Opt-in — default AutoGate stays free-refuse. GET /v1/intel/quick/{seller} is a $0.001 score teaser and is not this SKU (not challenge-bound). Intel does not issue the record over x402 yet; pass a verified record or an explicit issuerUrl.

Spec (schema, challenge normalization, what is signed, what is forbidden): docs/payment-decision-v1-spec.md · JSON Schema: docs/schemas/twzrd.payment_decision.v1.schema.json · test vectors: test/fixtures/payment-decision-v1-vectors.json.

Run attribution (optional, for integration correlation)

When you set attribution, the gate stamps only the TWZRD preflight request (never the paid /v1/intel/trust call or the resource fetch) with correlation headers:

X-TWZRD-Integration: <integration>
X-TWZRD-Run-Id:      <runId>
X-TWZRD-Client:      twzrd-x402-gate/<version>
installTwzrdAutoGate((guarded) => wrapFetchWithPayment(guarded, wallet), {
  attribution: {
    integration: "payai-x402-solana-pr38",
    runId: crypto.randomUUID(), // echo this in your transcript / issue comment
  },
});

This is correlation evidence, not proof of adoption — the runId is caller-supplied and spoofable. A run counts as an external execution only when the same runId (1) appears in the integrator's own transcript, (2) is observed server-side with a real policy decision, and (3) comes from non-internal lineage. No PII, secret, wallet, or payload is added; both fields must be set or nothing is stamped.

Compatibility note

Proxied x402 clients (AgentCash's .fetch, ClawRouter :8402): these clients handle 402 internally and return 200. The guard never sees a 402 if it wraps the client's output — it must wrap the client's input. installTwzrdAutoGate enforces this composition order by construction: it guards the raw fetch first, then hands the guarded fetch to your payWrap. If you're composing withTwzrdGuard manually instead, pass the raw (non-paying) fetch to withTwzrdGuard, then wrap its output in your x402 client — never the reverse. Or call evaluate_x402_resource explicitly before routing through the proxy.

Why pre-spend, not post-pay

GET /v1/intel/trust/{wallet} is the paid ($0.05 USDC) deep-intel surface — not a gate. POST /v1/intel/preflight is the free ReadinessCard for the pre-spend decision. The gate path only ever calls free endpoints — the preflight plus, by default (refuseWashFlagged: true), the free merchant_card wash check. Paid intel (quickCheck, autoReceipt) is otherwise opt-in, with one documented exception (QUICKSTART 3b): on the fetch / payWrap seat, wiring a paying fetch auto-enables buyer Path A, so a proceeding warn settles the $0.001 quick tier (GET /v1/intel/quick/{payTo}) through your wallet with no further flag — opt out with escalateOnWarn: false, requireReceipt: false, or by leaving x402Fetch unwired. Optional V7 /trust at $0.05 stays on material allow or explicit autoReceipt. The merchant-facing payment still waits on the free preflight's verdict.

License

MIT

Keywords