twzrd-x402-gate
Product: the paying client that already has the brake.
Agents pay other machines unsupervised. Most 402s are junk or wash. Something has
to say no before the signature. TWZRD is that no — inside beforePayment /
fetch+sign, not inside PayAI’s SDK defaults, not as a shop at intel.twzrd.xyz.
The other reason the no must live at the signature: attaching a funded wallet to an agent that reads untrusted web content is a drain vector — indirect prompt injection can spend anything the agent can sign. The gate closes it by attaching the wallet to a policy runtime the injected content cannot renegotiate: per-request cap, rolling daily ceiling, wash brake, endpoint mandates, and a tamper-evident spend ledger that survives restarts. A fully compromised agent spends at most what policy allows, to whom it allows, with every decision signed.
Replace naked createX402Client({ wallet }) (5 lines)
npm install twzrd-x402-gate@0.9.3 x402-solana@3.0.0
import { createX402Client } from "x402-solana";
import { createTwzrdPayingClient } from "twzrd-x402-gate";
// was: createX402Client({ wallet })
const client = createX402Client(createTwzrdPayingClient({ wallet }));
Equivalent one-hook form:
import { createX402Client } from "x402-solana";
import { createTwzrdBeforePaymentHook } from "twzrd-x402-gate";
const client = createX402Client({
wallet,
network: "solana",
beforePayment: createTwzrdBeforePaymentHook(), // wash default
});
Default path (engine "wash"):GET /v1/intel/merchant_card/{payTo} → abort iff wash_flagged === true →
fail-open on timeout / non-2xx / throw (never invent wash).
No Path A, no requireReceipt, no second 402, no payment-control tokens.
Full 0.8.x engine (named opt-in): preflight + optional Path A / escalate / paymentControl:
createTwzrdBeforePaymentHook({ engine: "full", /* … */ })
// or createTwzrdFullBeforePaymentHook({ … })
Customer = whoever ships createX402Client({ wallet }) with no hook (Eliza, MCP
hosts, agent wallets). Crawlers will never npm install.
ESM-only: CommonJS
require()fails withERR_PACKAGE_PATH_NOT_EXPORTED— set"type": "module"(or use.mjs/ an ESM bundler).
Default-on AutoGate (alternate seats)
npm install twzrd-x402-gate@0.9.3 x402-solana@3.0.0
# official @x402/* path (alternate seat):
# npm install twzrd-x402-gate@0.9.3 @x402/core @x402/fetch @x402/svm
import { x402Client } from "@x402/core/client";
import { installTwzrdAutoGate } from "twzrd-x402-gate";
const client = new x402Client();
// stock solana seat still defaults to wash via createTwzrdBeforePaymentHook()
installTwzrdAutoGate(client, { refuseWashFlagged: true });
// then register schemes + wrapFetchWithPayment as usual
Intercept proof (0 USDC, bad seller never reaches signer):
cd packages/twzrd-x402-gate && npm run autogate-block-proof
# writes block-proof-<run_id>.json (schema twzrd.autogate_block_proof.v1)
# public reason: "TWZRD_TRUST_GATE_BLOCK: wash_flagged" (wash basis)
# or "TWZRD_TRUST_GATE_BLOCK: decision_block" (readiness block basis)
# fixture resolves live: wash_flagged seller if available, else decision=block
gateOnCanSpend remains opt-in (false by default; set true or TWZRD_GATE_ON_CAN_SPEND=1 only when you want hard cap enforcement). Full-engine preflight outage default stays fail-closed unless failOpen: true / TWZRD_FAIL_OPEN=1. The product wash path always fail-opens on intel outage.
Optional: 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
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.9.3
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 |
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 x402-solana@2.1.0+ (stock client - primary Solana seat via beforePayment).
Both 2.1.0 and 3.0.0 are supported and produce an identical refuse transcript; 3.0.0
is the current release and what the install lines pin. The hook absorbs the one
breaking change between them (declaredResource is a string on 2.1.0, { url } on
3.0.0) - see src/x402-client-hook.ts and test/x402-solana-before-payment.test.ts.
import { createX402Client } from "x402-solana";
import { createTwzrdPayingClient } from "twzrd-x402-gate";
const client = createX402Client(createTwzrdPayingClient({ wallet }));
Also: installTwzrdAutoGate("x402-solana", opts) returns the same wash-default hook.
Prove refuse-before-sign: npm run x402-solana-before-payment-proof.
PayAI agentic-payments (alternate PayAI surface; prefer x402-solana@3.0.0 when available):
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.9.3
# 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.9.1 && npm run wash-dogfoodor 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 footprintfound: false) - Stack: official
@x402/coreclient +@x402/fetch+@x402/svmExactSvmScheme, TWZRD viainstallTwzrdX402ClientHookatonBeforePaymentCreation - 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.
MCP (@x402/mcp)
Wire twzrdOnPaymentRequested / prefer onPaymentRequired + onBeforePayment per
lifecycle hooks. Same policy core.
Cloudflare Agents x402 (Base)
Cloudflare's withX402Client takes an onPaymentRequired callback before its
automatic payment retry. Use the adapter below at that boundary:
import { createTwzrdCloudflareX402Approval } from "twzrd-x402-gate";
const approvePayment = createTwzrdCloudflareX402Approval({
// Base/EVM has no TWZRD behavioral reputation yet. Refuse it rather than
// presenting a policy allow as a trust verdict.
unsupportedNetworkMode: "strict",
});
await this.x402Client.callTool(approvePayment, {
name: "paid_tool",
arguments: {},
});
observe is available for an agent that intentionally permits unscored Base
payments, but it returns policy allow with decision=unknown; it is never a
TWZRD reputation approval. This adapter protects the Cloudflare x402 path only.
MPP is a separate protocol and belongs on its own onChallenge /
onPaymentRequired control path.
Cloudflare Worker / Base x402 (edge-safe preflight)
For a Worker, import the dedicated subpath rather than the package root. It has
no Node-native imports and sends the exact Base payTo plus chain_id: 8453 to
/v1/intel/preflight before a Viem account signs:
import { withTwzrdBasePreflight } from "twzrd-x402-gate/cloudflare-base";
const signature = await withTwzrdBasePreflight(
paymentRequirements, // accepts: [{ network: "eip155:8453", payTo: "0x..." }]
{ intelBase: "https://intel.twzrd.xyz" },
() => account.signTypedData(eip3009Authorization),
);
block throws TwzrdBasePaymentBlockedError and never invokes the signing
callback. allow and warn both proceed; enforce any additional amount or
mandate policy in the caller. Base is not presented as reputation-scored.
Checksummed and lowercase EVM addresses are accepted unchanged. The fixture in
test/cloudflare-base-edge.test.ts proves
block → zero signTypedData calls and the compiled subpath is checked for Node
runtime leaks.
Raw-fetch composition (injectible pay client only)
import { installTwzrdAutoGate } from "twzrd-x402-gate";
import { wrapFetchWithPayment } from "@x402/fetch"; // or @x402/svm helper
// 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 (trustless, fail-open) — 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 | Optional paid trust | $0.05 / $0.001 | On warn or high-value: GET /v1/intel/trust/{payTo} or quickCheck. Never required for the free refuse path. |
| 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 in gate 0.2+ (
TWZRD_FAIL_OPEN=truerestores legacy allow-on-outage). - Merchant card unreachable / non-2xx / timeout → fail-open (do not invent wash).
- A returned card with
wash_flagged: null, missing coverage,wash_confidenceother thanfull,ring_evaluated: false, orwash_stale: trueis unknown ≠ clean: refuse (or configured cap). Reasontwzrd_wash_unknown/twzrd_wash_unknown_capped_*— nevertwzrd_wash_ok.
Wash policy (exact):
- Prior preflight deny → unchanged (wash never loosens a block).
refuseWashFlagged=false→ keep prior approval (opt out).wash_flagged=true+ no cap →approved=false,reason=twzrd_wash_flagged,verdict=block.wash_flagged=true+washMaxUsdcset +priceUsdc <= cap→ allow,washCapped=true, reasontwzrd_wash_capped_{price}_le_{cap}.wash_flagged=true+ price above cap (or price unknown) → refuse withtwzrd_wash_flagged_above_cap_*.- Adequately measured no-signal (
wash_flagged=falseandwash_confidence=full, with no stale or unevaluated-ring flag) → keep prior allow (twzrd_wash_okon the default hook).
Order note: onWarnUpsell (points at paid /trust) 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.
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/baseand a funded Solana key (SVM_KEYPAIR_PATHor~/.agentcash/solana-wallet.json).--blockexercises hardgateOnCanSpendabort with $0 spend. - Multi-hook composition (amount cap then TWZRD, abort short-circuit) is proven offline against the real
x402Clientintest/x402-official-compat.test.ts. - Release identity:
CLIENT_VERSIONis read frompackage.json(single source of truth);npm testincludesversion-identity;npm run pack-smokepacks the tarball and checks the installed header.
Install
npm install twzrd-x402-gate@0.9.3 x402-solana@3.0.0
Install pin is the published version (GATE_PACKAGE_PIN in
packages/twzrd-agent-intel/src/twzrd_agent_intel/pins.py). Repo package.json
may be one patch ahead until that release is on npm. Confirm with
npm view twzrd-x402-gate version.
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 INTENT_HASH_MISMATCH | DECISION_EXPIRED | DECISION_NOT_ALLOW |
// DECISION_REPLAYED | BAD_SIGNATURE -> the signer is never invoked
PaymentIntentv1 (frozen): protocolx402 | ap2 | ucp | mpp | direct+ network/asset/amount/payTo + resource + facilitator + mandate + recurrence context, bound into one canonicaltiv1: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 mutatespayToafter 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
paymentControlblock aborts even when the legacy preflight allowed; it never loosens a legacy denial. - Opt-in: with
paymentControlunset the hook behaves exactly as before. - x402 wire amounts (USDC micro units) are converted to the decimal USD the
runtime expects by
x402RequirementsToIntent(decimalsdefaults 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 and narrow Base EVM charge
createTwzrdMppOnChallenge guards Mppx.create({ onChallenge }). It supports
solana/charge and the published native MPP evm/charge shape only for
Base-mainnet native USDC (EIP-3009 authorization). onChallenge is the last
deterministic checkpoint before createCredential() makes the credential, 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; other methods/intents fail closed
(allowUnevaluated: true is an explicit ungated 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" },
}),
});
For Base, use mppx's EVM client with explicit local spend policy:
import { Mppx } from "mppx/client";
import { evm } from "mppx/evm/client";
import { createTwzrdMppOnChallenge, createLocalDecisionSigner } from "twzrd-x402-gate";
const mppx = Mppx.create({
methods: [evm({ account: wallet })],
onChallenge: createTwzrdMppOnChallenge({
signer: createLocalDecisionSigner(),
policy: {
allowedNetworks: ["eip155:8453"],
allowedAssets: ["0x833589fCD6EDb6E08f4c7C32D4f71b54bdA02913"],
maxAmountUsd: "1.00",
},
}),
});
Base is not reputation-scored. The EVM path provides an actual pre-credential local control point for Base USDC; it does not call a Solana-derived score or present a local policy allow as a TWZRD trust verdict.
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. |
| Non-Base EVM chain | UNSUPPORTED_EVM_NETWORK |
EVM MPP is narrow: Base mainnet only. A testnet or another EVM network cannot inherit a Base USDC valuation. |
| EVM split recipients | MULTI_LEG_CHARGE |
mppx EVM permits splits; each is an additional recipient. PaymentIntent v1 binds one amount to one payTo, so the guard refuses rather than approve transfers it cannot represent. |
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.
mppChallengeToIntentbinds the challenge id + realm into the intent (resource.operation), so the signed decision covers this exact challenge.- Cluster names stay honest:
solana:devnetclassifies 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:
installTwzrdAutoGateover raw fetch + injectible pay client, ortwzrdOnPaymentRequested(MCP). Do not wrap AgentCash's paying fetch withwithTwzrdGuard.
# 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/EVM: explicit
decision=unknown(see Networks).
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, three adapters:
| Call | Adapter |
|---|---|
installTwzrdAutoGate(payWrap, opts?) |
Fetch: guard raw fetch → pay client |
installTwzrdAutoGate(x402Client, opts?) |
Official @x402/core onBeforePaymentCreation |
installTwzrdAutoGate("x402-solana", opts?) |
PayAI stock client beforePayment (2.1.0+) |
installTwzrdAutoGate("mpp", opts) |
MPP onChallenge (returns handler) |
createTwzrdBeforePaymentHook(opts?) |
Same as "x402-solana" — pass to createX402Client |
Aliases: installTwzrdX402ClientHook, createTwzrdMppOnChallenge remain; docs prefer AutoGate.
Kill switch: TWZRD_GATE_ENABLED=false or TWZRD_AUTO_GATE=0. Uninstall x402 installs with uninstallTwzrdAutoGate(client).
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/svm";
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.).
Default ON. Disable with TWZRD_AUTO_GATE=0 (env, deploy-time kill switch) or
{ disabled: true } (per-call, e.g. in tests) — the raw fetch is handed straight to
payWrap, unguarded.
What happens on every HTTP 402 the raw fetch returns:
- Reads the Solana-network entry from
accepts[](falls back to first entry) to get the seller wallet. - Calls
POST /v1/intel/preflight— free, no auth.decision=block(or score floor) throws —payWrap's client never signs. - Calls
GET /v1/intel/merchant_card/{payTo}— free, no auth.wash_flagged: truerefuses by default (only tightens step 2; fail-open if the card is unreachable — no invent). - Otherwise returns the 402 to
payWrap's client, which pays normally.
Non-402 responses pass through unchanged.
Networks (Solana-deep, chain-neutral envelope)
The gate recognizes multi-chain 402s but only reputation-scores Solana mainnet.
| Network | Reputation scored? | Default policy (unsupportedNetworkMode) |
|---|---|---|
| Solana mainnet | Yes — free preflight + merchant_card | allow/block from intel |
Base / other EVM (eip155:*) |
No | observe (default): decision=unknown, policyAction=allow, telemetry unsupported_network_seen |
Base / EVM in strict mode |
No | policyAction=block before sign |
This is intentional: Base listing abundance ≠ Solana behavioral history. Unsupported is never
represented as a TWZRD trust allow. Set TWZRD_UNSUPPORTED_NETWORK_MODE=strict (or
{ unsupportedNetworkMode: "strict" }) to hard-block unscored networks.
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/svm";
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:
- Reads the Solana-network entry from
accepts[](falls back to first entry) to get the seller wallet. - Free
POST /v1/intel/preflight—decision=block/ score floor deny. - Free
GET /v1/intel/merchant_card/{payTo}—wash_flagged:truerefuses by default (reason: twzrd_wash_flagged). Fail-open if the card is unreachable (no invent). - 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.
Auto-receipt on warn (revenue path)
const safeFetch = withTwzrdGuard(x402Fetch, {
autoReceipt: true, // on warn or allow, auto-buy the $0.05 TWZRD trust receipt
x402Fetch, // the paying fetch — TWZRD earns the fee on-chain
onReceipt: (receipt, tx) => {
// receipt is a twzrd_receipt (V6 + ERC-8004 reputation_credential)
console.log("Trust receipt captured:", tx);
},
});
Path A — default on the buyer seat when a paying fetch is wired
Free preflight still decides allow|warn|block. Facilitator onBeforeSettle
stays free (abort on block only — we do not tax their rail).
On the buyer install, if you pass x402Fetch (or use
installTwzrdAutoGate(payWrap), which auto-wires payWrap(raw)), Path A
defaults on:
| Free decision | Resource price | What fires |
|---|---|---|
block |
any | free refuse |
warn |
≥ $2.50 | $0.05 V6 (requireReceipt) |
warn |
< $2.50 | $0.001 quick re-decide (escalateOnWarn) |
allow |
> $2.50 | $0.05 V6 |
allow |
≤ $2.50 | free proceed |
import { wrapFetchWithPayment } from "@x402/svm";
import { createTwzrdBeforePaymentHook } from "twzrd-x402-gate";
const x402Fetch = wrapFetchWithPayment(fetch, buyerWallet);
const client = createX402Client({
wallet: buyerWallet,
network: "solana",
beforePayment: createTwzrdBeforePaymentHook({
refuseWashFlagged: true,
x402Fetch, // turns Path A defaults on
}),
});
Opt out: requireReceipt: false and/or escalateOnWarn: false.
Refuse-only seats (no x402Fetch) stay free.
evaluate_x402_resource itself stays opt-in. autoReceipt: true still buys
$0.05 on every non-block (broader than the default ladder).
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/svm";
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 V6
receipt). When the free preflight is inconclusive (warn / unknown seller) 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)
The free preflight leaves an unknown/uncertain seller at warn, which proceeds by
default. escalateOnWarn closes the loop autonomously: on a proceeding warn, the guard
settles the cheap $0.001 quick tier and re-decides on the paid score — below the
floor the payment is blocked, at/above it proceeds. 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 (V6 + 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:
decision ∈ blockDecisions(default:["block"])trust_score < preflightMinScore(default:40) — note a card with notrust_scoreis scored0(policy.ts:card.trust_score ?? 0), so a missing score blocks under the default floorcan_spend === false— only whengateOnCanSpend: true(defaultfalse, opt-in)merchant_card.wash_flagged === true— on by default (refuseWashFlagged,TWZRD_REFUSE_WASH_FLAGGED); forcesverdict: "block"withreason: twzrd_wash_flagged. A returned card withwash_flagged: null, missing/base_2cyclecoverage, orring_evaluated: falseis unknown ≠ clean (twzrd_wash_unknown). Unreachable card still fail-opens (no invent). Soft-cap instead of hard refuse withwashMaxUsdc/TWZRD_WASH_MAX_USDC.
warn is allowed unless overridden. Preflight network failure fails closed by default (a preflight outage blocks the payment, so an intel hiccup never silently approves a spend); set failOpen: true / TWZRD_FAIL_OPEN=true to opt into legacy allow-on-outage.
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. This is unconditional (not affected by failOpen): failOpen governs what happens when the TWZRD service is unreachable, not what happens when the caller can't say who they're paying.
can_spendnote: the free preflight returnscan_spend=falsefor most sellers not yet in the TWZRD corpus, including legitimate ones. Thatcan_spendleg alone is opt-in — it is ignored unless you setgateOnCanSpend: true— so an unknown seller on a platform like Agentic.Market is not blocked on that basis.This is not a blanket "unknown sellers are allowed by default". Conditions 1, 2 and 4 above are all active by default, and condition 2 is the one that catches unknown sellers: a card carrying no
trust_scoreevaluates as0and blocks under the defaultpreflightMinScore: 40withtwzrd_score_0_below_40. To actually let unscored sellers through, lower or disablepreflightMinScore— settinggateOnCanSpenddoes not govern it.
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 |
TWZRD_FAIL_OPEN |
false |
true opts into legacy allow-on-outage; default blocks (fail-closed) |
gateOnCanSpend |
TWZRD_GATE_ON_CAN_SPEND |
false |
Also block when can_spend=false |
refuseWashFlagged |
TWZRD_REFUSE_WASH_FLAGGED |
true |
Refuse when merchant_card.wash_flagged === true (Policy 4). Set false / 0 to opt out |
washMaxUsdc |
TWZRD_WASH_MAX_USDC |
— | Soft cap instead of hard refuse on wash: allow up to this USDC amount |
unsupportedNetworkMode |
TWZRD_UNSUPPORTED_NETWORK_MODE |
observe |
Behavior on a non-Solana / unrecognized network |
fetch |
— | global fetch |
Injected fetch used for preflight + merchant card (testing / custom agents) |
onWarnUpsell |
— | — | Callback fired on a warn verdict (Path A upsell hook) |
attribution |
TWZRD_ATTRIBUTION_INTEGRATION + TWZRD_ATTRIBUTION_RUN_ID |
— | Opt-in run attribution (see below) |
The four options below are not TwzrdGateConfig fields — they belong to
TwzrdGuardOptions / InstallAutoGateOptions (withTwzrdGuard /
installTwzrdAutoGate). Passing them to createTwzrdGate({...}) is a type error
and is silently dropped at runtime:
| Option | Env | Default | Description |
|---|---|---|---|
autoReceipt |
— | false |
Auto-buy $0.05 TWZRD receipt on warn/allow |
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 |
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): monorepo docs/strategy/gate-adoption-operator-proof.md.
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 opt-in and never runs implicitly; you decide
whether to proceed before any USDC leaves your wallet.
License
MIT
Pay Kit before-payment hook (upstream PR pending)
solana-foundation/pay-kit#303 is open as of 2026-09-07. This adapter targets
that PR's onBeforeX402PaymentCreation option; a released Pay Kit client must
expose that option before this integration can enforce policy.
import { createPayKitClient } from "@solana/pay-kit";
import { installTwzrdAutoGate } from "twzrd-x402-gate";
const client = await createPayKitClient({
accept: ["x402"],
rpcUrl,
signer,
onBeforeX402PaymentCreation: installTwzrdAutoGate("pay-kit", {
refuseWashFlagged: true,
}),
});
The adapter returns the official x402 context hook and reuses the shared before-payment evaluator. It adds no Pay Kit dependency. Supply it at client construction; passing the constructed Pay Kit client to AutoGate is unsupported. This hook covers x402 payment creation, not Pay Kit's MPP path.
createTwzrdPayKitBeforePaymentHook(options) exposes the same evaluator directly.
The AutoGate form reads environment kill switches on every call; the direct
factory does not. Calling AutoGate installs protection by default, whereas
ClawRouter's integration loads AutoGate only after explicit opt-in.
Source-checkout verification: npx tsx test/pay-kit-before-payment.test.ts.
The test covers a fake Pay Kit host and the real x402 client with a fake scheme;
it does not establish released Pay Kit compatibility or external use.