twzrd-x402-gate 0.9.2 → 0.9.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/QUICKSTART.md +55 -11
  2. package/README.md +291 -57
  3. package/dist/auto-gate.d.ts +5 -2
  4. package/dist/auto-gate.d.ts.map +1 -1
  5. package/dist/auto-gate.js +12 -3
  6. package/dist/auto-gate.js.map +1 -1
  7. package/dist/cloudflare-base.d.ts +49 -0
  8. package/dist/cloudflare-base.d.ts.map +1 -0
  9. package/dist/cloudflare-base.js +113 -0
  10. package/dist/cloudflare-base.js.map +1 -0
  11. package/dist/cloudflare-x402.d.ts +29 -0
  12. package/dist/cloudflare-x402.d.ts.map +1 -0
  13. package/dist/cloudflare-x402.js +31 -0
  14. package/dist/cloudflare-x402.js.map +1 -0
  15. package/dist/config.js +2 -2
  16. package/dist/config.js.map +1 -1
  17. package/dist/decision-token.d.ts +3 -3
  18. package/dist/decision-token.d.ts.map +1 -1
  19. package/dist/decision-token.js +4 -1
  20. package/dist/decision-token.js.map +1 -1
  21. package/dist/doctor.d.ts.map +1 -1
  22. package/dist/doctor.js +24 -2
  23. package/dist/doctor.js.map +1 -1
  24. package/dist/evidence-verify.d.ts +145 -0
  25. package/dist/evidence-verify.d.ts.map +1 -0
  26. package/dist/evidence-verify.js +784 -0
  27. package/dist/evidence-verify.js.map +1 -0
  28. package/dist/foreign-key.d.ts +19 -0
  29. package/dist/foreign-key.d.ts.map +1 -0
  30. package/dist/foreign-key.js +61 -0
  31. package/dist/foreign-key.js.map +1 -0
  32. package/dist/gate.d.ts +0 -13
  33. package/dist/gate.d.ts.map +1 -1
  34. package/dist/gate.js +0 -22
  35. package/dist/gate.js.map +1 -1
  36. package/dist/index.d.ts +13 -5
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +11 -5
  39. package/dist/index.js.map +1 -1
  40. package/dist/merchant-card.d.ts +57 -18
  41. package/dist/merchant-card.d.ts.map +1 -1
  42. package/dist/merchant-card.js +108 -23
  43. package/dist/merchant-card.js.map +1 -1
  44. package/dist/mpp-hook.d.ts +39 -12
  45. package/dist/mpp-hook.d.ts.map +1 -1
  46. package/dist/mpp-hook.js +76 -13
  47. package/dist/mpp-hook.js.map +1 -1
  48. package/dist/paying-fetch.d.ts +15 -0
  49. package/dist/paying-fetch.d.ts.map +1 -0
  50. package/dist/paying-fetch.js +67 -0
  51. package/dist/paying-fetch.js.map +1 -0
  52. package/dist/policy-fetch.d.ts +18 -0
  53. package/dist/policy-fetch.d.ts.map +1 -0
  54. package/dist/policy-fetch.js +77 -0
  55. package/dist/policy-fetch.js.map +1 -0
  56. package/dist/policy-runtime.d.ts +17 -2
  57. package/dist/policy-runtime.d.ts.map +1 -1
  58. package/dist/policy-runtime.js +69 -1
  59. package/dist/policy-runtime.js.map +1 -1
  60. package/dist/policy.d.ts.map +1 -1
  61. package/dist/policy.js +79 -40
  62. package/dist/policy.js.map +1 -1
  63. package/dist/record-leash-settle.d.ts +20 -0
  64. package/dist/record-leash-settle.d.ts.map +1 -0
  65. package/dist/record-leash-settle.js +25 -0
  66. package/dist/record-leash-settle.js.map +1 -0
  67. package/dist/resource-bind.d.ts.map +1 -1
  68. package/dist/resource-bind.js +1 -0
  69. package/dist/resource-bind.js.map +1 -1
  70. package/dist/spend-control.d.ts +19 -0
  71. package/dist/spend-control.d.ts.map +1 -1
  72. package/dist/spend-control.js +125 -56
  73. package/dist/spend-control.js.map +1 -1
  74. package/dist/types.d.ts +3 -3
  75. package/dist/wash-default.d.ts +112 -0
  76. package/dist/wash-default.d.ts.map +1 -0
  77. package/dist/wash-default.js +229 -0
  78. package/dist/wash-default.js.map +1 -0
  79. package/dist/x402-client-hook.d.ts +83 -11
  80. package/dist/x402-client-hook.d.ts.map +1 -1
  81. package/dist/x402-client-hook.js +168 -13
  82. package/dist/x402-client-hook.js.map +1 -1
  83. package/package.json +18 -7
package/QUICKSTART.md CHANGED
@@ -1,17 +1,15 @@
1
1
  # twzrd-x402-gate — Quickstart (15 minutes, 0 USDC)
2
2
 
3
- Refuses payment to a wash-flagged merchant **before your wallet signs**. Free intel, no
4
- API key, no signup, no config — every `TWZRD_*` env var is an optional override. If intel
5
- is unreachable the gate never *invents* a wash flag (the wash check fails open); a failed
6
- preflight blocks the payment by default (`TWZRD_FAIL_OPEN=true` to allow).
3
+ **Product:** replace naked `createX402Client({ wallet })` with a client that
4
+ **refuses wash before sign**. Free `merchant_card`, no API key. Intel down
5
+ **fail-open** (never invents wash). No Path A in the default path.
7
6
 
