npm.io
0.9.4 • Published yesterdayCLI

twzrd-x402-gate

Licence
MIT
Version
0.9.4
Deps
0
Size
819 kB
Vulns
0
Weekly
0
Stars
1

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 with ERR_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 onPaymentVerifiedtoPayaiVerifyResult 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-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.

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=true restores 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_confidence other than full, ring_evaluated: false, or wash_stale: true is unknown ≠ clean: refuse (or configured cap). Reason twzrd_wash_unknown / twzrd_wash_unknown_capped_* — never twzrd_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 + 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_*.
  • Adequately measured no-signal (wash_flagged=false and wash_confidence=full, with no stale or unevaluated-ring flag) → keep prior allow (twzrd_wash_ok on 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-dogfoodexamples/wash-refuse-dogfood.ts
  • Official client + Path E hook (live Solana ≤$0.001): npm run official-dogfoodexamples/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.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
  • 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 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.

  • 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/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:

  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; fail-open if the card is unreachable — no invent).
  4. 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:

  1. Reads the Solana-network entry from accepts[] (falls back to first entry) to get the seller wallet.
  2. Free POST /v1/intel/preflightdecision=block / score floor deny.
  3. Free GET /v1/intel/merchant_card/{payTo}wash_flagged:true refuses by default (reason: twzrd_wash_flagged). Fail-open if the card is unreachable (no invent).
  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.

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:

  1. decision ∈ blockDecisions (default: ["block"])
  2. trust_score < preflightMinScore (default: 40) — note a card with no trust_score is scored 0 (policy.ts: card.trust_score ?? 0), so a missing score blocks under the default floor
  3. can_spend === falseonly when gateOnCanSpend: true (default false, opt-in)
  4. merchant_card.wash_flagged === trueon by default (refuseWashFlagged, TWZRD_REFUSE_WASH_FLAGGED); forces verdict: "block" with reason: twzrd_wash_flagged. A returned card with wash_flagged: null, missing/base_2cycle coverage, or ring_evaluated: false is unknown ≠ clean (twzrd_wash_unknown). Unreachable card still fail-opens (no invent). Soft-cap instead of hard refuse with washMaxUsdc / 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_spend note: the free preflight returns can_spend=false for most sellers not yet in the TWZRD corpus, including legitimate ones. That can_spend leg alone is opt-in — it is ignored unless you set gateOnCanSpend: 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_score evaluates as 0 and blocks under the default preflightMinScore: 40 with twzrd_score_0_below_40. To actually let unscored sellers through, lower or disable preflightMinScore — setting gateOnCanSpend does 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.

Keywords