npm.io
0.11.2 • Published 13h agoCLI

twzrd-x402-gate

Licence
MIT
Version
0.11.2
Deps
0
Size
1.2 MB
Vulns
0
Weekly
0

twzrd-x402-gate

Don't let your agent sign blind. TWZRD is the spend-control SDK for agents paying over x402 — and the default onBeforePaymentCreation policy engine for official x402 clients.

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.

One 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.11.2 @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

Intercept proof (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.11.2
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.

Demonstrable refuse mechanism (free, fail-open, 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.11.2
# 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.8.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.

Foreign-wallet block proof (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 install twzrd-x402-gate@0.11.2

Do not hardcode a version in this doc — every past pin here (0.5.4, 0.7.1, 0.8.5, 0.8.6) has gone stale. Check npm view twzrd-x402-gate version if in doubt.

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 earn on 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. Reads the Solana-network entry from accepts[] (falls back to first entry) to get the seller wallet.
  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 (only tightens step 2). A reachable card with no wash signal fails open (no invent). A card outage on the scored path honours failOpen (default fail-closed).
  4. Otherwise returns the 402 to payWrap's client, which pays normally.

Non-402 responses pass through unchanged.

One-line x402 fetch with local spend limits

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.

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.11.2 is not uniformly fail-closed.

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). refused only with unsupportedNetworkMode: "strict"; the default observe allows
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_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.

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.
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]

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