8
- **Pin:** `twzrd-x402-gate@0.8.18` + stock PayAI client `x402-solana@2.1.0` (official
9
- `beforePayment` seat). `@x402/core` Path E remains supported; refuse script is fallback.
7
+ **Pin:** `twzrd-x402-gate@0.9.3` + `x402-solana@3.0.0`.
10
8
 
11
9
  ## 1. Stock PayAI client (default seat — copy-paste)
12
10
 
13
11
  ```bash
14
- npm install twzrd-x402-gate@0.8.18 x402-solana@2.1.0
12
+ npm install twzrd-x402-gate@0.9.3 x402-solana@3.0.0
15
13
  ```
16
14
 
17
15
  > **ESM-only.** The package ships `import` conditions only — a CommonJS
@@ -19,6 +17,16 @@ npm install twzrd-x402-gate@0.8.18 x402-solana@2.1.0
19
17
  > fails with `ERR_PACKAGE_PATH_NOT_EXPORTED`. Set `"type": "module"` in your
20
18
  > package.json, use `.mjs`, or bundle ESM.
21
19
 
20
+ ```typescript
21
+ import { createX402Client } from "x402-solana";
22
+ import { createTwzrdPayingClient } from "twzrd-x402-gate";
23
+
24
+ // was: createX402Client({ wallet })
25
+ const client = createX402Client(createTwzrdPayingClient({ wallet }));
26
+ ```
27
+
28
+ Or hook-only:
29
+
22
30
  ```typescript
23
31
  import { createX402Client } from "x402-solana";
24
32
  import { createTwzrdBeforePaymentHook } from "twzrd-x402-gate";
@@ -26,17 +34,18 @@ import { createTwzrdBeforePaymentHook } from "twzrd-x402-gate";
26
34
  const client = createX402Client({
27
35
  wallet,
28
36
  network: "solana",
29
- beforePayment: createTwzrdBeforePaymentHook({ refuseWashFlagged: true }),
37
+ beforePayment: createTwzrdBeforePaymentHook(),
30
38
  });
31
39
  ```
32
40
 
33
- Equivalent: `beforePayment: installTwzrdAutoGate("x402-solana", { refuseWashFlagged: true })`.
41
+ Default engine = wash only. Full 0.8 preflight/Path A: `{ engine: "full" }`.
42
+
34
43
  Then: `await client.fetch("https://merchant.example/paid");`
35
44
 
36
45
  ## 1b. Official `@x402/core` client (Path E)
37
46
 
38
47
  ```bash
39
- npm install twzrd-x402-gate@0.8.18 @x402/core @x402/fetch @x402/svm
48
+ npm install twzrd-x402-gate@0.9.3 @x402/core @x402/fetch @x402/svm
40
49
  ```
41
50
 
42
51
  ```typescript
@@ -53,7 +62,7 @@ installTwzrdAutoGate(client, { refuseWashFlagged: true });
53
62
 
54
63
  ```bash
55
64
  # Stock-client seat proof (beforePayment, signer_invocation_count=0)
56
- # needs x402-solana@2.1.0 installed - the harness fallback runs but does not
65
+ # needs x402-solana@3.0.0 installed - the harness fallback runs but does not
57
66
  # clear closure (its negative arm counts a would-be sign)
58
67
  npm run x402-solana-before-payment-proof
59
68
  # writes block-proof-<run_id>.json — hook: beforePayment
@@ -124,3 +133,38 @@ Everything else (thresholds, wash caps, fail posture, settle guard): [README](./
124
133
  allowed, observed) unless you set `TWZRD_UNSUPPORTED_NETWORK_MODE=strict`.
125
134
  - **No delivery guarantee.** It screens the merchant before you pay, nothing after.
126
135
  - **Not a wallet.** It never holds keys or signs — it only decides if your signer runs.
136
+
137
+
138
+ ## Pay Kit before-payment hook (upstream PR pending)
139
+
140
+ `solana-foundation/pay-kit#303` is open as of 2026-09-07. This adapter targets
141
+ that PR's `onBeforeX402PaymentCreation` option; a released Pay Kit client must
142
+ expose that option before this integration can enforce policy.
143
+
144
+ ```ts
145
+ import { createPayKitClient } from "@solana/pay-kit";
146
+ import { installTwzrdAutoGate } from "twzrd-x402-gate";
147
+
148
+ const client = await createPayKitClient({
149
+ accept: ["x402"],
150
+ rpcUrl,
151
+ signer,
152
+ onBeforeX402PaymentCreation: installTwzrdAutoGate("pay-kit", {
153
+ refuseWashFlagged: true,
154
+ }),
155
+ });
156
+ ```
157
+
158
+ The adapter returns the official x402 context hook and reuses the shared
159
+ before-payment evaluator. It adds no Pay Kit dependency. Supply it at client
160
+ construction; passing the constructed Pay Kit client to AutoGate is unsupported.
161
+ This hook covers x402 payment creation, not Pay Kit's MPP path.
162
+
163
+ `createTwzrdPayKitBeforePaymentHook(options)` exposes the same evaluator directly.
164
+ The AutoGate form reads environment kill switches on every call; the direct
165
+ factory does not. Calling AutoGate installs protection by default, whereas
166
+ ClawRouter's integration loads AutoGate only after explicit opt-in.
167
+
168
+ Source-checkout verification: `npx tsx test/pay-kit-before-payment.test.ts`.
169
+ The test covers a fake Pay Kit host and the real x402 client with a fake scheme;
170
+ it does not establish released Pay Kit compatibility or external use.
package/README.md CHANGED
@@ -1,35 +1,72 @@
1
1
  # twzrd-x402-gate
