x402trace 0.1.0

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 (62) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/LICENSE +201 -0
  3. package/README.md +121 -0
  4. package/dist/chain/abi.d.ts +48 -0
  5. package/dist/chain/abi.js +34 -0
  6. package/dist/chain/client.d.ts +6 -0
  7. package/dist/chain/client.js +283 -0
  8. package/dist/chain/index.d.ts +4 -0
  9. package/dist/chain/index.js +4 -0
  10. package/dist/chain/retry.d.ts +18 -0
  11. package/dist/chain/retry.js +37 -0
  12. package/dist/chain/types.d.ts +94 -0
  13. package/dist/chain/types.js +12 -0
  14. package/dist/cli/color.d.ts +30 -0
  15. package/dist/cli/color.js +26 -0
  16. package/dist/cli/exit-codes.d.ts +12 -0
  17. package/dist/cli/exit-codes.js +11 -0
  18. package/dist/cli/format-result.d.ts +20 -0
  19. package/dist/cli/format-result.js +58 -0
  20. package/dist/cli/index.d.ts +26 -0
  21. package/dist/cli/index.js +130 -0
  22. package/dist/cli/inspect-command.d.ts +28 -0
  23. package/dist/cli/inspect-command.js +63 -0
  24. package/dist/cli/proxy-command.d.ts +74 -0
  25. package/dist/cli/proxy-command.js +197 -0
  26. package/dist/cli/replay.d.ts +44 -0
  27. package/dist/cli/replay.js +193 -0
  28. package/dist/cli.d.ts +2 -0
  29. package/dist/cli.js +20 -0
  30. package/dist/decoder/decoder.d.ts +20 -0
  31. package/dist/decoder/decoder.js +118 -0
  32. package/dist/decoder/format.d.ts +9 -0
  33. package/dist/decoder/format.js +50 -0
  34. package/dist/decoder/index.d.ts +5 -0
  35. package/dist/decoder/index.js +5 -0
  36. package/dist/decoder/parse.d.ts +53 -0
  37. package/dist/decoder/parse.js +179 -0
  38. package/dist/decoder/redact.d.ts +12 -0
  39. package/dist/decoder/redact.js +22 -0
  40. package/dist/decoder/types.d.ts +86 -0
  41. package/dist/decoder/types.js +9 -0
  42. package/dist/proxy/event-bus.d.ts +18 -0
  43. package/dist/proxy/event-bus.js +75 -0
  44. package/dist/proxy/id.d.ts +6 -0
  45. package/dist/proxy/id.js +8 -0
  46. package/dist/proxy/index.d.ts +5 -0
  47. package/dist/proxy/index.js +5 -0
  48. package/dist/proxy/jsonl-sink.d.ts +18 -0
  49. package/dist/proxy/jsonl-sink.js +44 -0
  50. package/dist/proxy/proxy.d.ts +21 -0
  51. package/dist/proxy/proxy.js +238 -0
  52. package/dist/proxy/types.d.ts +95 -0
  53. package/dist/proxy/types.js +32 -0
  54. package/dist/reconciliation/engine.d.ts +40 -0
  55. package/dist/reconciliation/engine.js +178 -0
  56. package/dist/reconciliation/index.d.ts +3 -0
  57. package/dist/reconciliation/index.js +3 -0
  58. package/dist/reconciliation/match.d.ts +31 -0
  59. package/dist/reconciliation/match.js +39 -0
  60. package/dist/reconciliation/types.d.ts +72 -0
  61. package/dist/reconciliation/types.js +17 -0
  62. package/package.json +85 -0
