twzrd-x402-gate
Wrap your agent's paying fetch. Refuse before its wallet signs when a quote is over your cap, its recipient is not allowed, or the free preflight blocks or cannot answer. Keep the reason in a local JSONL file.
Which entry point?
| You are… | Use |
|---|---|
| Starting a new integration | createBuyerFetch (since 0.12.3; optional expectedAmount since 0.12.4) |
Already using an official x402Client |
installTwzrdAutoGate(client) |
Using PayKit (createPayKitClient) |
installTwzrdAutoGate("pay-kit") |
Using PayAI createX402Client({ beforePayment }) |
installTwzrdAutoGate("x402-solana") |
Using MPP (Mppx.create({ onChallenge })) |
installTwzrdAutoGate("mpp") |
Using the @x402/mcp client |
twzrdOnPaymentRequested |
| Keeping an older integration | createGuardedX402Fetch, withTwzrdGuard, wrapFetchWithTwzrdGate, or twzrd.safeFetch |
installTwzrdAutoGate is one function with those adapters. twzrd.safeFetch is
spendControlSafeFetch. The other export named safeFetch is the advisory CLI
pipeline in src/safe-fetch.ts, not this row.
createBuyerFetch is the front door for a new buyer. createGuardedX402Fetch
is legacy and stays for callers that already use it. They are not the same
wrapper:
- The hourly ledger in
createBuyerFetchis a JSONL file (spendLedgerFile, default.twzrd/spend.jsonl) shared by every fetch that points at that path, including after a restart.createGuardedX402Fetchkeeps its rolling hour in memory for the life of that returned fetch and drops it when the process ends. createBuyerFetchrequiresmaxPricePerCallandallowedRecipients, and it setsunsupportedNetworkMode: "strict". A network the scorer marks unscored is refused. That includes a name containingdevnet,testnet, orsepolia, and Base Sepolia. A Solana devnet named only by its CAIP-2 genesis id is still treated as scored (network.ts) and is not refused by this rule.createGuardedX402Fetchleaves caps and the allowlist optional. Its preflight uses the shared defaultunsupportedNetworkMode: "observe", so those same unscored networks are allowed through without a reputation score. If you did set a spend cap, a non-USDC asset is still refused locally.
Defaults by entry point
Read from the constructors, not from the prose below. "Wash flagged" is
refuseWashFlagged. refuseWashUnknown is off on every row in 0.12.x unless
that call opts in. TWZRD_AUTO_GATE=0 and TWZRD_GATE_ENABLED=0 are a
different switch: only installTwzrdAutoGate reads them, and
createBuyerFetch does not.
| Entry point | Preflight outage | TWZRD_FAIL_OPEN |
Wash flagged | Unscored network |
|---|---|---|---|---|
createBuyerFetch |
fail-closed (failOpen: false is passed in) |
no | on, and TWZRD_REFUSE_WASH_FLAGGED cannot turn it off |
strict: refuse |
installTwzrdAutoGate(client), "pay-kit", "x402-solana" |
fail-closed | yes | on, unless TWZRD_REFUSE_WASH_FLAGGED is 0 or false |
observe: allow, then the wash check still runs |
installTwzrdAutoGate("mpp") |
not this preflight. A thrown evaluation refuses and does not return a credential | not read | not this flag. No wash_flagged check in mpp-hook.ts |
not observe/strict. An unknown cluster is refused; devnet is a known cluster |
twzrdOnPaymentRequested |
fail-closed when called with no config | yes, inside the approval (resolveConfig()) |
on, unless that env opt-out, when no config is passed | observe |
createGuardedX402Fetch |
fail-closed. After the local cap check it calls createTwzrdPayKitBeforePaymentHook, which is evaluateBeforePaymentCreation. That is the same evaluator installTwzrdAutoGate("pay-kit") returns. The client you pass does not have to be PayKit |
yes | on, unless that env opt-out | observe. A set spend cap still refuses a non-USDC asset |
withTwzrdGuard |
fail-closed | yes | on, unless that env opt-out | observe |
wrapFetchWithTwzrdGate() |
fail-closed when you pass no config, or pass resolveConfig() |
yes, unless the config you pass already set failOpen |
same as the config you pass; resolveConfig() is on unless that env opt-out |
observe, unless the config you pass says strict |
twzrd.safeFetch |
none, unless you pass preflight. A throw from that callback propagates |
not read | not applied | no observe/strict mode. Optional allowNetworks can refuse network_not_allowed |
Start here: a blocked payment, no wallet
createBuyerFetch and twzrd-buyer-demo ship from 0.12.3, and expectedAmount from 0.12.4. In an ESM project:
npm install twzrd-x402-gate@0.12.5 @x402/core @x402/fetch
npx twzrd-buyer-demo
Or from a clone of this repository:
npm ci
npm run build
npm run buyer-demo --workspace=twzrd-x402-gate
The demo runs a real @x402/fetch client against a synthetic 402, refuses the
over-cap quote, and writes .twzrd/demo-decisions.jsonl under the directory it
runs in. No keys, network access, or funds are used. Expected fields:
{"mode":"offline_fixture","decision":"block","reason":"price_cap_exceeded","signer_invocations":0,"paid_requests":0,"usdc_spent":0}
To test an unpublished build outside this workspace, pack from twzrd-x402-gate/
with npm pack --ignore-scripts and install the tarball in place of the registry
version.
Put it on your paying call
Pass a dedicated official x402Client with your scheme and signer registered:
import { createBuyerFetch } from "twzrd-x402-gate";
const guardedFetch = createBuyerFetch({
client, // your configured x402Client
maxPricePerCall: "0.01", // exact USDC decimal; required
allowedRecipients: [sellerPayTo],// explicit payTo allowlist; required
hourlyBudgetCap: "1.00", // optional rolling cumulative cap
decisionFile: ".twzrd/decisions.jsonl",
spendLedgerFile: ".twzrd/spend.jsonl",
});
try {
const response = await guardedFetch(resourceUrl); // ordinary fetch signature
} catch (error) {
console.error(error.reason ?? error.message);
}
Caps and recipients are checked against the selected requirement at
onBeforePaymentCreation. Supported payment assets are USDC on scored Solana
mainnet and Base mainnet; unscored networks refuse. EVM recipient comparison
ignores case; Solana comparison preserves case. An empty allowlist refuses all.
Bind the quote: expectedAmount (0.12.4)
maxPricePerCall is a ceiling: with a $1,000 cap, a $1,000 challenge passes it.
To refuse any amount other than the one you were quoted, also pass
expectedAmount:
const guardedFetch = createBuyerFetch({
client, maxPricePerCall: "1.00", allowedRecipients: [sellerPayTo],
expectedAmount: "0.01", // exact USDC decimal the seller quoted
});
The atomic amount the signer would authorize, on the offer the client selected,
must equal it exactly. A lower or higher amount refuses with
expected_amount_mismatch before preflight or signing. Only the selected offer
is checked, so a 402 that lists several offers can refuse even when another
offer matches. The cap still applies and keeps price_cap_exceeded. A fixed
expectedAmount above maxPricePerCall throws at install; values take up to 6
decimal places.
One wrapper can serve several resources with a function that returns the quote
per payment: expectedAmount: ({ resource, payTo, network, signal }) => quotes.get(resource). It never sees the challenged amount. resource is the
normalized request URL (new Request(url).url, with its query), and payTo is
spelled as the challenge spells it, so key lookups the same way. When it returns
no decimal string or throws, the call refuses with expected_amount_unavailable
and the error keeps the cause. An abort while it is pending refuses with
request_aborted.
Added in 0.12.4. 0.12.3 does not have this option: TypeScript rejects it there as an unknown property, and plain JavaScript ignores it without an error, so pin 0.12.4 or later before relying on it.
The free preflight and merchant-card checks run before signing. This entry
point has no fail-open or disable switch: legacy TWZRD_FAIL_OPEN,
TWZRD_AUTO_GATE, and TWZRD_GATE_ENABLED cannot bypass it. A warn can proceed
within local caps and the preflight's policy. A check that cannot answer refuses.
Decisions are flushed locally before signing; no decision-upload request is
made. Rows retain recipient, network, atomic amount and resource origin,
not URL paths/queries, credentials, signatures or response bodies. The
signer_invocations: 0 field describes the decision's pre-sign point, including
an allow row; it does not claim an allowed call stayed unsigned or settled.
Free preflight still sends the requested seller, resource URL and price to intel.
The spend ledger counts authorized signing attempts, including signer errors and uncertain payment outcomes. It does not claim confirmed settlement or refunds. A supplied hourly budget survives process restarts. Pending calls share reservations within one process. Use separate files per process or serialize externally; these ledgers are not a multi-process budget service. Local write failures refuse signing. HTTP redirects and pre-signed input headers are refused. Configure the client before wrapping it, use the returned fetch for payments, and do not add bypass/recovery hooks or mutate its signer path afterward. Core's own spend controls can impose additional restrictions.
No paid score or receipt is bought by this wrapper. /quick (0.001 USDC) is an
optional unsigned Solana seller score; /trust (0.05 USDC) is an optional signed
V7 receipt of that score. Neither proves a GPU or model completed work.
The offline demo proves a local refusal, not outside adoption. First outside use requires evidence from another agent using the installed wrapper on its own payment path.
Other integration APIs
The existing APIs below retain their own defaults and optional switches.
Commerce loop (directory → preflight → AutoGate → verify → evidence bundle):
repo docs/COMMERCE-KIT.md. exportEvidenceBundle /
npx twzrd-evidence-bundle writes twzrd.evidence_bundle.v1. This package is
not Catena's Agent Commerce Kit.
Lower-level call (named export twzrd)
import { twzrd } from "twzrd-x402-gate";
const result = await twzrd.safeFetch(url, {
maxSpend: "0.10", // per-call cap AND cumulative budget
allowNetworks: ["solana", "base"],
requireOfferBinding: true, // compose → verify hard bind → only then pay()
composeBoundTransaction, // build unsigned bound bytes (no keys)
pay, // sign/submit only after bind-v1 hard verify
});
// result.verdict: "allow" | "warn" | "block" — blocks have signerInvocations === 0
// result.receipt: { strength: "hard"|"soft"|"refuse", leaf_hash, fact_type: "resource_bound" }
requireOfferBinding is fail-closed: missing composeBoundTransaction returns
bind_required_no_compose with signerInvocations === 0 (pay is never called).
The gate verifies the composed bytes, then calls pay() with that
transactionBase64. The old post-pay check remains as
verifyOfferBindingAfterPay only. This SDK never holds keys.
Full walkthrough: QUICKSTART.md · verify receipts yourself: REVIEW.md
Core product (buyer gate): after the client selects the exact payment requirement and
before payment payload creation / wallet signing — free preflight + merchant_card wash
refuse. Protects the payer from a risky merchant (payTo). Chain-neutral envelope;
Solana mainnet and Base mainnet (eip155:8453) are scored. Other EVM networks do not run the scored preflight.
Default-on AutoGate (5 lines)
npm install twzrd-x402-gate@0.12.5 @x402/core @x402/fetch @x402/svm
import { x402Client } from "@x402/core/client";
import { installTwzrdAutoGate } from "twzrd-x402-gate";
const client = new x402Client();
// refuseWashFlagged defaults true; gateOnCanSpend stays false unless you opt in
installTwzrdAutoGate(client, { refuseWashFlagged: true });
// then register schemes + wrapFetchWithPayment as usual
Historical intercept proof (2026-07; 0 USDC, wash seller never reaches signer):
cd twzrd-x402-gate && npm run autogate-block-proof # needs network: live intel preflight
# writes block-proof-<run_id>.json (schema twzrd.autogate_block_proof.v1)
# public reason: TWZRD_TRUST_GATE_BLOCK: wash_flagged
gateOnCanSpend remains opt-in (false by default; set true or TWZRD_GATE_ON_CAN_SPEND=1 only when you want hard cap enforcement).
Optional (0.8.1): a resource-server settle hook so merchants can apply customer policy before they settle and serve (abuse, sanctions, bots, “don’t serve this payer”). Not an equal mirror of the buyer problem — settled USDC is final; wash resistance is mainly TWZRD scoring.
Seller settle guard (onBeforeSettle) — optional 0.8.1
Resource servers can screen the payer before they settle an inbound payment and serve
the resource. Use for merchant policy (abuse / sanctions / bots / customer selection). TWZRD is
not in the settlement path: advisory + fail-open by default. Attaches to official
x402ResourceServer.onBeforeSettle (inherited by @x402/express|hono|next|fastify and Python x402).
Do not treat this as “protect merchant reputation by rejecting USDC” — anyone can still transfer on-chain. Wash/sybil edges are primarily discounted in TWZRD scoring, not forced revenue refusal.
npm install twzrd-x402-gate@0.12.5
import { x402ResourceServer } from "@x402/core/server";
import { createTwzrdSettleGuard, twzrdPayerScreen } from "twzrd-x402-gate";
const server = new x402ResourceServer(facilitator);
server.onBeforeSettle(
createTwzrdSettleGuard({ screen: twzrdPayerScreen() }),
);
Defaults (fail-open / advisory):
| Behavior | Default |
|---|---|
| Abort settlement | decision=block or wash_flagged=true |
warn |
allowed (continues) unless you set abortOn.warn |
| Default screen | free GET /v1/intel/merchant_card/{payer} via twzrdPayerScreen() |
| Screen/extract timeout | timeoutMs: 3000 — on timeout, continue (fail-open) |
| Unresolved payer / null screen / screen error | continue unless failOpen: false |
exact-SVM payload, no @x402/svm peer installed |
reports twzrd_svm_peer_missing (warns once) and continues — without the peer, SVM payers are never screened; install it or set failOpen: false |
Paid /v1/intel/trust |
not default — inject a custom screen if you want it |
Payer identity prefers signed/encoded scheme fields (EIP-3009 authorization.from, Permit2
permit2Authorization.from, exact-SVM payload.transaction via optional peer @x402/svm)
over client-supplied loose aliases — so a spoofed payload.payer cannot bypass screening.
Offline demo: npx tsx examples/seller-settle-guard.ts
Fixture-backed SVM extract tests live in test/seller-hook.test.ts +
test/fixtures/exact-svm-transfer-checked.ts.
PayAI agentic-payments (the active PayAI SDK, not the dormant x402-solana):
npx tsx examples/payai-agentic-onPaymentVerified.ts — wire onPaymentVerified
→ toPayaiVerifyResult to screen payers before serving. Fail-open by default.
Historical: demonstrable refuse mechanism (2026-07-16; free, reproducible)
Status (2026-07-16): mechanism proof — not adoption or demand.
Install the published gate and run against wash fixtures:
npm install twzrd-x402-gate@0.12.5
# from package root after install, or from a checkout:
npm run wash-dogfood
Verified live (2026-07-16):
| Fixture | preflight_id | decision | note |
|---|---|---|---|
7G73PL… wash dogfood |
378468 | block | wash_flagged=true |
HuSiSpc… |
378469 | block | wash_flagged=true, fleet≈98% |
BJGds… alt wash |
378470 | warn | wash_flagged=true (nuance — not all wash is hard-block) |
4LkEF… clean control |
378471 | warn | not wash |
Gate dogfood: approved=false reason=twzrd_decision_block, USDC spent = 0, tx broadcast = none, ALL PASS.
Public transcript: https://gist.github.com/twzrd-sol/2882bddee912f89e99061f3bc1da8227
Accurate paste line:
Preflight returned decision=block on wash seller 7G73PL… / HuSiSpc… (preflight_id 378468 / 378469, wash_flagged=true). Gate approved=false reason=twzrd_decision_block. No USDC spent. No tx broadcast. Repro:
npm i twzrd-x402-gate@0.12.5 && npm run wash-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.
Historical: foreign-wallet block proof (2026-07-23; official @x402 stack, fresh key, zero history)
Verified 2026-07-23: the same pre-sign block with a brand-new wallet that has zero TWZRD/corpus history — proving the mechanism is wallet-independent, not a whitelisted internal path.
- Wallet:
3GMuabSAATKEXTchSpyP1y5raBS7y6Kx8GShdAbiDLce(generated fresh; corpus pre-check:intel_score: 0,paid_calls: 0, facilitator 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.
Note on
@x402/coreDefaults:@x402/core>= 2.23.0 evaluates default spend controls before invoking TWZRD hooks. Calls breachingcoredefaults or referencing unrecognized assets abort before the gate preflight or callbacks execute. UsesetSpendControls(false)orx402Client.fromConfigif core spend limits should be deferred entirely to TWZRD.
PayKit (@solana/pay-kit)
Foundation pay-kit#303
exposes onBeforeX402PaymentCreation on createPayKitClient and registers it
on the internal x402Client. Pass TWZRD as that option — no TWZRD branding inside
pay-kit itself. This package does not hard-depend on unpublished @solana/pay-kit.
import { createPayKitClient } from "@solana/pay-kit";
import { createTwzrdPayKitBeforePaymentHook } from "twzrd-x402-gate";
const client = await createPayKitClient({
accept: ["x402"],
onBeforeX402PaymentCreation: createTwzrdPayKitBeforePaymentHook({
refuseWashFlagged: true,
}),
rpcUrl,
signer,
});
Equivalent: onBeforeX402PaymentCreation: installTwzrdAutoGate("pay-kit", { refuseWashFlagged: true }).
Same official @x402/core context hook as Path E. Abort returns
{ abort: true, reason }; PayKit throws before signTransactions.
MCP (@x402/mcp)
Wire twzrdOnPaymentRequested / prefer onPaymentRequired + onBeforePayment per
lifecycle hooks. Same policy core.
Raw-fetch composition (injectible pay client only)
import { installTwzrdAutoGate } from "twzrd-x402-gate";
import { wrapFetchWithPayment } from "@x402/fetch";
// Guard RAW fetch, then hand to a client that still surfaces 402 to the guard layer
// — OR installTwzrdAutoGate(x402Client) (canonical) / installTwzrdX402ClientHook alias.
const payingFetch = installTwzrdAutoGate((guarded) =>
wrapFetchWithPayment(guarded, client),
);
Buyer flow — locked sequence
Canonical path for every agent that spends USDC on Solana x402:
| Step | Call | Cost | What gates pay |
|---|---|---|---|
| 1 | POST /v1/intel/preflight |
free | decision=block → refuse (twzrd_decision_block). Score floor / optional can_spend also deny. |
| 2 | GET /v1/intel/merchant_card/{payTo} |
free | wash_flagged: true → refuse by default (twzrd_wash_flagged). Only tightens step 1. |
| 3 | First paid hop quickCheck |
$0.001 | On an approved warn with x402Fetch wired: GET /v1/intel/quick/{payTo} only. That hop does not request GET /v1/intel/trust/{payTo}. /trust at $0.05 is a separate allow-path receipt, not the warn hop. |
| 4 | Pay (or refuse) | resource price | Only if steps 1–2 approved (and any opt-in paid escalate did not block). |
Fail-open (no invent):
- Preflight HTTP/network error → default fail-closed (
TWZRD_FAIL_OPEN=truerestores legacy allow-on-outage). - Merchant card outage on a reputation-scored path (5xx, 429, thrown fetch, non-JSON 200) → same
failOpenswitch; default refuses withtwzrd_card_unreachable_fail_closed. This is tighten-only: an already-blocked payment keeps its more specific reason. - Merchant card reachable with no
wash_flagged(including 4xx, or 200 +insufficient_evidence) →washFlagged=null→ do not refuse on wash. - Unscored-network
observe: a card outage keeps the observe allow;wash_flagged: truestill refuses. - Only a successful card with
wash_flagged: truetriggers wash refuse or soft cap.
Wash policy (exact):
- Prior preflight deny → unchanged (wash never loosens a block).
refuseWashFlagged=falseor wash not true → keep preflight approval.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_*.
Order note: onWarnUpsell (points at first paid hop GET /v1/intel/quick $0.001) fires on preflight warn before the merchant_card wash check. A wash-flagged seller that preflighted as warn may still get the upsell hook, then be refused on step 2. Optional V7 GET /v1/intel/trust $0.05 stays autoReceipt / requireReceipt / escalateOnWarn:false.
Dogfood (one public live proof path):
- Free only (wash refuse):
npm run wash-dogfood→examples/wash-refuse-dogfood.ts - Official client + Path E hook (live Solana ≤$0.001):
npm run official-dogfood→examples/official-x402-dogfood.ts. Needs@x402/fetch@x402/svm@x402/core@solana/kit@scure/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 i twzrd-x402-gate
This doc describes 0.12.4. Check npm view twzrd-x402-gate version for the latest release.
CLI commands (package bin)
twzrd-safe-fetch, twzrd-gate-eval-refuse, twzrd-gate-doctor, twzrd-evidence-bundle,
twzrd-payment-decision, twzrd-bounty-preflight, twzrd-cold-start (run with npx <name>).
Pay-path behavior (0.12.4)
- Scored preflight runs on Solana mainnet and Base mainnet (
eip155:8453). Other EVM networks do not run the scored preflight. - A missing
payTois never approved. - A buyer preflight outage does not sign unless
TWZRD_FAIL_OPEN=true, exceptcreateBuyerFetch, which forcesfailOpen: falseand does not read that variable. - An unevaluated seller (
null_reason: unknown_subject) can be paid up to its card'srecommended_cap_usdc; above the cap it does not sign.refuseUnevaluated: truerefuses it outright (see Unevaluated sellers under Policy). - A price above
recommended_cap_usdcdoes not sign. - Disagreeing v1/v2 price or recipient fields are refused.
- An approved
warnwith a wired paying fetch requests/v1/intel/quickand does not request/v1/intel/trust. createTwzrdBeforePaymentHookdoes not readTWZRD_AUTO_GATEorTWZRD_GATE_ENABLED; those switches apply only toinstallTwzrdAutoGate.createTwzrdSettleGuardstays fail-open by default, so an unavailable gate does not abort.- The pay path is not uniformly fail-closed in any release:
createTwzrdSettleGuardis fail-open by default,TWZRD_FAIL_OPEN=truelets a buyer preflight outage sign on entry points that do not forcefailOpen, and those entry points default unscored networks toobserve(allow, wash check still runs).createBuyerFetchis the exception: the outage stays closed, and an unscored network is refused.
Optional settle guard (resource-server payer policy): see Seller settle guard
(onBeforeSettle) — optional 0.8.1 above. Do not confuse with facilitator
createOnBeforeSettleHook in @wzrd_sol/plugin-trustgate/facilitator — that screens
the merchant/payTo on a brokered settle (buyer-side counterparty check at the rail).
Live card screen: npx tsx examples/seller-settle-guard.ts --live <payerWallet>
TWZRD Payment Control (protocol-neutral authorization core)
The gate now ships a protocol-neutral policy runtime underneath the x402 surface.
Defining invariant: a signer path that calls assertIntentApproved will not sign
an intent that differs from what TWZRD evaluated. (Honest scope: TWZRD does not
own third-party wallets - the binding is enforceable exactly where the check runs
before the signer, not as a claim over arbitrary wallet internals.)
import {
evaluateIntent,
assertIntentApproved,
createLocalDecisionSigner,
createDecisionRegistry,
x402RequirementsToIntent, // or ap2CheckoutToIntent
} from "twzrd-x402-gate";
const signer = createLocalDecisionSigner();
const registry = createDecisionRegistry(); // consume-once
const intent = x402RequirementsToIntent(selectedRequirements, { resourceUrl });
const token = await evaluateIntent(intent, {
signer,
mandate, // user/company mandate (purpose, ceilings, resource scope)
policy, // local hard controls (caps, lists, recurring checks)
intelligence: twzrdIntel, // optional remote counterparty intelligence
});
// Wallet-side, immediately before signing the EXACT intent:
assertIntentApproved(intentBeingSigned, token, {
registry, // replay / consume-once
publicKeyPem: signer.publicKeyPem, // signature verification
});
// throws MISSING_VERIFIER_KEY | BAD_SIGNATURE | INTENT_HASH_MISMATCH |
// DECISION_EXPIRED | DECISION_NOT_ALLOW | DECISION_REPLAYED
// -> the signer is never invoked
// There is no published way to match an intent without verifying the decision
// signature: the old "twzrd-x402-gate/unsafe" subpath was removed in 0.9.4 and
// its module in 0.11.2. Always verify with a key.
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 charge only
createTwzrdMppOnChallenge guards Mppx.create({ onChallenge }). The mppx
Solana method signs AND broadcasts the transaction inside createCredential(),
so onChallenge is the last deterministic checkpoint before money moves - and
mppx re-throws onChallenge errors, so a TWZRD block is an exception that means
createCredential() never runs and nothing signs. On allow, the guard
creates the credential for the exact challenge it evaluated; non-solana/charge
challenges fail closed (allowUnevaluated: true to opt out).
import { Mppx } from "mppx/client";
import { client as solanaClient } from "mppx-solana";
import { createTwzrdMppOnChallenge, createLocalDecisionSigner } from "twzrd-x402-gate";
const mppx = Mppx.create({
methods: [solanaClient({ signer: wallet })],
onChallenge: createTwzrdMppOnChallenge({
signer: createLocalDecisionSigner(),
policy: { maxAmountUsd: "1.00" },
}),
});
Scope limit (honest): the guard is authoritative only when no
onChallengeReceived event handler supplies a credential. mppx resolves
eventCredential ?? onChallenge(...), so an event handler returning a credential
short-circuits the guard and pays ungated. Do not register both on one client.
What it refuses, and why. Each of these is a case where the transaction mppx would actually sign is not the transaction the decision covers - so the guard fails closed rather than approve a payment it cannot bind:
| Case | Code | Reason |
|---|---|---|
| Sponsored charge | SPONSORED_CHARGE |
sponsored / sponsorPath / feeTokenAmount make mppx-solana build a second transfer to the sponsor on top of the advertised amount. PaymentIntent v1 binds one amount to one payTo, so approving it would authorize strictly more value than the decision covers. |
| Non-USD-pegged asset | UNPRICED_ASSET |
Policy ceilings are USD; the wire amount is base-unit tokens. Pricing SOL would store asset: solana:native beside a dollar amount and lose the token quantity actually transferred. (A 1.5 SOL charge of 1500000000 at 9 decimals would otherwise evaluate as "$1.50" and sail under a $5 ceiling while moving ~$270.) USDC/USDT only until an intent version carries token amount + quote. |
| Unknown cluster | UNKNOWN_CLUSTER |
mppx-solana's resolveEndpoint returns an unrecognized cluster verbatim as the RPC endpoint URL, and a network string containing "solana" is otherwise scored as mainnet - so solana:https://seller-rpc.example would inherit mainnet reputation for a chain never observed. Known cluster names only. |
| Misdeclared decimals | MALFORMED_CHALLENGE |
A known stablecoin declaring the wrong decimals is a discount attempt, not a rounding error. |
The intent binds a digest of the entire normalized challenge, not just
realm:id - swapping recipient or amount under the same challenge id changes
the intent hash, so the decision no longer matches.
Verdicts: block throws. warn proceeds to pay by default - set
treatWarnAsBlock: true to refuse on warn too.
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 mainnet (
eip155:8453) is scored, like Solana mainnet. Other EVM networks do not run the scored preflight (see Networks).
CLI: twzrd-bounty-preflight (worker-side refuse, zero spend)
For agents that work bounty boards (DeskCrew arena, ClawTasks). Before you pay
an attempt fee or stake collateral, read the board, read the paid door's unpaid
402 for its payTo, gate that payTo, and decide each open row. One JSON transcript
on stdout (schema: twzrd.bounty_preflight.v1, usdc_spent: 0,
signer_invocation_count: 0). Exit 0 means at least one open row is eligible
— pay only rows with proceed: true. Exit 1 means none are. Nothing signs,
nothing spends.
# DeskCrew: ticket-only attempt cost; the row's entry fee is added separately
npx twzrd-bounty-preflight --board https://deskcrew.io/api/arena/contests \
--attempt-cost-usd 0.02 --max-attempt-usd 0.25 --assumed-win-prob 0.2
# Any other board: name its paid door explicitly
npx twzrd-bounty-preflight --board https://clawtasks.com/api/bounties?status=open \
--paid-endpoint https://clawtasks.com/api/paid --my-networks base --allow-unscored-payee
Refuse reasons, in order: gate_block / gate_wash_flagged (TWZRD refused the
payee), gate_unscored (the payee's rail is outside the scored corpus — Solana
mainnet and Base mainnet (eip155:8453) are scored; other rails are not; the
gate's "allow" there is a policy pass-through, never a trust allow; opt in with
--allow-unscored-payee, wash still refuses),
payout_network_unsupported / payout_network_unknown, attempt_cost_over_max
(attempt fee + board entry fee + stake vs the ceiling), below_break_even /
assumed_win_prob_missing (your declared win probability vs
at_risk / (agent_share * bounty)). The board's approval rate and contest size
are reported as naive and contested EV for context only; they are never
substituted for --assumed-win-prob. Self-serve transcript, not adoption proof.
Library: import { safeFetch } from "twzrd-x402-gate/safe-fetch".
Quickstart: installTwzrdAutoGate (default-on)
Canonical entry point (design: docs/strategy/install-autogate-design.md). One name, five adapters:
| Call | Adapter |
|---|---|
installTwzrdAutoGate(payWrap, opts?) |
Fetch: guard raw fetch → pay client |
installTwzrdAutoGate(x402Client, opts?) |
Official x402 onBeforePaymentCreation |
installTwzrdAutoGate("x402-solana", opts) |
PayAI beforePayment on createX402Client |
installTwzrdAutoGate("pay-kit", opts) |
PayKit onBeforeX402PaymentCreation (Foundation #303) |
installTwzrdAutoGate("mpp", opts) |
MPP onChallenge (returns handler) |
Aliases: installTwzrdX402ClientHook, createTwzrdMppOnChallenge remain; docs prefer AutoGate.
Kill switch: TWZRD_GATE_ENABLED=false or TWZRD_AUTO_GATE=0 applies to installTwzrdAutoGate only. createTwzrdBeforePaymentHook does not read those variables. Uninstall x402 installs with uninstallTwzrdAutoGate(client).
installTwzrdAutoGate reads that switch per call on the x402-client, x402-solana, pay-kit and MPP seats before it calls the factory, so flipping it moves an already-installed
AutoGate seat in both directions. The fetch / payWrap seat is the exception: it resolves the switch once, when the fetch is composed,
so a fetch built while the switch was off stays ungated after you clear it — rebuild the fetch. options.disabled: true is a
permanent install-time opt-out everywhere and no env change revives it.
installTwzrdAutoGate is the one-liner form of "guard the raw fetch, then hand it to your
x402 client." It takes a payWrap function — whatever composes your paying client on top of
a fetch — and returns a fetch that's already gated: a blocked seller throws before your client
ever gets a chance to sign.
import { installTwzrdAutoGate } from "twzrd-x402-gate";
import { wrapFetchWithPayment } from "@x402/fetch";
const payingFetch = installTwzrdAutoGate((guarded) => wrapFetchWithPayment(guarded, buyerWallet));
// Use payingFetch everywhere you'd call a paid resource:
const response = await payingFetch("https://api.exa.ai/search");
payWrap receives the guarded fetch (the guard has already run by the time your client
sees a 402) — this is the only correct composition order. Building it the other way round
(guarding an already-paying fetch) is a no-op; see Compatibility note.
Any x402 client that composes over an underlying fetch works the same way — swap in
whatever payWrap your client's API expects (agentcash, ClawRouter, PayAI, a custom
@x402/svm scheme, etc.).
This installTwzrdAutoGate(payWrap) seat is default ON. Disable that seat with
TWZRD_AUTO_GATE=0 or TWZRD_GATE_ENABLED=false, or with { disabled: true }
(install option, for example in tests) — the raw fetch is handed straight to
payWrap, unguarded. createTwzrdBeforePaymentHook does not read those
variables. Setting them does not stop that factory from aborting a bad payment.
What happens on every HTTP 402 the raw fetch returns:
- Every distinct
accepts[]entry has to pass. The guard does not keep only the Solana entry. - 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. A reachable card with no wash finding is not a refusal unlessrefuseWashUnknownis on. A card outage on the scored path honoursfailOpen(default fail-closed). - An unscored network (keyword devnet, Base Sepolia, other EVM) is allowed under the default
observemode. The wash check still runs.createBuyerFetchrefuses that network instead. - Otherwise returns the 402 to
payWrap's client, which pays normally.
Non-402 responses pass through unchanged.
Legacy: createGuardedX402Fetch
Kept for compatibility. A new integration should use createBuyerFetch.
The older wrapper's hourly budget is in memory only, its caps are optional,
and unscored networks follow observe instead of createBuyerFetch's strict.
createGuardedX402Fetch combines a configured @x402/core client with the
existing TWZRD pre-sign evaluator and local recipient / spend rules. Register
the payment schemes and wallet signer on the client first; the helper returns
the paying fetch, so the request can use the normal x402 402 flow:
import { x402Client } from "@x402/core/client";
import { createGuardedX402Fetch } from "twzrd-x402-gate";
const client = new x402Client();
// Register the x402 payment schemes and wallet signer on `client` here.
const guardedFetch = createGuardedX402Fetch({
client,
maxPricePerCall: "0.05",
hourlyBudgetCap: "2.00",
allowedRecipients: ["0x1234567890abcdef1234567890abcdef12345678"],
});
const response = await guardedFetch("https://api.example.com/paid-resource");
The local checks run against the x402 client's selected payment requirement,
then TWZRD's existing @x402/core pre-sign evaluator runs before the client
creates the payment signature. The caps are decimal USDC values compared with
the selected requirement's six-decimal atomic amount. When a spend cap is
enabled, the helper accepts only canonical USDC on Solana mainnet/devnet and
Base mainnet/Sepolia; unsupported tokens and networks fail closed. EVM
recipient matching is case-insensitive; Solana base58 matching is
case-sensitive. Supplying an empty allowedRecipients array denies every
recipient.
The hourly ledger is in memory for the lifetime of this returned fetch and is
shared by concurrent calls through it. Spend is reserved while TWZRD evaluates
the challenge and recorded before the pre-sign hook returns, so a later signer
failure still consumes the budget. It resets when the process restarts. The
existing TWZRD_AUTO_GATE=0 / TWZRD_GATE_ENABLED=false switches disable the
remote TWZRD evaluation only; local caps and the recipient allowlist remain on.
The helper expects a configured x402 client because a bare signer.pay() method
does not define a standard x402 challenge-to-payment contract. A separate paid
Path A fetch cannot be passed through twzrd.x402Fetch: it has a different
signer and cannot share this fetch's local ledger.
Fulfillment record (opt-in, local only)
Pass onFulfillment to keep one row per paid call: who was paid, the
settlement transaction, who the seller says served, and whether the seller says
the job became ready. Nothing is sent anywhere, and it never changes what is
allowed or refused.
import { createFileFulfillmentLog, createGuardedX402Fetch } from "twzrd-x402-gate";
const log = createFileFulfillmentLog("./fulfillment.jsonl");
const guardedFetch = createGuardedX402Fetch({ client, onFulfillment: log.record });
Each row (schema: "twzrd.fulfillment.v1") carries paid_to, network and
amount_atomic from the requirement the client signed, tx and settled from
the facilitator's PAYMENT-RESPONSE, and, from the seller's JSON body only,
served_by (provider or model), requested_model, failover_hops, ready,
job_status and job_id. The resource is kept as its origin; no other body
field is copied. verdict is one of:
| verdict | meaning |
|---|---|
match |
The seller reports ready: true, or it served the model that was requested (a dated snapshot of it counts). For a GPU job this is the seller's claim that capacity is ready, not evidence that the workload ran or its output is correct. |
pending |
The seller reports ready: false with a non-terminal status such as provisioning. Readiness arrives later; it is not a failure. |
mismatch |
One or more reasons: not_ready (terminal status without ready), model_substituted, tx_disagrees (the body's tx is not the PAYMENT-RESPONSE tx), settle_failed, paid_http_error. |
not_reported |
The response carries none of these fields. |
The row records the seller's own claims next to a transaction anyone can check
on chain. It is not a delivery proof, and it is not a TWZRD receipt: the signed
V7 receipt from paid /trust attests TWZRD's check of a seller, not that a GPU
or a model was delivered. A GPU seller's provider is often a node label, so it
is recorded but never compared with paid_to. To record a later job-status body
(for example once the job is ready), call buildFulfillmentRecord with that body
and append the row yourself. A proxy that pays internally and returns 200 never
exposes a payment header, so no row is written for it.
Networks (Solana mainnet and Base mainnet scored)
The gate recognizes multi-chain 402s. Solana mainnet and Base mainnet (eip155:8453) run the scored preflight. Other EVM networks do not. eip155:137 does not run twzrdPreflight or the scored preflight.
| Network | Reputation scored? | Default policy (unsupportedNetworkMode) |
|---|---|---|
| Solana mainnet | Yes — free preflight + merchant_card | allow/block from intel |
Base mainnet (eip155:8453, base) |
Yes — free preflight + merchant_card | allow/block from intel |
Other EVM (eip155:* except 8453) |
No scored preflight | observe (default): decision=unknown, policyAction=allow, telemetry unsupported_network_seen. Wash still runs — wash_flagged refuses before sign. |
Other EVM in strict mode |
No | policyAction=block before sign |
An unscored network is never represented as a TWZRD trust allow. Set TWZRD_UNSUPPORTED_NETWORK_MODE=strict (or
{ unsupportedNetworkMode: "strict" }) to hard-block unscored networks.
requireReceipt (Path A) follows the same line: hard receipts apply to scored networks
only — on an unscored network there is no receipt to buy, so the decision carries
receiptSkipped: "unscored_network" and continues. Use strict mode to fail closed there.
requireLogInclusion sits one step further: a captured Path A receipt does not count as
trust until its leaf is proven included in the Receipt Transparency log under a key you
pinned (spec). A valid signature proves authorship and
integrity; only the log proves the issuer showed everyone the same answer. The log is live
(v0.1 domain, genesis 2026-09-03), and a paid response carries its proof inline as
log_inclusion — the audit path plus the signed head it targets — once the leaf is merged, so
the usual case verifies offline with nothing but your pinned key. The gate takes no
dependency on the log verifier — you wire it; it receives the receipt and the whole paid
response, and twzrd-log-verifier's results fit the verdict shape with no adapter:
import { verifyLogInclusion, verifyReceiptInLog } from "twzrd-log-verifier"; // not yet on npm — see its README
// Today the log serves a single signing key (the receipt issuer key); pin it out of band.
// When v0.2 ships a key directory, pass that here instead — same call.
const PIN = "Ak5SQwHpuQAqU7ty7ZWX7qgF39A9yi72c22KNn8sHzvS";
await evaluate_x402_resource(url, requirements, {
requireReceipt: true,
x402Fetch,
requireLogInclusion: {
verifier: async (_receipt, ctx) =>
ctx.logInclusion
? verifyLogInclusion(ctx.response, PIN) // offline: no round-trip
: verifyReceiptInLog({ baseUrl: "https://intel.twzrd.xyz", receipt: ctx.response, trusted: PIN }), // fetch; 404 ⇒ pending
// hard: true — an unproven receipt denies spend (default)
// onPending: "deny" — a leaf not merged yet is unprovable at pay time; "allow"
// tolerates the one-anchor-period merge window (default "deny")
// refuseTofu: true — never accept keys the log advertised about itself (default)
},
});
The receipt is returned either way (result.receipt) — you paid for it. What changes is
approved: a denial sets logInclusionDenied: true, policyAction: "block", and a reason of
twzrd_log_inclusion_failed | _pending | _tofu_refused | _error; result.logInclusion carries
the verdict (key_id, leaf_index, tree_size, pending, tofu). A verifier that throws
denies under hard — a broken or unreachable verifier must not wave receipts through. So does a
Path A attempt that yields no receipt at all (non-OK response, thrown fetch, x402Fetch not
wired): an outage on the receipt endpoint must never produce a better outcome than an empty
receipt body. Path A that your requireReceipt threshold never attempted is out of scope — this
knob gates receipts, it does not override your threshold.
Two things to know today: a leaf is merged within one anchor period, so a receipt paid for
just now may not yet carry log_inclusion — the fetch fallback above then reports pending,
and onPending decides. And nothing is anchored on Solana yet (anchor_authority is null),
so inclusion currently proves what the log committed to, not yet when — the anchor is what
makes backdating detectable.
Dual-chain accepts still prefer the Solana entry for scoring (same as payment clients that prefer Solana when available).
Lower-level: withTwzrdGuard
installTwzrdAutoGate is built on withTwzrdGuard — the fetch wrapper itself, if you want to
manage the raw/paying composition yourself:
import { withTwzrdGuard } from "twzrd-x402-gate";
import { wrapFetchWithPayment } from "@x402/fetch";
const raw = globalThis.fetch; // MUST still surface HTTP 402
const guarded = withTwzrdGuard(raw); // guard sits upstream
const safeFetch = wrapFetchWithPayment(guarded, buyerWallet);
const response = await safeFetch("https://api.exa.ai/search");
What the guard does on HTTP 402:
- 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). Reachable + no signal fails open (no invent). A card outage on the scored path honoursfailOpen(default fail-closed). - If approved: returns the original 402 for the x402 client to pay.
Opt out of wash refuse: withTwzrdGuard(fetch, { refuseWashFlagged: false }) or
TWZRD_REFUSE_WASH_FLAGGED=0. Soft cap instead of hard refuse: washMaxUsdc /
TWZRD_WASH_MAX_USDC.
Non-402 responses pass through unchanged.
First paid hop on warn (revenue path)
Wiring x402Fetch turns on buyer defaults: a proceeding warn settles
GET /v1/intel/quick/{payTo} at $0.001 (escalateOnWarn). That is the first paid hop.
Optional V7 — autoReceipt or material-allow requireReceipt — buys
GET /v1/intel/trust/{payTo} at $0.05 for a signed credential. Opt out of the
cheap hop with escalateOnWarn: false if the host explicitly wants /trust first.
const safeFetch = withTwzrdGuard(x402Fetch, {
x402Fetch, // warn → $0.001 /quick first
autoReceipt: true, // optional V7: also buy $0.05 /trust on allow (not after /quick)
onReceipt: (receipt, tx) => {
// receipt is a twzrd_receipt (V7 + ERC-8004 reputation_credential)
console.log("Trust receipt captured:", tx);
},
});
autoReceipt is off by default — it spends the buyer's USDC, so you opt in. When on
and /quick did not already settle, warn/allow can buy the $0.05 V7 credential.
x402Fetch is yours to supply (this package is dependency-free). Wire the proven
@x402/svm sponsored-feePayer client — the same one twzrd-mcp-server uses:
import { wrapFetchWithPayment } from "@x402/fetch";
const x402Fetch = wrapFetchWithPayment(fetch, buyerWallet); // settles 402 challenges
Gate it behind your own ROI policy (e.g. only auto-buy the receipt for payments above a
threshold). Runnable, no-spend demo: examples/auto-receipt.ts
(npm run autoreceipt-demo). A bundled/sponsored x402Fetch (so integrators need no wallet)
is the next step.
Quick tier ($0.001) — cheap paid qualify
The reputation ladder has three rungs: free preflight (allow/warn/block), $0.001
quickCheck (tier + score, no receipt), $0.05 autoReceipt (full intel + signed V7
receipt). When an evaluated warn is inconclusive and you want a
cheap paid confirmation before committing — without paying 50× for the portable receipt —
use quickCheck:
import { quickCheck } from "twzrd-x402-gate";
const q = await quickCheck(sellerWallet, { x402Fetch }); // settles $0.001 to /v1/intel/quick
if (q.available && (q.tier === "Gold" || q.tier === "Platinum")) {
// tier is high enough — proceed with the larger payment
}
quickCheck is fail-soft — it never throws; any gap (no x402Fetch, unreachable, settle
failure) returns available: false, so a quick-tier hiccup can't break your flow. The hard
allow/warn/block decision stays the free preflight's job.
Autonomous risk-escalation — escalateOnWarn (pay-to-confirm on warn)
An evaluated warn (no null_reason, score present) proceeds by default. A card
with null_reason: unknown_subject is allowed only up to its recommended_cap_usdc
(see Policy), and this hop never runs for it: there is no paid score for a
seller intel has never seen either, so the $0.001 would buy nothing. escalateOnWarn closes the loop on a proceeding warn:
the guard settles the cheap $0.001 GET /v1/intel/quick/{payTo} and re-decides on
the paid score — below the floor the payment is blocked, at/above it proceeds. That
hop does not request GET /v1/intel/trust/. The paid call fires from your
agent's own risk policy (no human), and the paid signal actually gates the spend (unlike
autoReceipt, which is upsell-only and never changes the decision).
const safeFetch = withTwzrdGuard(x402Fetch, {
escalateOnWarn: {
minSpendUsdc: 0.01, // don't pay $0.001 to vet a sub-cent buy
blockBelowScore: 40, // block when the paid quick score is below this (default: preflightMinScore)
},
x402Fetch, // settles the $0.001 quick charge
});
// warn + paid score < 40 -> throws "[twzrd-guard] payment blocked: twzrd_escalated_warn_block ..."
// warn + paid score >= 40 -> proceeds (result.escalated=true, result.escalatedScore set)
Opt-in, fail-soft (if the quick tier can't answer, the base warn is preserved), and it
only tightens — a warn may become a block, but an allow or block is never changed.
This is the autonomous demand loop: an uncertain counterparty is vetted with real paid intel,
automatically, before your agent commits.
Sponsored payer — use the paid rungs with no wallet (prototype)
createSponsoredX402Fetch lets a sponsor settle the paid rungs on the agent's behalf, so
an integrator can call quickCheck / autoReceipt with no wallet of their own:
import { createSponsoredX402Fetch, quickCheck } from "twzrd-x402-gate";
// `settle` = the funded backend (your @x402/svm fetch, or a TWZRD treasury sponsor endpoint).
const x402Fetch = createSponsoredX402Fetch({ settle });
const q = await quickCheck(seller, { x402Fetch }); // sponsor pays — caller holds no wallet
Two backends plug into settle: gas-sponsored (live via @x402/svm — agent pays USDC, the
resource server's feePayer covers SOL gas, the model twzrd-mcp-server uses) and
full-sponsor (a TWZRD treasury endpoint pays on the agent's behalf — the true no-wallet
path). The full-sponsor endpoint + treasury is founder-gated (who funds it + per-agent
budget caps); this ships the client seam + a dry-run so the wiring is ready.
No-spend demo: examples/sponsored-payer.ts (npm run sponsored-demo).
evaluate_x402_resource — standalone preflight
Use when you already have the paymentRequirements object from a parsed 402 body:
import { evaluate_x402_resource } from "twzrd-x402-gate";
const result = await evaluate_x402_resource(
"https://api.exa.ai/search",
paymentRequirements, // X402PaymentRequirements from the 402 body
);
console.log(result.decision); // "allow" | "warn" | "block"
console.log(result.trustScore); // number | null
console.log(result.approved); // boolean
console.log(result.receiptUrl); // "https://intel.twzrd.xyz/v1/intel/trust/<payTo>"
if (!result.approved) throw new Error(`Blocked: ${result.reason}`);
With autoReceipt:
const result = await evaluate_x402_resource(url, requirements, {
autoReceipt: true,
x402Fetch: myPayingFetch,
onReceipt: (receipt, tx) => storeCredential(receipt),
});
// result.receipt — twzrd_receipt (V7 + ERC-8004 reputation_credential)
// result.receiptTx — on-chain settlement tx
// result.receiptFeeCaptured — true when fee landed
Fee-payer preference (multi-facilitator accepts[])
When a seller lists more than one facilitator in its 402 accepts[] (e.g. Dexter
and TWZRD), you can tell the gate which fee payer to settle through. It selects
the matching entry the seller already offers — it never adds, rewrites, or
forces an entry onto the seller's 402, and falls back to the normal
Solana-mainnet preference when nothing matches.
Opt-in, off by default. Set once via env:
# prefer TWZRD's facilitator when a seller multi-lists (alias resolves to its feePayer)
export TWZRD_PREFER_FEE_PAYER=twzrd
# or any explicit base58 fee payer you want to route to
export TWZRD_PREFER_FEE_PAYER=4LkEFjJdXARkKx8FBx4LBFa2SvJNmjQpgGDLoJcypZUE
or programmatically:
import { pickRequirements, TWZRD_FEE_PAYER } from "twzrd-x402-gate";
const chosen = pickRequirements(body.accepts, { preferFeePayer: TWZRD_FEE_PAYER });
With no preference set, selection is unchanged (first Solana-mainnet entry).
Lower-level APIs
Direct approval call
import { createTwzrdGate } from "twzrd-x402-gate";
const gate = createTwzrdGate();
const { approved, reason, card } = await gate.approvePayment({
payTo: "SELLER_WALLET_FROM_402",
resourceUrl: "https://merchant.example/paid",
priceUsdc: 0.003,
});
if (!approved) abort(reason);
@x402/mcp payment hook
The hook accepts the real @x402/mcp v2 PaymentRequestedContext
({ toolName, arguments, paymentRequired }) — wire it directly:
import { createx402MCPClient } from "@x402/mcp";
import { registerExactSvmScheme } from "@x402/svm/exact/client";
import { twzrdOnPaymentRequested } from "twzrd-x402-gate";
const client = createx402MCPClient({
name: "my-agent",
version: "1.0.0",
schemes: [/* e.g. registered SVM scheme */],
autoPayment: true,
onPaymentRequested: (ctx) => twzrdOnPaymentRequested(ctx), // false = deny before signing
});
The legacy flat shape ({ accepts, context }) is still accepted. Prior to
0.6.1, only the flat shape was read — wired into the real @x402/mcp runtime
the hook saw accepts: undefined and fail-closed-blocked every payment (safe,
but a 100% false-block).
wrapFetchWithTwzrdGate
import { wrapFetchWithTwzrdGate, resolveConfig } from "twzrd-x402-gate";
// Alternative fetch wrapper — same interception logic, no autoReceipt.
const gatedFetch = wrapFetchWithTwzrdGate(fetch, resolveConfig());
withTwzrdGuard is preferred — it composes with autoReceipt and onReceipt.
wrapFetchWithTwzrdGate remains for codebases that can't migrate.
Policy
A payment is blocked when:
decision ∈ blockDecisions(default:["block"])trust_score < preflightMinScore(default:40), after an evaluated card.null_reason: unknown_subject(orscore: null) is not a low score: see Unevaluated sellers below.can_spend === false— only whengateOnCanSpend: true(defaultfalse, opt-in)- the price is above
recommended_cap_usdcwhen the card was otherwise approved - on Solana or Base, the requirement names an asset that is not USDC on that network (
twzrd_non_usdc_asset, 0.11.1+). Every cap is in USDC, andamountis in the named asset's base units, so any other asset cannot be priced. A requirement that names no asset is read as USDC.
Unevaluated sellers (0.11.0+). Live intel answers a seller it has never evaluated with null_reason: unknown_subject, score: null, a floor trust_score of 45, decision: "warn" and a recommended_cap_usdc. For that floor score live intel's ceiling is $0.10, and the card reports the lower of $0.10 and the requested price (2026-09-28). The gate follows that card instead of refusing every new seller, so a never-seen seller can be paid up to $0.10 per call:
| Case | Result | Reason |
|---|---|---|
price at or under recommended_cap_usdc |
signs | twzrd_unevaluated_within_cap_<price>_le_<cap> |
| price above the cap | does not sign | twzrd_unevaluated_over_cap_<price>_gt_<cap> |
| card carries no finite cap | does not sign | twzrd_unevaluated_no_cap_<null_reason> |
| price unknown | does not sign | twzrd_unevaluated_unknown_price_<null_reason> |
decision other than allow / warn |
does not sign | twzrd_unevaluated_subject_<null_reason> |
refuseUnevaluated: true |
does not sign | twzrd_unevaluated_subject_<null_reason> |
An approval here carries unevaluated: true and score: null, never a trust score. The free wash check still runs after it, so a wash-flagged unevaluated seller does not sign. refuseUnevaluated: true (or TWZRD_REFUSE_UNEVALUATED=1) restores the 0.9.9–0.9.16 behaviour: null_reason: unknown_subject returns reason twzrd_unevaluated_subject_unknown_subject and nothing unevaluated signs.
A price above recommended_cap_usdc returns a reason that starts with twzrd_over_recommended_cap_. A direct createTwzrdBeforePaymentHook abort of a block card returns reason twzrd_decision_block. That reason string is not block and is not twzrd_fail_closed. A missing payTo returns reason twzrd_unidentifiable_payment_recipient from twzrdApprovePayment. That reason is not twzrd_missing_payTo. When the buyer preflight fetch throws and TWZRD_FAIL_OPEN is unset, twzrdApprovePayment returns reason twzrd_fail_closed. That reason is not twzrd_preflight_fetch_error.
An evaluated warn (no null_reason, score present) is allowed unless overridden. When the buyer preflight fetch throws and TWZRD_FAIL_OPEN is unset, twzrdApprovePayment returns approved: false with reason twzrd_fail_closed and the wallet does not sign. An omitted failOpen on createTwzrdSettleGuard is a different default: a thrown screen returns without abort. 0.12.4 is not uniformly fail-closed: createBuyerFetch refuses that outage and does not read TWZRD_FAIL_OPEN, the other buyer entry points honor the env, and createTwzrdSettleGuard stays fail-open when failOpen is omitted.
A 402 whose payment requirements yield no identifiable seller wallet (missing/empty payTo, or an unparseable accepts[]) is a different case from "unknown seller" — it always blocks with reason: twzrd_unidentifiable_payment_recipient, without ever calling the preflight network. The wallet does not sign. This is unconditional (not affected by buyer failOpen): that switch governs a buyer preflight outage, not a missing payTo. An omitted failOpen on createTwzrdSettleGuard is a different default: a thrown screen returns without abort.
can_spendnote: the free preflight returnscan_spend=falsefor most sellers not yet in the TWZRD corpus, including legitimate ones.can_spend: falsealone does not block (gateOnCanSpenddefaults false). That is separate fromnull_reason: unknown_subject, which signs only within its card's cap. SetgateOnCanSpend: trueto also block oncan_spend: false.
Refusals
Every reason the gate returns, by entry point. A refusal means the wallet was not asked
to sign (signerInvocations = 0). test/readme-refusal-codes.test.ts fails if a reason
string in the source is missing here, so this table cannot drift from the code.
<...> marks a value filled in at runtime.
Buyer approval (twzrdApprovePayment, used by every buyer entry point)
On the x402 client hooks (createTwzrdBeforePaymentHook, installTwzrdAutoGate,
createTwzrdPayKitBeforePaymentHook, createGuardedX402Fetch) the abort reason is
[twzrd] <reason> payTo=<payTo> network=<network>.
| Reason | When | Default |
|---|---|---|
twzrd_unidentifiable_payment_recipient |
The requirement names no payTo. Intel is not called. |
always |
twzrd_invalid_price |
A caller passed a priceUsdc that is negative or not finite. Intel is not called. |
always (0.11.2+) |
twzrd_non_usdc_asset |
On Solana or Base, the requirement names an asset that is not USDC on that network. Intel is not called; failOpen does not apply. |
always (0.11.1+) |
network_not_scored, network_missing |
The network is not scored (not Solana or Base mainnet). A name containing devnet, testnet, or sepolia, including Base Sepolia, is unscored. A Solana devnet named only by its CAIP-2 genesis id is still scored. |
refused when unsupportedNetworkMode is strict. The shared default is observe (allow). createBuyerFetch sets strict |
twzrd_decision_<decision> |
Intel's card decision is in blockDecisions. |
twzrd_decision_block |
twzrd_unevaluated_subject_<null_reason> |
Intel has never evaluated the seller, and refuseUnevaluated is set or the decision is not allow/warn. |
strict mode off |
twzrd_unevaluated_no_cap_<null_reason> |
Never evaluated, and the card has no finite recommended_cap_usdc. |
always |
twzrd_unevaluated_unknown_price_<null_reason> |
Never evaluated, and the price is unknown. | always |
twzrd_unevaluated_over_cap_<price>_gt_<cap> |
Never evaluated, and the price is above the card's cap (live: min($0.10, price)). | always |
twzrd_can_spend_false |
The card says can_spend: false. |
only with gateOnCanSpend: true |
twzrd_score_<score>_below_<min> |
An evaluated seller scores below preflightMinScore. |
min 40 |
twzrd_over_recommended_cap_<price>_gt_<cap> |
An evaluated seller, and the price is above the card's recommended_cap_usdc. |
always |
twzrd_wash_flagged |
The free merchant card says wash_flagged: true. |
on (refuseWashFlagged defaults true on every entry point) |
twzrd_wash_flagged_above_cap_<price>_gt_<cap>, twzrd_wash_flagged_above_cap_unknown_price_max_<cap> |
Wash-flagged, washMaxUsdc is set, and the price is above it or unknown. |
only with washMaxUsdc |
twzrd_wash_unknown |
The merchant card answered but has not fully answered the wash question: no card (including a 4xx), wash confidence other than full (for example partial_inbound_only, base_2cycle), ring_evaluated: false, or wash_stale: true. Never reported as an outage; failOpen does not open it. Scored networks only. |
opt-in (refuseWashUnknown: true; off by default in 0.12.x, on from 0.13.0; also needs refuseWashFlagged) |
twzrd_fail_closed (<error>) |
The preflight did not answer (non-2xx, non-JSON, network error, or no answer within intelTimeoutMs). |
fail-closed unless failOpen |
twzrd_card_unreachable_fail_closed (<error>) |
The merchant card did not answer (5xx, 429, non-JSON, network error, or no answer within intelTimeoutMs). A 4xx is an answer, not an outage. With failOpen the payment is allowed and the result carries cardUnreachable: true (the wash check did not run). |
fail-closed unless failOpen |
Approval reasons, for onDecision consumers: twzrd_allow, twzrd_warn_allowed,
twzrd_unevaluated_within_cap_<price>_le_<cap>, twzrd_wash_capped_<price>_le_<cap>,
twzrd_fail_open (an outage allowed because failOpen is set).
x402 client hook only
| Reason | When |
|---|---|
amount_field_conflict, payto_field_conflict |
The requirement's v1 and v2 price fields (maxAmountRequired / amount) or recipient fields (payTo / pay_to) disagree. Two spellings of the same 0x address are one recipient. |
amount_malformed |
The amount is present but is not an ASCII base-unit integer (a sign, decimal point, exponent, whitespace or non-ASCII digit). Refused before intel on every entry point (0.11.2+); twzrd.safeFetch reports it as malformed_amount. |
payment_control_unevaluable: missing <field> |
Payment Control is on and the requirement has no amount or payTo. |
payment_control_block:<reason codes> |
Payment Control (paymentControl option) refused the intent. |
twzrd_escalated_warn_block (paid quick score <score> < <floor>) |
escalateOnWarn bought the $0.001 /quick score and it is below the floor. |
twzrd_receipt_required_missing_x402Fetch |
requireReceipt needs a paid receipt and no x402Fetch is wired. |
twzrd_receipt_required_failed (HTTP <status>), twzrd_receipt_required_error (<error>) |
The required paid receipt could not be bought. |
aborted_before_payment: signal already aborted |
The caller's abort signal fired before the hook ran. |
Fetch wrappers and the MCP hook
withTwzrdGuard, wrapFetchWithTwzrdGate, installTwzrdAutoGate(payWrap) and
twzrdOnPaymentRequested see the whole 402 but not which accepts[] entry the
paying client will choose, so every distinct entry must pass the buyer approval
(one free preflight each; paid hops run once, for the preferred entry). The
first refusal is reported with that entry's payTo (0.11.2+).
| Reason | When |
|---|---|
too_many_payment_options |
The 402 lists more than 8 distinct offers. |
./cloudflare-base (edge Worker seat)
withTwzrdBasePreflight and createTwzrdCloudflareBaseApproval apply the same
rules to the Base USDC entry, restated in the module so it imports nothing:
twzrd_unidentifiable_payment_recipient, amount_field_conflict,
amount_malformed, twzrd_non_usdc_asset, invalid_base_offer (before
intel; failOpen never signs these), then twzrd_decision_block, twzrd_wash_flagged,
twzrd_unevaluated_subject_*, twzrd_unevaluated_no_cap_*,
twzrd_unevaluated_over_cap_*, twzrd_score_*_below_* and
twzrd_over_recommended_cap_*. The price comes from the entry's own amount;
the priceUsdc option is ignored for the decision. Before 0.11.2 this seat
signed on any non-block verdict.
createBuyerFetch
Thrown as BuyerFetchBlockedError with reason; message [twzrd-buyer-fetch] <reason>.
Refusals are flushed to the decision file before the error is thrown, except
use_buyer_fetch_for_payment and decision_log_unavailable. Rows are in the
order the checks run. The buyer approval above runs between
hourly_budget_exceeded and invalid_preflight_decision, and its twzrd_*
reasons also refuse here.
| Reason | When |
|---|---|
presigned_payment_not_allowed |
The caller's request already carries a payment header. |
use_buyer_fetch_for_payment |
The client's hook ran for a payment not made through the returned fetch. |
amount_field_conflict, payto_field_conflict, amount_malformed |
As in the client hook table. |
recipient_missing |
The selected offer names no recipient. |
unauthorized_recipient |
payTo is not in allowedRecipients (an empty list refuses all). |
unsupported_or_non_usdc_asset |
The asset is not USDC on that network. |
amount_missing_or_malformed |
The amount is not a base-unit integer. |
price_cap_exceeded |
The amount is above maxPricePerCall. |
amount_out_of_range |
The amount is above 2^53-1 micro-USDC. |
expected_amount_unavailable |
expectedAmount is a function that returned no decimal string or threw. |
expected_amount_mismatch |
The amount is not exactly expectedAmount. |
hourly_budget_exceeded |
The payment would take spend in the last hour above hourlyBudgetCap. |
invalid_preflight_decision |
The preflight card's decision is neither allow nor warn. |
request_aborted |
The request's signal aborted before signing. |
decision_log_unavailable |
The decision file could not be written and flushed. |
spend_ledger_unavailable |
The spend could not be recorded before signing. |
payment_without_approval |
A payment header reached the transport without this wrapper's approval. |
core_spend_controls |
The x402 client's own spend controls refused before this wrapper's hook ran. |
payment_client_refused |
The x402 client failed after a 402 for another reason. |
request_failed |
The request failed before any 402 (recorded as an error row, not a block). |
createGuardedX402Fetch local caps
Abort reason [twzrd-guarded-fetch] <reason>; these run before the buyer approval above.
| Reason | When |
|---|---|
price_cap_exceeded |
The price is above maxPricePerCall. |
hourly_budget_exceeded |
The payment would take spend in the last hour above hourlyBudgetCap. |
amount_field_conflict, payto_field_conflict |
As in the client hook table. |
recipient_missing |
allowedRecipients is set and the requirement names no recipient. |
unauthorized_recipient |
allowedRecipients is set and payTo is not in it. |
amount_missing_or_malformed |
A spend rule is set and the amount is not a base-unit integer. |
unsupported_or_non_usdc_asset |
A spend rule is set and the asset is not USDC on that network. |
twzrd.safeFetch verdicts
{ verdict: "block", reason }:
| Reason | When |
|---|---|
unparseable_402 |
The 402 has no readable payment requirements. |
network_not_allowed |
No offer is on an allowNetworks network. |
amount_field_conflict, payto_field_conflict |
As above. |
no_payable_requirement |
No offer names both a recipient and an amount. |
malformed_amount |
The amount is not a base-unit integer. |
non_usdc_asset |
On Solana or Base, the offer names an asset that is not USDC (the budget counts micro-USDC). |
over_max_spend, over_cumulative_spend |
The payment, or the running total, would pass maxSpend. |
intel_block |
The injected preflight answered block. |
decision_bind_required |
A signed decision is required and none was bound. |
bind_required_no_compose, bind_required_no_settlement, bind_required_no_leaf_hash, bind_mismatch |
A resource bind was required and could not be built or did not match. |
Config
| Option | Env | Default | Description |
|---|---|---|---|
intelBase |
TWZRD_INTEL_BASE |
https://intel.twzrd.xyz |
Preflight API base |
preflightMinScore |
TWZRD_PREFLIGHT_MIN_SCORE |
40 |
Block below this score |
blockDecisions |
TWZRD_BLOCK_DECISIONS |
block |
Decisions that throw |
failOpen (buyer preflight only) |
TWZRD_FAIL_OPEN |
false |
Buyer outage only: default does not sign. createTwzrdSettleGuard omitted failOpen is the other default — a thrown screen returns without abort |
gateOnCanSpend |
TWZRD_GATE_ON_CAN_SPEND |
false |
Also block when can_spend=false |
refuseUnevaluated |
TWZRD_REFUSE_UNEVALUATED |
false |
Refuse every seller intel has not evaluated, instead of allowing it up to the card's recommended_cap_usdc. true, 1, "yes", "on" in any case turn it on. |
refuseWashUnknown |
TWZRD_REFUSE_WASH_UNKNOWN |
false (0.12.x; true from 0.13.0) |
Refuse (twzrd_wash_unknown) when the merchant card is reachable but its wash coverage is missing, partial or stale. Also refuses an unevaluated seller within its cap when its card has no full coverage. true, 1, "yes", "on" turn it on. |
intelTimeoutMs |
TWZRD_INTEL_TIMEOUT_MS |
2000 |
Deadline for each free intel call (preflight, merchant card). A miss is an outage, decided by failOpen. Paid hops are not cut off mid-payment. |
autoReceipt |
— | false |
Optional V7: auto-buy $0.05 /trust when /quick did not already settle |
x402Fetch |
— | — | x402-capable fetch for autoReceipt |
onReceipt |
— | — | Callback after receipt is captured |
disabled (installTwzrdAutoGate only) |
TWZRD_AUTO_GATE=0/false |
false |
Bypass the guard entirely — payWrap gets the raw, unguarded fetch |
attribution |
TWZRD_ATTRIBUTION_INTEGRATION + TWZRD_ATTRIBUTION_RUN_ID |
— | Opt-in run attribution (see below) |
Gate adoption proof (no-spend harness)
Deterministic install→transcript path for operators (no wallet, no USDC):
npm run adoption-proof -- --integration demo-autogate-proof --run-id 00000000-0000-4000-8000-000000000001
Emits twzrd.gate_adoption_transcript.v1 JSON: block path aborts with signerInvocations: 0, allow path emits decision, attribution headers stamped on mocked preflight. Full acceptance criteria (what counts as EXTERNAL_RUN vs dogfood), and the
harness's own limits: docs/strategy/gate-adoption-operator-proof.md
in this repo — the same path the transcript's acceptanceDoc field cites.
Portable decision receipt: twzrd.payment_decision.v1
A frozen, closed public record of one decision — what the agent saw
(challenge_hash), what it decided (allow | block | warn | unavailable), why
(one code from a closed enum) and an evidence_id — signed with your existing
decision signer and verifiable offline by anyone holding your public key.
No TWZRD call in the verification path. unavailable is a first-class decision
and is never written as block. The record carries no score, secret, raw
authorization/payload, amount or resource URL; the verifier rejects any of them.
import { createLocalDecisionSigner, evaluateIntent } from "twzrd-x402-gate";
import { paymentDecisionRecordFromToken, verifyPaymentDecisionRecord } from "twzrd-x402-gate/payment-decision";
const signer = createLocalDecisionSigner({ keyId: "ops-2026-09" });
const token = await evaluateIntent(intent, { signer, policy });
const record = await paymentDecisionRecordFromToken(token, selectedAccepts, signer);
// relying party, anywhere, later:
const r = verifyPaymentDecisionRecord(record, { publicKeyPem, challenge: selectedAccepts });
r.ok && r.decision; // "block" — or null when rejected
npx twzrd-payment-decision --verify record.json --pubkey issuer.spki.pem [--challenge accepts-entry.json] [--json]
This record is the $0.001 clearance SKU (requirePaymentDecision on the
before-payment hook). The hook will not proceed to sign without an unexpired
allow or warn for this exact challenge_hash. Opt-in — default AutoGate
stays free-refuse. GET /v1/intel/quick/{seller} is a $0.001 score teaser
and is not this SKU (not challenge-bound). Intel does not issue the
record over x402 yet; pass a verified record or an explicit issuerUrl.
Spec (schema, challenge normalization, what is signed, what is forbidden):
docs/payment-decision-v1-spec.md ·
JSON Schema: docs/schemas/twzrd.payment_decision.v1.schema.json ·
test vectors: test/fixtures/payment-decision-v1-vectors.json.
Run attribution (optional, for integration correlation)
When you set attribution, the gate stamps only the TWZRD preflight request (never the
paid /v1/intel/trust call or the resource fetch) with correlation headers:
X-TWZRD-Integration: <integration>
X-TWZRD-Run-Id: <runId>
X-TWZRD-Client: twzrd-x402-gate/<version>
installTwzrdAutoGate((guarded) => wrapFetchWithPayment(guarded, wallet), {
attribution: {
integration: "payai-x402-solana-pr38",
runId: crypto.randomUUID(), // echo this in your transcript / issue comment
},
});
This is correlation evidence, not proof of adoption — the runId is caller-supplied and
spoofable. A run counts as an external execution only when the same runId (1) appears in the
integrator's own transcript, (2) is observed server-side with a real policy decision, and (3)
comes from non-internal lineage. No PII, secret, wallet, or payload is added; both fields must
be set or nothing is stamped.
Compatibility note
Proxied x402 clients (AgentCash's .fetch, ClawRouter :8402): these clients handle
402 internally and return 200. The guard never sees a 402 if it wraps the client's output —
it must wrap the client's input. installTwzrdAutoGate enforces this composition order by
construction: it guards the raw fetch first, then hands the guarded fetch to your payWrap.
If you're composing withTwzrdGuard manually instead, pass the raw (non-paying) fetch to
withTwzrdGuard, then wrap its output in your x402 client — never the reverse. Or call
evaluate_x402_resource explicitly before routing through the proxy.
Why pre-spend, not post-pay
GET /v1/intel/trust/{wallet} is the paid ($0.05 USDC) deep-intel surface — not a gate.
POST /v1/intel/preflight is the free ReadinessCard for the pre-spend decision.
The gate path only ever calls free endpoints — the preflight plus, by default
(refuseWashFlagged: true), the free merchant_card wash check. Paid intel
(quickCheck, autoReceipt) is otherwise opt-in, with one documented
exception (QUICKSTART 3b): on the fetch / payWrap seat, wiring a paying fetch
auto-enables buyer Path A, so a proceeding warn settles the $0.001 quick
tier (GET /v1/intel/quick/{payTo}) through your wallet with no further flag —
opt out with escalateOnWarn: false, requireReceipt: false, or by leaving
x402Fetch unwired. Optional V7 /trust at $0.05 stays on material allow or
explicit autoReceipt. The merchant-facing payment still waits on the free
preflight's verdict.
License
MIT