2
2
 
3
- **TWZRD is the spend-control SDK for agents paying over x402** and the default
4
- `onBeforePaymentCreation` policy engine for official x402 clients.
3
+ **Product: the paying client that already has the brake.**
5
4
 
6
- ### One call (named export `twzrd`)
5
+ Agents pay other machines unsupervised. Most 402s are junk or wash. Something has
6
+ to say **no** before the signature. TWZRD is that no — inside `beforePayment` /
7
+ fetch+sign, not inside PayAI’s SDK defaults, not as a shop at intel.twzrd.xyz.
7
8
 
8
- ```js
9
- import { twzrd } from "twzrd-x402-gate";
9
+ The other reason the no must live at the signature: **attaching a funded wallet
10
+ to an agent that reads untrusted web content is a drain vector** — indirect
11
+ prompt injection can spend anything the agent can sign. The gate closes it by
12
+ attaching the wallet to a policy runtime the injected content cannot
13
+ renegotiate: per-request cap, rolling daily ceiling, wash brake, endpoint
14
+ mandates, and a tamper-evident spend ledger that survives restarts. A fully
15
+ compromised agent spends at most what policy allows, to whom it allows, with
16
+ every decision signed.
10
17
 
11
- const result = await twzrd.safeFetch(url, {
12
- maxSpend: "0.10", // per-call cap AND cumulative budget
13
- allowNetworks: ["solana", "base"],
14
- requireOfferBinding: true, // demand a chain-verifiable bind-v1 receipt
15
- pay, // your wallet signs — this SDK never holds keys
18
+ ### Replace naked `createX402Client({ wallet })` (5 lines)
19
+
20
+ ```bash
21
+ npm install twzrd-x402-gate@0.9.3 x402-solana@3.0.0
22
+ ```
23
+
24
+ ```typescript
25
+ import { createX402Client } from "x402-solana";
26
+ import { createTwzrdPayingClient } from "twzrd-x402-gate";
27
+
28
+ // was: createX402Client({ wallet })
29
+ const client = createX402Client(createTwzrdPayingClient({ wallet }));
30
+ ```
31
+
32
+ Equivalent one-hook form:
33
+
34
+ ```typescript
35
+ import { createX402Client } from "x402-solana";
36
+ import { createTwzrdBeforePaymentHook } from "twzrd-x402-gate";
37
+
38
+ const client = createX402Client({
39
+ wallet,
40
+ network: "solana",
41
+ beforePayment: createTwzrdBeforePaymentHook(), // wash default
16
42
  });
17
- // result.verdict: "allow" | "warn" | "block" — blocks have signerInvocations === 0
18
- // result.receipt: { strength: "hard"|"soft"|"refuse", leaf_hash, fact_type: "resource_bound" }
19
43
  ```
20
44
 
21
- Full walkthrough: [QUICKSTART.md](https://github.com/twzrd-sol/twzrd-trust/blob/main/QUICKSTART.md) ·
22
- verify receipts yourself: [REVIEW.md](https://github.com/twzrd-sol/twzrd-trust/blob/main/REVIEW.md)
45
+ **Default path (engine `"wash"`):**
46
+ `GET /v1/intel/merchant_card/{payTo}` → abort **iff** `wash_flagged === true` →
47
+ **fail-open** on timeout / non-2xx / throw (never invent wash).
48
+ No Path A, no requireReceipt, no second 402, no payment-control tokens.
23
49
 
24
- **Core product (buyer gate):** after the client selects the exact payment requirement and
25
- **before** payment payload creation / wallet signing — free preflight + merchant_card wash
26
- refuse. Protects the **payer** from a risky **merchant** (`payTo`). Chain-neutral envelope;
27
- **Solana-deep** reputation only (Base/EVM = explicit `unknown`).
50
+ **Full 0.8.x engine (named opt-in):** preflight + optional Path A / escalate /
51
+ paymentControl:
28
52
 
29
- ### Default-on AutoGate (5 lines)
53
+ ```typescript
54
+ createTwzrdBeforePaymentHook({ engine: "full", /* … */ })
55
+ // or createTwzrdFullBeforePaymentHook({ … })
56
+ ```
57
+
58
+ Customer = whoever ships `createX402Client({ wallet })` with no hook (Eliza, MCP
59
+ hosts, agent wallets). Crawlers will never `npm install`.
60
+
61
+ > **ESM-only:** CommonJS `require()` fails with `ERR_PACKAGE_PATH_NOT_EXPORTED`
62
+ > — set `"type": "module"` (or use `.mjs` / an ESM bundler).
63
+
64
+ ### Default-on AutoGate (alternate seats)
30
65
 
31
66
  ```bash
32
- npm install twzrd-x402-gate @x402/core @x402/fetch @x402/svm
67
+ npm install twzrd-x402-gate@0.9.3 x402-solana@3.0.0
68
+ # official @x402/* path (alternate seat):
69
+ # npm install twzrd-x402-gate@0.9.3 @x402/core @x402/fetch @x402/svm
33
70
  ```
34
71
 
35
72
  ```typescript
@@ -37,26 +74,28 @@ import { x402Client } from "@x402/core/client";
37
74
  import { installTwzrdAutoGate } from "twzrd-x402-gate";
38
75
 
39
76
  const client = new x402Client();
40
- // refuseWashFlagged defaults true; gateOnCanSpend stays false unless you opt in
77
+ // stock solana seat still defaults to wash via createTwzrdBeforePaymentHook()
41
78
  installTwzrdAutoGate(client, { refuseWashFlagged: true });