@@ -0,0 +1,32 @@
1
+ /**
2
+ * X402-10 proxy core types.
3
+ *
4
+ * The proxy is deliberately dumb: it captures raw HTTP and emits typed
5
+ * events. No x402 protocol parsing happens here — that's the decoder's
6
+ * job (X402-11). These shapes are the JSONL-on-disk format per
7
+ * ARCHITECTURE.md § JSONL record format; downstream readers depend on
8
+ * them being stable.
9
+ */
10
+ /**
11
+ * Heuristic: is this request/response pair x402-related at all?
12
+ *
13
+ * v0.1 detection rule (pure HTTP signals, no x402 parsing):
14
+ * - Request has an `X-PAYMENT` or `PAYMENT-SIGNATURE` header, OR
15
+ * - Response has a `402` status, OR
16
+ * - Response has an `X-PAYMENT-RESPONSE` or `PAYMENT-RESPONSE` header.
17
+ *
18
+ * The proxy logs every request/response regardless; this heuristic is
19
+ * used downstream by the decoder/reconciler to skip non-x402 traffic
20
+ * cheaply.
21
+ */
22
+ export function isLikelyX402Exchange(req, res) {
23
+ if (req.headers["x-payment"] || req.headers["payment-signature"])
24
+ return true;
25
+ if (!res)
26
+ return false;
27
+ if (res.status === 402)
28
+ return true;
29
+ if (res.headers["x-payment-response"] || res.headers["payment-response"])
30
+ return true;
31
+ return false;
32
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * X402-13 reconciliation engine.
3
+ *
4
+ * Joins proxy + decoder + chain streams into a single ReconciliationResult
5
+ * stream. Maintains an in-memory pending set keyed by exchange id;
6
+ * sweeps it periodically to emit `not_settled` for entries that don't
7
+ * find their on-chain match within `watchTimeoutMs`.
8
+ *
9
+ * Per ARCHITECTURE.md § Reconciliation engine:
10
+ *
11
+ * "Holds an in-memory pending set of `PaymentExchange`s whose
12
+ * `outcome.kind` is `rejected` or `upstream_timeout`. … On a match:
13
+ * emits a `ReconciliationResult` … and removes from pending. On no
14
+ * match after `--watch-timeout-ms` (default 60000): emits
15
+ * `kind: 'not_settled'` and removes from pending."
16
+ */
17
+ import type { ChainTransfer } from "../chain/types.js";
18
+ import type { DecodedEvent } from "../decoder/types.js";
19
+ import type { ProxyEvent } from "../proxy/types.js";
20
+ import type { EngineOptions, PendingExchange, ReconciliationResult } from "./types.js";
21
+ export interface ReconciliationEngine {
22
+ ingestProxyEvent(event: ProxyEvent): void;
23
+ ingestDecoderEvent(event: DecodedEvent): void;
24
+ ingestChainTransfer(transfer: ChainTransfer): void;
25
+ /** Snapshot of currently-pending exchanges (for diagnostics / tests). */
26
+ pending(): readonly PendingExchange[];
27
+ /** AsyncIterable of every result emitted from now onward. */
28
+ results(): AsyncIterable<ReconciliationResult> & {
29
+ unsubscribe(): void;
30
+ };
31
+ /**
32
+ * Run the expiry sweep once. Useful for offline replay (X402-14
33
+ * `inspect`) where a virtual clock advances faster than the
34
+ * `sweepIntervalMs` real-time interval can catch.
35
+ */
36
+ tick(): void;
37
+ /** Stop sweeping; resolve any subscriber awaits. */
38
+ close(): Promise<void>;
39
+ }
40
+ export declare function createReconciliationEngine(opts?: EngineOptions): ReconciliationEngine;
@@ -0,0 +1,178 @@
1
+ /**
2
+ * X402-13 reconciliation engine.
3
+ *
4
+ * Joins proxy + decoder + chain streams into a single ReconciliationResult
5
+ * stream. Maintains an in-memory pending set keyed by exchange id;
6
+ * sweeps it periodically to emit `not_settled` for entries that don't
7
+ * find their on-chain match within `watchTimeoutMs`.
8
+ *
9
+ * Per ARCHITECTURE.md § Reconciliation engine:
10
+ *
11
+ * "Holds an in-memory pending set of `PaymentExchange`s whose
12
+ * `outcome.kind` is `rejected` or `upstream_timeout`. … On a match:
13
+ * emits a `ReconciliationResult` … and removes from pending. On no
14
+ * match after `--watch-timeout-ms` (default 60000): emits
15
+ * `kind: 'not_settled'` and removes from pending."
16
+ */
17
+ import { createEventBus } from "../proxy/event-bus.js";
18
+ import { matchPendingAgainstTransfer } from "./match.js";
19
+ const DEFAULT_WATCH_TIMEOUT_MS = 60_000;
20
+ const DEFAULT_SWEEP_INTERVAL_MS = 1_000;
21
+ export function createReconciliationEngine(opts = {}) {
22
+ const watchTimeoutMs = opts.watchTimeoutMs ?? DEFAULT_WATCH_TIMEOUT_MS;
23
+ const sweepIntervalMs = opts.sweepIntervalMs ?? DEFAULT_SWEEP_INTERVAL_MS;
24
+ const now = opts.now ?? (() => Date.now());
25
+ const partial = new Map();
26
+ const pending = new Map();
27
+ const resultBus = createEventBus();
28
+ const promote = (id) => {
29
+ const half = partial.get(id);
30
+ if (!half || !half.payment || !half.outcome)
31
+ return;
32
+ // Only flag reconcile-worthy outcomes; success/paid is fine.
33
+ if (half.outcome.kind === "paid") {
34
+ partial.delete(id);
35
+ return;
36
+ }
37
+ pending.set(id, {
38
+ id,
39
+ addedAt: now(),
40
+ payment: half.payment,
41
+ outcomeKind: half.outcome.kind,
42
+ ...(half.outcome.errorReason ? { errorReason: half.outcome.errorReason } : {}),
43
+ });
44
+ partial.delete(id);
45
+ };
46
+ const ingestProxyEvent = (event) => {
47
+ if (event.event === "exchange.closed") {
48
+ const half = partial.get(event.id) ?? {};
49
+ let kind;
50
+ switch (event.outcome.kind) {
51
+ case "paid":
52
+ kind = { kind: "paid" };
53
+ break;
54
+ case "rejected":
55
+ kind = {
56
+ kind: "rejected",
57
+ ...(event.outcome.rawBody
58
+ ? { errorReason: extractErrorReason(event.outcome.rawBody) }
59
+ : {}),
60
+ };
61
+ break;
62
+ case "upstream_timeout":
63
+ kind = { kind: "upstream_timeout" };
64
+ break;
65
+ case "unknown":
66
+ default:
67
+ kind = { kind: "unknown" };
68
+ }
69
+ half.outcome = kind;
70
+ partial.set(event.id, half);
71
+ promote(event.id);
72
+ }
73
+ // `exchange.opened` and `proxy.error` are observed but don't gate
74
+ // reconciliation — the payment + closed events are the join points.
75
+ };
76
+ const ingestDecoderEvent = (event) => {
77
+ if (event.event === "exchange.payment") {
78
+ const half = partial.get(event.id) ?? {};
79
+ half.payment = event.payment;
80
+ partial.set(event.id, half);
81
+ promote(event.id);
82
+ }
83
+ // `exchange.challenge` and `exchange.settlement` are not required
84
+ // for matching. They could be persisted in the future to enrich
85
+ // ReconciliationResult.pending.
86
+ };
87
+ const ingestChainTransfer = (transfer) => {
88
+ if (!transfer.authorizationNonce) {
89
+ // Without a nonce we can't safely match; ignore.
90
+ return;
91
+ }
92
+ for (const [id, p] of pending) {
93
+ const result = matchPendingAgainstTransfer(p, transfer);
94
+ if (result.kind === "no_match")
95
+ continue;
96
+ const t = new Date(now()).toISOString();
97
+ const gapMs = now() - p.addedAt;
98
+ if (result.kind === "exact") {
99
+ resultBus.emit({
100
+ kind: "settled_on_chain",
101
+ t,
102
+ exchangeId: id,
103
+ pending: p,
104
+ onChain: transfer,
105
+ gapMs,
106
+ });
107
+ }
108
+ else if (result.kind === "value_mismatch") {
109
+ resultBus.emit({
110
+ kind: "value_mismatch",
111
+ t,
112
+ exchangeId: id,
113
+ pending: p,
114
+ onChain: transfer,
115
+ expected: result.expected,
116
+ actual: result.actual,
117
+ });
118
+ }
119
+ else if (result.kind === "recipient_mismatch") {
120
+ resultBus.emit({
121
+ kind: "recipient_mismatch",
122
+ t,
123
+ exchangeId: id,
124
+ pending: p,
125
+ onChain: transfer,
126
+ expectedPayee: p.payment.payload.authorization.to,
127
+ actualPayee: transfer.to,
128
+ });
129
+ }
130
+ pending.delete(id);
131
+ return; // one match per chain transfer
132
+ }
133
+ };
134
+ const sweep = () => {
135
+ const cutoff = now() - watchTimeoutMs;
136
+ for (const [id, p] of pending) {
137
+ if (p.addedAt <= cutoff) {
138
+ resultBus.emit({
139
+ kind: "not_settled",
140
+ t: new Date(now()).toISOString(),
141
+ exchangeId: id,
142
+ pending: p,
143
+ waitedMs: now() - p.addedAt,
144
+ });
145
+ pending.delete(id);
146
+ }
147
+ }
148
+ };
149
+ const interval = setInterval(sweep, sweepIntervalMs);
150
+ // Don't let the timer keep the Node event loop alive on its own.
151
+ interval.unref?.();
152
+ return {
153
+ ingestProxyEvent,
154
+ ingestDecoderEvent,
155
+ ingestChainTransfer,
156
+ pending: () => Array.from(pending.values()),
157
+ results: () => resultBus.subscribe(),
158
+ tick: sweep,
159
+ async close() {
160
+ clearInterval(interval);
161
+ },
162
+ };
163
+ }
164
+ /**
165
+ * Extract a top-level `error` string from a 402 response body if it
166
+ * looks like x402-shaped JSON. Returns the string or undefined.
167
+ */
168
+ function extractErrorReason(rawBody) {
169
+ try {
170
+ const parsed = JSON.parse(rawBody);
171
+ if (typeof parsed?.error === "string")
172
+ return parsed.error;
173
+ }
174
+ catch {
175
+ // not JSON; skip
176
+ }
177
+ return undefined;
178
+ }
@@ -0,0 +1,3 @@
1
+ export { createReconciliationEngine, type ReconciliationEngine } from "./engine.js";
2
+ export { matchPendingAgainstTransfer, type MatchResult } from "./match.js";
3
+ export { type EngineOptions, type PendingExchange, type ReconciliationResult } from "./types.js";
@@ -0,0 +1,3 @@
1
+ export { createReconciliationEngine } from "./engine.js";
2
+ export { matchPendingAgainstTransfer } from "./match.js";
3
+ export {} from "./types.js";
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Pure match function: does this on-chain ChainTransfer correspond to
3
+ * this pending PaymentExchange?
4
+ *
5
+ * Match key per ADR-001 + ARCHITECTURE.md § Reconciliation engine:
6
+ *
7
+ * (payer, payee, value, nonce) exact equality
8
+ *
9
+ * The EIP-3009 `nonce` is unique per (authorizer, contract) so a
10
+ * (payer, payee, value, nonce) tuple is collision-safe — no fuzziness
11
+ * needed. Addresses compared case-insensitively; bigints compared
12
+ * structurally.
13
+ *
14
+ * Returns a small discriminated union so the engine can decide
15
+ * whether to fire `settled_on_chain` / `value_mismatch` /
16
+ * `recipient_mismatch` / no-op.
17
+ */
18
+ import type { ChainTransfer } from "../chain/types.js";
19
+ import type { PendingExchange } from "./types.js";
20
+ export type MatchResult = {
21
+ readonly kind: "no_match";
22
+ } | {
23
+ readonly kind: "exact";
24
+ } | {
25
+ readonly kind: "value_mismatch";
26
+ readonly expected: bigint;
27
+ readonly actual: bigint;
28
+ } | {
29
+ readonly kind: "recipient_mismatch";
30
+ };
31
+ export declare function matchPendingAgainstTransfer(pending: PendingExchange, transfer: ChainTransfer): MatchResult;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Pure match function: does this on-chain ChainTransfer correspond to
3
+ * this pending PaymentExchange?
4
+ *
5
+ * Match key per ADR-001 + ARCHITECTURE.md § Reconciliation engine:
6
+ *
7
+ * (payer, payee, value, nonce) exact equality
8
+ *
9
+ * The EIP-3009 `nonce` is unique per (authorizer, contract) so a
10
+ * (payer, payee, value, nonce) tuple is collision-safe — no fuzziness
11
+ * needed. Addresses compared case-insensitively; bigints compared
12
+ * structurally.
13
+ *
14
+ * Returns a small discriminated union so the engine can decide
15
+ * whether to fire `settled_on_chain` / `value_mismatch` /
16
+ * `recipient_mismatch` / no-op.
17
+ */
18
+ export function matchPendingAgainstTransfer(pending, transfer) {
19
+ const auth = pending.payment.payload.authorization;
20
+ const sameNonce = typeof transfer.authorizationNonce === "string" &&
21
+ transfer.authorizationNonce.toLowerCase() === auth.nonce.toLowerCase();
22
+ const samePayer = auth.from.toLowerCase() === transfer.from.toLowerCase();
23
+ // Nonce alone is collision-safe per (payer, contract). If both differ,
24
+ // it's definitely not us. If only nonce matches but payer differs,
25
+ // the chain client filtered to the wrong wallet or the chain emitted
26
+ // a malformed event — either way, not our exchange.
27
+ if (!sameNonce || !samePayer)
28
+ return { kind: "no_match" };
29
+ const expectedTo = auth.to.toLowerCase();
30
+ const actualTo = transfer.to.toLowerCase();
31
+ if (expectedTo !== actualTo) {
32
+ return { kind: "recipient_mismatch" };
33
+ }
34
+ const expectedValue = BigInt(auth.value);
35
+ if (expectedValue !== transfer.value) {
36
+ return { kind: "value_mismatch", expected: expectedValue, actual: transfer.value };
37
+ }
38
+ return { kind: "exact" };
39
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * X402-13 reconciliation engine types.
3
+ *
4
+ * The engine joins three input streams:
5
+ * 1. Proxy `exchange.closed` events — tells us a request finished
6
+ * with a `rejected` / `upstream_timeout` / `paid` outcome.
7
+ * 2. Decoder `exchange.payment` events — gives us the EIP-3009
8
+ * `PaymentAuthorization` (with nonce) for that same exchange id.
9
+ * 3. Chain client `ChainTransfer`s — actual on-chain USDC transfers
10
+ * with the matching `AuthorizationUsed.nonce`.
11
+ *
12
+ * For every exchange whose facilitator outcome was failed-but-the-
13
+ * underlying-tx-might-have-settled, the engine emits a
14
+ * `ReconciliationResult`. The headline case (`settled_on_chain`) is
15
+ * the canonical #1062 detection: facilitator says no, chain says yes.
16
+ */
17
+ import type { Address, ChainTransfer } from "../chain/types.js";
18
+ import type { PaymentPayload } from "../decoder/types.js";
19
+ /** A two-half-joined exchange currently awaiting on-chain confirmation. */
20
+ export interface PendingExchange {
21
+ readonly id: string;
22
+ /** Unix-ms timestamp when this exchange was flagged as pending. */
23
+ readonly addedAt: number;
24
+ /** Decoded payment payload from the X-PAYMENT header. */
25
+ readonly payment: PaymentPayload;
26
+ /** Why the proxy classified the response as a candidate for reconciliation. */
27
+ readonly outcomeKind: "rejected" | "upstream_timeout" | "unknown";
28
+ /** errorReason from the 402 body, if the proxy captured one. */
29
+ readonly errorReason?: string;
30
+ }
31
+ /**
32
+ * Output of the engine. One emitted per pending exchange that either
33
+ * (a) finds its on-chain match, or (b) gives up after `watchTimeoutMs`.
34
+ */
35
+ export type ReconciliationResult = {
36
+ readonly kind: "settled_on_chain";
37
+ readonly t: string;
38
+ readonly exchangeId: string;
39
+ readonly pending: PendingExchange;
40
+ readonly onChain: ChainTransfer;
41
+ readonly gapMs: number;
42
+ } | {
43
+ readonly kind: "not_settled";
44
+ readonly t: string;
45
+ readonly exchangeId: string;
46
+ readonly pending: PendingExchange;
47
+ readonly waitedMs: number;
48
+ } | {
49
+ readonly kind: "value_mismatch";
50
+ readonly t: string;
51
+ readonly exchangeId: string;
52
+ readonly pending: PendingExchange;
53
+ readonly onChain: ChainTransfer;
54
+ readonly expected: bigint;
55
+ readonly actual: bigint;
56
+ } | {
57
+ readonly kind: "recipient_mismatch";
58
+ readonly t: string;
59
+ readonly exchangeId: string;
60
+ readonly pending: PendingExchange;
61
+ readonly onChain: ChainTransfer;
62
+ readonly expectedPayee: Address;
63
+ readonly actualPayee: Address;
64
+ };
65
+ export interface EngineOptions {
66
+ /** How long to wait for an on-chain match before emitting `not_settled`. Default 60_000 ms (per ARCHITECTURE.md). */
67
+ readonly watchTimeoutMs?: number;
68
+ /** Sweep interval for expiring pending entries. Default 1_000 ms. */
69
+ readonly sweepIntervalMs?: number;
70
+ /** Injectable clock for tests. Default Date.now. */
71
+ readonly now?: () => number;
72
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * X402-13 reconciliation engine types.
3
+ *
4
+ * The engine joins three input streams:
5
+ * 1. Proxy `exchange.closed` events — tells us a request finished
6
+ * with a `rejected` / `upstream_timeout` / `paid` outcome.
7
+ * 2. Decoder `exchange.payment` events — gives us the EIP-3009
8
+ * `PaymentAuthorization` (with nonce) for that same exchange id.
9
+ * 3. Chain client `ChainTransfer`s — actual on-chain USDC transfers
10
+ * with the matching `AuthorizationUsed.nonce`.
11
+ *
12
+ * For every exchange whose facilitator outcome was failed-but-the-
13
+ * underlying-tx-might-have-settled, the engine emits a
14
+ * `ReconciliationResult`. The headline case (`settled_on_chain`) is
15
+ * the canonical #1062 detection: facilitator says no, chain says yes.
16
+ */
17
+ export {};
package/package.json ADDED
@@ -0,0 +1,85 @@
1
+ {
2
+ "name": "x402trace",
3
+ "version": "0.1.0",
4
+ "description": "Local CLI for debugging x402 payment flows on Base — catches timeout reconciliation failures",
5
+ "type": "module",
6
+ "bin": {
7
+ "x402trace": "./dist/cli.js"
8
+ },
9
+ "main": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "files": [
12
+ "dist",
13
+ "README.md",
14
+ "LICENSE",
15
+ "CHANGELOG.md"
16
+ ],
17
+ "keywords": [
18
+ "x402",
19
+ "base",
20
+ "usdc",
21
+ "payments",
22
+ "cli",
23
+ "debugging",
24
+ "observability",
25
+ "web3",
26
+ "ethereum"
27
+ ],
28
+ "author": "fardin vahdat",
29
+ "license": "Apache-2.0",
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "git+https://github.com/fardinvahdat/x402trace.git"
33
+ },
34
+ "bugs": {
35
+ "url": "https://github.com/fardinvahdat/x402trace/issues"
36
+ },
37
+ "homepage": "https://github.com/fardinvahdat/x402trace#readme",
38
+ "engines": {
39
+ "node": ">=20.0.0"
40
+ },
41
+ "devDependencies": {
42
+ "@eslint/js": "^9.39.4",
43
+ "@types/node": "^22.0.0",
44
+ "@typescript-eslint/eslint-plugin": "^8.0.0",
45
+ "@typescript-eslint/parser": "^8.0.0",
46
+ "@vitest/coverage-v8": "^3.0.0",
47
+ "eslint": "^9.0.0",
48
+ "prettier": "^3.3.0",
49
+ "tsx": "^4.19.0",
50
+ "typescript": "^5.6.0",
51
+ "vitest": "^3.0.0"
52
+ },
53
+ "dependencies": {
54
+ "commander": "^14.0.3",
55
+ "dotenv": "^17.4.2",
56
+ "hono": "^4.12.18",
57
+ "viem": "^2.48.11",
58
+ "x402": "^1.2.0",
59
+ "x402-fetch": "^1.2.0",
60
+ "x402-hono": "^1.2.0"
61
+ },
62
+ "scripts": {
63
+ "build": "tsc -p tsconfig.build.json",
64
+ "dev": "tsx src/cli.ts",
65
+ "dogfood:server": "tsx scripts/dev-server.ts",
66
+ "dogfood:client": "tsx scripts/dogfood-client.ts",
67
+ "dogfood:mock-facilitator": "tsx scripts/mock-facilitator.ts",
68
+ "dogfood:failure-modes": "tsx scripts/failure-modes.ts",
69
+ "proxy": "tsx scripts/proxy.ts",
70
+ "x402trace": "tsx src/cli.ts",
71
+ "decoder:demo": "tsx scripts/decoder-demo.ts",
72
+ "test": "vitest run",
73
+ "test:watch": "vitest",
74
+ "test:unit": "vitest run tests/unit",
75
+ "test:integration": "vitest run tests/integration",
76
+ "test:e2e": "vitest run tests/e2e",
77
+ "test:smoke": "vitest run tests/smoke",
78
+ "test:coverage": "vitest run --coverage",
79
+ "typecheck": "tsc --noEmit",
80
+ "lint": "eslint . && prettier --check \"{src,scripts,tests,api}/**/*.ts\" \"*.json\" \"*.js\"",
81
+ "lint:fix": "eslint . --fix && prettier --write \"{src,scripts,tests,api}/**/*.ts\" \"*.json\" \"*.js\"",
82
+ "lint:docs": "echo 'TODO: wire up markdown lint in X402-2'",
83
+ "lint:links": "echo 'TODO: wire up link checker in X402-2'"
84
+ }
85
+ }