42
79
  // then register schemes + wrapFetchWithPayment as usual
43
80
  ```
44
81
 
45
- **Intercept proof (0 USDC, wash seller never reaches signer):**
82
+ **Intercept proof (0 USDC, bad seller never reaches signer):**
46
83
 
47
84
  ```bash
48
85
  cd packages/twzrd-x402-gate && npm run autogate-block-proof
49
86
  # writes block-proof-<run_id>.json (schema twzrd.autogate_block_proof.v1)
50
- # public reason: TWZRD_TRUST_GATE_BLOCK: wash_flagged
87
+ # public reason: "TWZRD_TRUST_GATE_BLOCK: wash_flagged" (wash basis)
88
+ # or "TWZRD_TRUST_GATE_BLOCK: decision_block" (readiness block basis)
89
+ # fixture resolves live: wash_flagged seller if available, else decision=block
51
90
  ```
52
91
 
53
- `gateOnCanSpend` remains **opt-in** (`false` by default; set `true` or `TWZRD_GATE_ON_CAN_SPEND=1` only when you want hard cap enforcement).
92
+ `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.
54
93
 
55
- **Optional (0.8.1):** a resource-server settle hook so merchants can apply **customer policy**
94
+ **Optional:** a resource-server settle hook so merchants can apply **customer policy**
56
95
  before they settle and serve (abuse, sanctions, bots, “don’t serve this payer”). Not an equal
57
96
  mirror of the buyer problem — settled USDC is final; wash resistance is mainly TWZRD scoring.
58
97
 
59
- ## Seller settle guard (`onBeforeSettle`) — optional 0.8.1
98
+ ## Seller settle guard (`onBeforeSettle`) — optional
60
99
 
61
100
  Resource servers can screen the **payer** before *they* settle an inbound payment and serve
62
101
  the resource. Use for merchant policy (abuse / sanctions / bots / customer selection). TWZRD is
@@ -68,7 +107,7 @@ transfer on-chain. Wash/sybil edges are primarily discounted in TWZRD scoring, n
68
107
  revenue refusal.
69
108
 
70
109
  ```bash
71
- npm install twzrd-x402-gate@0.8.18
110
+ npm install twzrd-x402-gate@0.9.3
72
111
  ```
73
112
 
74
113
  ```typescript
@@ -100,7 +139,23 @@ Offline demo: `npx tsx examples/seller-settle-guard.ts`
100
139
  Fixture-backed SVM extract tests live in `test/seller-hook.test.ts` +
101
140
  `test/fixtures/exact-svm-transfer-checked.ts`.
102
141
 
103
- **PayAI agentic-payments** (the active PayAI SDK, not the dormant x402-solana):
142
+ **PayAI x402-solana@2.1.0+** (stock client - primary Solana seat via `beforePayment`).
143
+ Both 2.1.0 and 3.0.0 are supported and produce an identical refuse transcript; 3.0.0
144
+ is the current release and what the install lines pin. The hook absorbs the one
145
+ breaking change between them (`declaredResource` is a string on 2.1.0, `{ url }` on
146
+ 3.0.0) - see `src/x402-client-hook.ts` and `test/x402-solana-before-payment.test.ts`.
147
+
148
+ ```typescript
149
+ import { createX402Client } from "x402-solana";
150
+ import { createTwzrdPayingClient } from "twzrd-x402-gate";
151
+
152
+ const client = createX402Client(createTwzrdPayingClient({ wallet }));
153
+ ```
154
+
155
+ Also: `installTwzrdAutoGate("x402-solana", opts)` returns the same wash-default hook.
156
+ Prove refuse-before-sign: `npm run x402-solana-before-payment-proof`.
157
+
158
+ **PayAI agentic-payments** (alternate PayAI surface; prefer x402-solana@3.0.0 when available):
104
159
  `npx tsx examples/payai-agentic-onPaymentVerified.ts` — wire `onPaymentVerified`
105
160
  → `toPayaiVerifyResult` to screen payers before serving. Fail-open by default.
106
161
 
@@ -112,7 +167,7 @@ Fixture-backed SVM extract tests live in `test/seller-hook.test.ts` +
112
167
  Install the published gate and run against wash fixtures:
113
168
 
114
169
  ```bash
115
- npm install twzrd-x402-gate@0.8.18
170
+ npm install twzrd-x402-gate@0.9.3
116
171
  # from package root after install, or from a checkout:
117
172
  npm run wash-dogfood
118
173
  ```
@@ -132,7 +187,7 @@ Gate dogfood: `approved=false` `reason=twzrd_decision_block`, **USDC spent = 0**
132
187
 
133
188
  **Accurate paste line:**
134
189
 
135
- > Preflight returned decision=block on wash seller 7G73PL… / HuSiSpc… (preflight_id 378468 / 378469, wash_flagged=true). Gate approved=false reason=twzrd_decision_block. No USDC spent. No tx broadcast. Repro: `npm i twzrd-x402-gate@0.8.5 && npm run wash-dogfood` or gist above.
190
+ > 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.
136
191
 
137
192
  This is a **reproducible demonstration** that the free gate blocks known wash sellers with stamped `preflight_id`s and zero spend. It is **not** proof that external agents already default to this path at scale.
138
193
 
@@ -210,11 +265,61 @@ No AgentCash. No marketplace. No second probe. No TWZRD custody.
210
265
  Wire `twzrdOnPaymentRequested` / prefer `onPaymentRequired` + `onBeforePayment` per
211
266
  [lifecycle hooks](https://docs.x402.org/advanced-concepts/lifecycle-hooks). Same policy core.
212
267
 
268
+ ### Cloudflare Agents x402 (Base)
269
+
270
+ Cloudflare's `withX402Client` takes an `onPaymentRequired` callback before its
271
+ automatic payment retry. Use the adapter below at that boundary:
272
+
273
+ ```ts
274
+ import { createTwzrdCloudflareX402Approval } from "twzrd-x402-gate";
275
+
276
+ const approvePayment = createTwzrdCloudflareX402Approval({
277
+ // Base/EVM has no TWZRD behavioral reputation yet. Refuse it rather than
278
+ // presenting a policy allow as a trust verdict.
279
+ unsupportedNetworkMode: "strict",
280
+ });
281
+
282
+ await this.x402Client.callTool(approvePayment, {
283
+ name: "paid_tool",
284
+ arguments: {},
285
+ });
286
+ ```
287
+
288
+ `observe` is available for an agent that intentionally permits unscored Base
289
+ payments, but it returns policy allow with `decision=unknown`; it is never a
290
+ TWZRD reputation approval. This adapter protects the Cloudflare x402 path only.
291
+ MPP is a separate protocol and belongs on its own `onChallenge` /
292
+ `onPaymentRequired` control path.
293
+
294
+ ### Cloudflare Worker / Base x402 (edge-safe preflight)
295
+
296
+ For a Worker, import the dedicated subpath rather than the package root. It has
297
+ no Node-native imports and sends the exact Base `payTo` plus `chain_id: 8453` to
298
+ `/v1/intel/preflight` before a Viem account signs:
299
+
300
+ ```ts
301
+ import { withTwzrdBasePreflight } from "twzrd-x402-gate/cloudflare-base";
302
+
303
+ const signature = await withTwzrdBasePreflight(
304
+ paymentRequirements, // accepts: [{ network: "eip155:8453", payTo: "0x..." }]
305
+ { intelBase: "https://intel.twzrd.xyz" },
306
+ () => account.signTypedData(eip3009Authorization),
307
+ );
308
+ ```
309
+
310
+ `block` throws `TwzrdBasePaymentBlockedError` and never invokes the signing
311
+ callback. `allow` and `warn` both proceed; enforce any additional amount or
312
+ mandate policy in the caller. Base is not presented as reputation-scored.
313
+ Checksummed and lowercase EVM addresses are accepted unchanged. The fixture in
314
+ [`test/cloudflare-base-edge.test.ts`](./test/cloudflare-base-edge.test.ts) proves
315
+ `block → zero signTypedData calls` and the compiled subpath is checked for Node
316
+ runtime leaks.
317
+
213
318
  ### Raw-fetch composition (injectible pay client only)
214
319
 
215
320
  ```typescript
216
321
  import { installTwzrdAutoGate } from "twzrd-x402-gate";
217
- import { wrapFetchWithPayment } from "@x402/fetch";
322
+ import { wrapFetchWithPayment } from "@x402/fetch"; // or @x402/svm helper
218
323
 
219
324
  // Guard RAW fetch, then hand to a client that still surfaces 402 to the guard layer
220
325
  // — OR installTwzrdAutoGate(x402Client) (canonical) / installTwzrdX402ClientHook alias.
@@ -236,15 +341,16 @@ Canonical path for every agent that spends USDC on Solana x402:
236
341
 
237
342
  **Fail-open (no invent):**
238
343
  - Preflight HTTP/network error → default fail-closed in gate 0.2+ (`TWZRD_FAIL_OPEN=true` restores legacy allow-on-outage).
239
- - Merchant card unreachable / non-2xx / missing `wash_flagged` `washFlagged=null` → **do not refuse on wash** (preflight decision stands).
240
- - Only a successful card with `wash_flagged: true` triggers wash refuse or soft cap.
344
+ - Merchant card unreachable / non-2xx / timeoutfail-open (do not invent wash).
345
+ - 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`.
241
346
 
242
347
  **Wash policy (exact):**
243
348
  - Prior preflight deny → unchanged (wash never loosens a block).
244
- - `refuseWashFlagged=false` or wash not true → keep preflight approval.
349
+ - `refuseWashFlagged=false` → keep prior approval (opt out).
245
350
  - `wash_flagged=true` + no cap → `approved=false`, `reason=twzrd_wash_flagged`, `verdict=block`.
246
351
  - `wash_flagged=true` + `washMaxUsdc` set + `priceUsdc <= cap` → allow, `washCapped=true`, reason `twzrd_wash_capped_{price}_le_{cap}`.
247
352
  - `wash_flagged=true` + price above cap (or price unknown) → refuse with `twzrd_wash_flagged_above_cap_*`.
353
+ - 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).
248
354
 
249
355
  **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.
250
356
 
@@ -257,11 +363,13 @@ Dogfood (one public live proof path):
257
363
  ## Install
258
364
 
259
365
  ```bash
260
- npm install twzrd-x402-gate@0.8.18
366
+ npm install twzrd-x402-gate@0.9.3 x402-solana@3.0.0
261
367
  ```
262
368
 
263
- Do not hardcode a version in this doc — every past pin here (**0.5.4**, **0.7.1**, **0.8.5**,
264
- **0.8.6**) has gone stale. Check `npm view twzrd-x402-gate version` if in doubt.
369
+ Install pin is the published version (`GATE_PACKAGE_PIN` in
370
+ `packages/twzrd-agent-intel/src/twzrd_agent_intel/pins.py`). Repo `package.json`
371
+ may be one patch ahead until that release is on npm. Confirm with
372
+ `npm view twzrd-x402-gate version`.
265
373
 
266
374
  Optional settle guard (resource-server **payer** policy): see **Seller settle guard
267
375
  (`onBeforeSettle`) — optional 0.8.1** above. Do not confuse with facilitator
@@ -355,15 +463,16 @@ installTwzrdX402ClientHook(client, {
355
463
  - Hook binding test:
356
464
  [`test/intent-binding.test.ts`](./test/intent-binding.test.ts).
357
465
 
358
- ### On MPP (Machine Payments Protocol) — Solana charge only
466
+ ### On MPP (Machine Payments Protocol) — Solana and narrow Base EVM charge
359
467
 
360
- `createTwzrdMppOnChallenge` guards `Mppx.create({ onChallenge })`. The mppx
361
- Solana method signs AND broadcasts the transaction inside `createCredential()`,
362
- so `onChallenge` is the last deterministic checkpoint before money moves - and
363
- mppx re-throws `onChallenge` errors, so a TWZRD block is an exception that means
364
- **`createCredential()` never runs and nothing signs**. On allow, the guard
365
- creates the credential for the exact challenge it evaluated; non-`solana/charge`
366
- challenges fail closed (`allowUnevaluated: true` to opt out).
468
+ `createTwzrdMppOnChallenge` guards `Mppx.create({ onChallenge })`. It supports
469
+ `solana/charge` and the published native MPP `evm/charge` shape only for
470
+ Base-mainnet native USDC (EIP-3009 authorization). `onChallenge` is the last
471
+ deterministic checkpoint before `createCredential()` makes the credential, so a
472
+ TWZRD block is an exception that means **`createCredential()` never runs and
473
+ nothing signs**. On allow, the guard creates the credential for the exact
474
+ challenge it evaluated; other methods/intents fail closed
475
+ (`allowUnevaluated: true` is an explicit ungated opt-out).
367
476
 
368
477
  ```typescript
369
478
  import { Mppx } from "mppx/client";
@@ -379,6 +488,30 @@ const mppx = Mppx.create({
379
488
  });
380
489
  ```
381
490
 
491
+ For Base, use mppx's EVM client with explicit local spend policy:
492
+
493
+ ```typescript
494
+ import { Mppx } from "mppx/client";
495
+ import { evm } from "mppx/evm/client";
496
+ import { createTwzrdMppOnChallenge, createLocalDecisionSigner } from "twzrd-x402-gate";
497
+
498
+ const mppx = Mppx.create({
499
+ methods: [evm({ account: wallet })],
500
+ onChallenge: createTwzrdMppOnChallenge({
501
+ signer: createLocalDecisionSigner(),
502
+ policy: {
503
+ allowedNetworks: ["eip155:8453"],
504
+ allowedAssets: ["0x833589fCD6EDb6E08f4c7C32D4f71b54bdA02913"],
505
+ maxAmountUsd: "1.00",
506
+ },
507
+ }),
508
+ });
509
+ ```
510
+
511
+ Base is **not reputation-scored**. The EVM path provides an actual pre-credential
512
+ local control point for Base USDC; it does not call a Solana-derived score or
513
+ present a local policy allow as a TWZRD trust verdict.
514
+
382
515
  **Scope limit (honest):** the guard is authoritative only when no
383
516
  `onChallengeReceived` event handler supplies a credential. mppx resolves
384
517
  `eventCredential ?? onChallenge(...)`, so an event handler returning a credential
@@ -394,6 +527,8 @@ fails closed rather than approve a payment it cannot bind:
394
527
  | 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. |
395
528
  | 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. |
396
529
  | Misdeclared decimals | `MALFORMED_CHALLENGE` | A known stablecoin declaring the wrong decimals is a discount attempt, not a rounding error. |
530
+ | 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. |
531
+ | 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. |
397
532
 
398
533
  The intent binds a **digest of the entire normalized challenge**, not just
399
534
  `realm:id` - swapping `recipient` or `amount` under the same challenge id changes
@@ -446,8 +581,10 @@ Canonical entry point (design: `docs/strategy/install-autogate-design.md`). One
446
581
  | Call | Adapter |
447
582
  |------|---------|
448
583
  | `installTwzrdAutoGate(payWrap, opts?)` | Fetch: guard raw fetch → pay client |
449
- | `installTwzrdAutoGate(x402Client, opts?)` | Official x402 `onBeforePaymentCreation` |
584
+ | `installTwzrdAutoGate(x402Client, opts?)` | Official `@x402/core` `onBeforePaymentCreation` |
585
+ | `installTwzrdAutoGate("x402-solana", opts?)` | PayAI stock client `beforePayment` (2.1.0+) |
450
586
  | `installTwzrdAutoGate("mpp", opts)` | MPP `onChallenge` (returns handler) |
587
+ | `createTwzrdBeforePaymentHook(opts?)` | Same as `"x402-solana"` — pass to `createX402Client` |
451
588
 
452
589
  Aliases: `installTwzrdX402ClientHook`, `createTwzrdMppOnChallenge` remain; docs prefer AutoGate.
453
590
  Kill switch: `TWZRD_GATE_ENABLED=false` or `TWZRD_AUTO_GATE=0`. Uninstall x402 installs with `uninstallTwzrdAutoGate(client)`.
@@ -460,7 +597,7 @@ ever gets a chance to sign.
460
597
 
461
598
  ```typescript
462
599
  import { installTwzrdAutoGate } from "twzrd-x402-gate";
463
- import { wrapFetchWithPayment } from "@x402/fetch";
600
+ import { wrapFetchWithPayment } from "@x402/svm";
464
601
 
465
602
  const payingFetch = installTwzrdAutoGate((guarded) => wrapFetchWithPayment(guarded, buyerWallet));
466
603
 
@@ -511,7 +648,7 @@ manage the raw/paying composition yourself:
511
648
 
512
649
  ```typescript
513
650
  import { withTwzrdGuard } from "twzrd-x402-gate";
514
- import { wrapFetchWithPayment } from "@x402/fetch";
651
+ import { wrapFetchWithPayment } from "@x402/svm";
515
652
 
516
653
  const raw = globalThis.fetch; // MUST still surface HTTP 402
517
654
  const guarded = withTwzrdGuard(raw); // guard sits upstream
@@ -546,15 +683,49 @@ const safeFetch = withTwzrdGuard(x402Fetch, {
546
683
  });
547
684
  ```
548
685
 
549
- `autoReceipt` is **off by default** it spends the **buyer's** USDC, so you opt in. When on,
550
- every warn/allow verdict settles $0.05 USDC to TWZRD and returns a signed V6 trust credential
551
- for the counterparty before you pay the resource.
686
+ ### Path A default on the buyer seat when a paying fetch is wired
687
+
688
+ Free preflight still decides `allow|warn|block`. **Facilitator `onBeforeSettle`
689
+ stays free** (abort on `block` only — we do not tax their rail).
690
+
691
+ On the **buyer** install, if you pass `x402Fetch` (or use
692
+ `installTwzrdAutoGate(payWrap)`, which auto-wires `payWrap(raw)`), Path A
693
+ defaults on:
694
+
695
+ | Free decision | Resource price | What fires |
696
+ |---------------|----------------|------------|
697
+ | `block` | any | free refuse |
698
+ | `warn` | ≥ $2.50 | $0.05 V6 (`requireReceipt`) |
699
+ | `warn` | < $2.50 | $0.001 quick re-decide (`escalateOnWarn`) |
700
+ | `allow` | > $2.50 | $0.05 V6 |
701
+ | `allow` | ≤ $2.50 | free proceed |
702
+
703
+ ```typescript
704
+ import { wrapFetchWithPayment } from "@x402/svm";
705
+ import { createTwzrdBeforePaymentHook } from "twzrd-x402-gate";
706
+
707
+ const x402Fetch = wrapFetchWithPayment(fetch, buyerWallet);
708
+ const client = createX402Client({
709
+ wallet: buyerWallet,
710
+ network: "solana",
711
+ beforePayment: createTwzrdBeforePaymentHook({
712
+ refuseWashFlagged: true,
713
+ x402Fetch, // turns Path A defaults on
714
+ }),
715
+ });
716
+ ```
717
+
718
+ Opt out: `requireReceipt: false` and/or `escalateOnWarn: false`.
719
+ Refuse-only seats (no `x402Fetch`) stay free.
720
+
721
+ `evaluate_x402_resource` itself stays opt-in. `autoReceipt: true` still buys
722
+ $0.05 on every non-block (broader than the default ladder).
552
723
 
553
724
  **`x402Fetch` is yours to supply** (this package is dependency-free). Wire the proven
554
725
  `@x402/svm` sponsored-feePayer client — the same one `twzrd-mcp-server` uses:
555
726
 
556
727
  ```typescript
557
- import { wrapFetchWithPayment } from "@x402/fetch";
728
+ import { wrapFetchWithPayment } from "@x402/svm";
558
729
  const x402Fetch = wrapFetchWithPayment(fetch, buyerWallet); // settles 402 challenges
559
730
  ```
560
731
 
@@ -746,17 +917,32 @@ const gatedFetch = wrapFetchWithTwzrdGate(fetch, resolveConfig());
746
917
 
747
918
  A payment is **blocked** when:
748
919
  1. `decision ∈ blockDecisions` (default: `["block"]`)
749
- 2. `trust_score < preflightMinScore` (default: `40`)
920
+ 2. `trust_score < preflightMinScore` (default: `40`) — note a card with **no**
921
+ `trust_score` is scored `0` (`policy.ts`: `card.trust_score ?? 0`), so a
922
+ missing score blocks under the default floor
750
923
  3. `can_spend === false` — **only** when `gateOnCanSpend: true` (default `false`, opt-in)
924
+ 4. `merchant_card.wash_flagged === true` — **on by default**
925
+ (`refuseWashFlagged`, `TWZRD_REFUSE_WASH_FLAGGED`); forces `verdict: "block"`
926
+ with `reason: twzrd_wash_flagged`. A *returned* card with `wash_flagged: null`,
927
+ missing/`base_2cycle` coverage, or `ring_evaluated: false` is unknown ≠ clean
928
+ (`twzrd_wash_unknown`). Unreachable card still fail-opens (no invent).
929
+ Soft-cap instead of hard refuse with `washMaxUsdc` / `TWZRD_WASH_MAX_USDC`.
751
930
 
752
931
  `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.
753
932
 
754
933
  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.
755
934
 
756
935
  > **`can_spend` note:** the free preflight returns `can_spend=false` for most sellers
757
- > not yet in the TWZRD corpus, including legitimate ones. The default is decision-only
758
- > gating so unknown sellers on platforms like Agentic.Market are not blocked by default.
759
- > Set `gateOnCanSpend: true` for strict mode.
936
+ > not yet in the TWZRD corpus, including legitimate ones. That **`can_spend` leg
937
+ > alone** is opt-in it is ignored unless you set `gateOnCanSpend: true` — so an
938
+ > unknown seller on a platform like Agentic.Market is not blocked *on that basis*.
939
+ >
940
+ > This is **not** a blanket "unknown sellers are allowed by default". Conditions 1,
941
+ > 2 and 4 above are all active by default, and condition 2 is the one that catches
942
+ > unknown sellers: a card carrying no `trust_score` evaluates as `0` and blocks
943
+ > under the default `preflightMinScore: 40` with `twzrd_score_0_below_40`. To
944
+ > actually let unscored sellers through, lower or disable `preflightMinScore` —
945
+ > setting `gateOnCanSpend` does not govern it.
760
946
 
761
947
  ## Config
762
948
 
@@ -767,11 +953,24 @@ A 402 whose payment requirements yield **no identifiable seller wallet** (missin
767
953
  | `blockDecisions` | `TWZRD_BLOCK_DECISIONS` | `block` | Decisions that throw |
768
954
  | `failOpen` | `TWZRD_FAIL_OPEN` | `false` | `true` opts into legacy allow-on-outage; default blocks (fail-closed) |
769
955
  | `gateOnCanSpend` | `TWZRD_GATE_ON_CAN_SPEND` | `false` | Also block when `can_spend=false` |
956
+ | `refuseWashFlagged` | `TWZRD_REFUSE_WASH_FLAGGED` | **`true`** | Refuse when `merchant_card.wash_flagged === true` (Policy 4). Set `false` / `0` to opt out |
957
+ | `washMaxUsdc` | `TWZRD_WASH_MAX_USDC` | — | Soft cap instead of hard refuse on wash: allow up to this USDC amount |
958
+ | `unsupportedNetworkMode` | `TWZRD_UNSUPPORTED_NETWORK_MODE` | `observe` | Behavior on a non-Solana / unrecognized network |
959
+ | `fetch` | — | global `fetch` | Injected fetch used for preflight + merchant card (testing / custom agents) |
960
+ | `onWarnUpsell` | — | — | Callback fired on a `warn` verdict (Path A upsell hook) |
961
+ | `attribution` | `TWZRD_ATTRIBUTION_INTEGRATION` + `TWZRD_ATTRIBUTION_RUN_ID` | — | Opt-in run attribution (see below) |
962
+
963
+ The four options below are **not** `TwzrdGateConfig` fields — they belong to
964
+ `TwzrdGuardOptions` / `InstallAutoGateOptions` (`withTwzrdGuard` /
965
+ `installTwzrdAutoGate`). Passing them to `createTwzrdGate({...})` is a type error
966
+ and is silently dropped at runtime:
967
+
968
+ | Option | Env | Default | Description |
969
+ |---|---|---|---|
770
970
  | `autoReceipt` | — | `false` | Auto-buy $0.05 TWZRD receipt on warn/allow |
771
971
  | `x402Fetch` | — | — | x402-capable fetch for `autoReceipt` |
772
972
  | `onReceipt` | — | — | Callback after receipt is captured |
773
973
  | `disabled` (`installTwzrdAutoGate` only) | `TWZRD_AUTO_GATE=0`/`false` | `false` | Bypass the guard entirely — `payWrap` gets the raw, unguarded fetch |
774
- | `attribution` | `TWZRD_ATTRIBUTION_INTEGRATION` + `TWZRD_ATTRIBUTION_RUN_ID` | — | Opt-in run attribution (see below) |
775
974
 
776
975
  ## Gate adoption proof (no-spend harness)
777
976
 
@@ -831,3 +1030,38 @@ whether to proceed before any USDC leaves your wallet.
831
1030
  ## License
832
1031
 
833
1032
  MIT
1033
+
1034
+
1035
+ ## Pay Kit before-payment hook (upstream PR pending)
1036
+
1037
+ `solana-foundation/pay-kit#303` is open as of 2026-09-07. This adapter targets
1038
+ that PR's `onBeforeX402PaymentCreation` option; a released Pay Kit client must
1039
+ expose that option before this integration can enforce policy.
1040
+
1041
+ ```ts
1042
+ import { createPayKitClient } from "@solana/pay-kit";
1043
+ import { installTwzrdAutoGate } from "twzrd-x402-gate";
1044
+
1045
+ const client = await createPayKitClient({
1046
+ accept: ["x402"],
1047
+ rpcUrl,
1048
+ signer,
1049
+ onBeforeX402PaymentCreation: installTwzrdAutoGate("pay-kit", {
1050
+ refuseWashFlagged: true,
1051
+ }),
1052
+ });
1053
+ ```
1054
+
1055
+ The adapter returns the official x402 context hook and reuses the shared
1056
+ before-payment evaluator. It adds no Pay Kit dependency. Supply it at client
1057
+ construction; passing the constructed Pay Kit client to AutoGate is unsupported.
1058
+ This hook covers x402 payment creation, not Pay Kit's MPP path.
1059
+
1060
+ `createTwzrdPayKitBeforePaymentHook(options)` exposes the same evaluator directly.
1061
+ The AutoGate form reads environment kill switches on every call; the direct
1062
+ factory does not. Calling AutoGate installs protection by default, whereas
1063
+ ClawRouter's integration loads AutoGate only after explicit opt-in.
1064
+
1065
+ Source-checkout verification: `npx tsx test/pay-kit-before-payment.test.ts`.
1066
+ The test covers a fake Pay Kit host and the real x402 client with a fake scheme;
1067
+ it does not establish released Pay Kit compatibility or external use.