cordon-mcp 0.1.0 → 0.2.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.
@@ -9,13 +9,15 @@ import { join } from "node:path";
9
9
 
10
10
  // ../fixtures/src/gates.gen.ts
11
11
  var GATE_RUNS = {
12
- "G1": { "id": "G1", "status": "green", "tests": 66, "passed": 66, "recordedAt": "2026-09-11" },
12
+ "G1": { "id": "G1", "status": "green", "tests": 68, "passed": 68, "recordedAt": "2026-09-26" },
13
13
  "G2": { "id": "G2", "status": "green", "tests": 8, "passed": 8, "recordedAt": "2026-09-11" },
14
14
  "G3": { "id": "G3", "status": "green", "tests": 5, "passed": 5, "recordedAt": "2026-09-10" },
15
- "G4": { "id": "G4", "status": "green", "tests": 1, "passed": 1, "recordedAt": "2026-09-08" },
16
- "G5": { "id": "G5", "status": "green", "tests": 12, "passed": 12, "recordedAt": "2026-09-08" },
17
- "G6": { "id": "G6", "status": "green", "tests": 14, "passed": 14, "recordedAt": "2026-09-08" },
18
- "G7": { "id": "G7", "status": "green", "tests": 10, "passed": 10, "recordedAt": "2026-09-09" }
15
+ "G4": { "id": "G4", "status": "green", "tests": 1, "passed": 1, "recordedAt": "2026-09-26" },
16
+ "G5": { "id": "G5", "status": "green", "tests": 12, "passed": 12, "recordedAt": "2026-09-26" },
17
+ "G6": { "id": "G6", "status": "green", "tests": 16, "passed": 16, "recordedAt": "2026-09-26" },
18
+ "G7": { "id": "G7", "status": "green", "tests": 10, "passed": 10, "recordedAt": "2026-09-09" },
19
+ "G8": { "id": "G8", "status": "green", "tests": 14, "passed": 14, "recordedAt": "2026-09-26" },
20
+ "G9": { "id": "G9", "status": "green", "tests": 4, "passed": 4, "recordedAt": "2026-09-26" }
19
21
  };
20
22
 
21
23
  // ../fixtures/src/search.gen.ts
@@ -33,13 +35,13 @@ var SEARCH = {
33
35
 
34
36
  // ../fixtures/src/deployment.gen.ts
35
37
  var DEPLOYMENT = {
36
- chainId: 5042002,
37
- registry: "0xf86de085e63b00c9fba300b19807c883deb961e9",
38
- vault: "0x00ab57acd260c594a661b6101bdf7e92267af135",
39
- record: "0x2a8361ac23f5ffcfde9f0d7bc7618178770332d0",
40
- fromBlock: "61265318",
41
- commit: "ee3955b82fb0b6f7786bbfb5b2f5ebc2b174f8cc",
42
- deployedAt: "2026-09-09T17:24:22.882Z"
38
+ chainId: 11155111,
39
+ registry: "0xe799edc4aa6bcaf6915c7a5eadbdc4e709aef4b2",
40
+ vault: "0x22d539bdf23e08a856fc80bc991e34948921ad46",
41
+ record: "0x5c026b1b129a9e2171e53c5024f76052006ef1e0",
42
+ fromBlock: "11784955",
43
+ commit: "6c83270702ecca055240055c5dbe9340fbfa867f",
44
+ deployedAt: "2026-09-26T08:09:50.086Z"
43
45
  };
44
46
 
45
47
  // ../fixtures/src/drill.gen.ts
@@ -79,6 +81,8 @@ var ARC = {
79
81
  faucet: "https://faucet.circle.com",
80
82
  /** Gas token is USDC, native, 18 decimals. */
81
83
  nativeDecimals: 18,
84
+ nativeName: "USDC",
85
+ nativeSymbol: "USDC",
82
86
  /**
83
87
  * The ERC-20 view of the same balance. balanceOf truncates below 1e-6.
84
88
  *
@@ -111,6 +115,97 @@ var ARC = {
111
115
  multicall3: "0xcA11bde05977b3631167028862bE2a173976CA11",
112
116
  mainnetLaunched: false
113
117
  };
118
+ var SEPOLIA = {
119
+ chainId: 11155111,
120
+ name: "Ethereum Sepolia",
121
+ /* Answered `eth_getBalance`, `eth_getCode` and `eth_call` on 25 Sep 2026;
122
+ the other two public endpoints tried that day returned 404 and a paywall. */
123
+ rpc: "https://ethereum-sepolia-rpc.publicnode.com",
124
+ explorer: "https://sepolia.etherscan.io",
125
+ /** ETH, for gas. Arc's faucet pours the gas token, which there is USDC. */
126
+ faucet: "https://cloud.google.com/application/web3/faucet/ethereum/sepolia",
127
+ /** USDC, from Circle, and a separate errand from the gas above. */
128
+ usdcFaucet: "https://faucet.circle.com",
129
+ /** ETH's, and it has nothing to do with USDC on this chain. */
130
+ nativeDecimals: 18,
131
+ nativeName: "Ether",
132
+ nativeSymbol: "ETH",
133
+ /**
134
+ * USDC, and the only view of it. A real FiatTokenV2 with EIP-3009, so x402
135
+ * `exact` settles here: probed on 20 Sep 2026 for `version() -> "2"`,
136
+ * `DOMAIN_SEPARATOR()` and an `authorizationState` that answers rather than
137
+ * falls through. Carried code on 25 Sep 2026.
138
+ */
139
+ erc20: "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238",
140
+ erc20Decimals: 6,
141
+ /** Multicall3, at the address it has on every chain that has one. */
142
+ multicall3: "0xcA11bde05977b3631167028862bE2a173976CA11",
143
+ mainnetLaunched: false
144
+ };
145
+ var ANVIL = {
146
+ chainId: 31337,
147
+ name: "Anvil",
148
+ rpc: "http://127.0.0.1:8545",
149
+ explorer: "",
150
+ faucet: "",
151
+ nativeDecimals: 18,
152
+ nativeName: "Ether",
153
+ nativeSymbol: "ETH",
154
+ erc20: "0x0000000000000000000000000000000000000000",
155
+ erc20Decimals: 6,
156
+ mainnetLaunched: false
157
+ };
158
+ var CHAINS = {
159
+ [ARC.chainId]: ARC,
160
+ [SEPOLIA.chainId]: SEPOLIA,
161
+ [ANVIL.chainId]: ANVIL
162
+ };
163
+ var DEFAULT_CHAIN = SEPOLIA;
164
+ function chainFacts(chainId) {
165
+ return CHAINS[chainId];
166
+ }
167
+ var ENSV2 = {
168
+ chainId: SEPOLIA.chainId,
169
+ registry: "0x657ea849311d3d5823348dded7c2aaafb3ede09e",
170
+ rootRegistry: "0x9703dbd26dab89504490994138cf2c575251a9ce",
171
+ registrar: "0xabe76f6c8dfced81aa5a2bb8034202a7136b94ca",
172
+ universalResolver: "0x5d25c1d6acbb71b7a28aa7899618a3412a8303e3",
173
+ permissionedResolverImpl: "0x14f09fd05d4585759e54844dc9b00147131cf243",
174
+ /**
175
+ * How a name gets subnames: they live in a registry of their own, a
176
+ * UserRegistry proxy the parent deploys through this factory and then points
177
+ * at with `setSubregistry`. A mandate tree is a tree of these.
178
+ */
179
+ verifiableFactory: "0x9e726eb570beb6bceb495ab8cda7df517d4e841c",
180
+ userRegistryImpl: "0xa80338aaa8d23831cea25e858d1774534abb0263",
181
+ /**
182
+ * What the registrar charges, and the reason this set is the right one.
183
+ *
184
+ * Sepolia carries two complete ENSv2 deployments, both live and both with
185
+ * names registered in them, because the contracts are an early preview and
186
+ * are redeployed as they change — a chain keeps every one of them. The other
187
+ * set is documented only in an unmerged docs build. This set is the one the
188
+ * published docs list, and it is self-consistent where it counts: the
189
+ * registrar at the address above answers `rentPriceOracle()` with exactly
190
+ * the oracle the docs list for it, and the root registry answers
191
+ * `getSubregistry("eth")` with exactly the registry above. Checked against
192
+ * the chain on 25 Sep 2026.
193
+ */
194
+ rentPriceOracle: "0x9b0b9c65bdaf9794ff7697e4dcfb1f50581072bb",
195
+ /**
196
+ * What the registrar charges registration fees in, and a different token
197
+ * from the one a mandate spends.
198
+ *
199
+ * It mints to anyone who asks, with no access control, which is what makes
200
+ * it safe to demo with — and exactly why it may never reach a vault. The
201
+ * money Cordon bounds is `SEPOLIA.erc20`, whose supply nobody here can
202
+ * conjure. Confusing the two turns a budget into a suggestion.
203
+ */
204
+ feeToken: "0x16f95d91dba7da3aca778ec053df0ff6c6a8aa8e",
205
+ feeTokenDecimals: 6,
206
+ /** The registrar takes either. Listed so the pair is not discovered late. */
207
+ feeTokenDai: "0x278053acc97888e63ec81c80fec641bf0bf19664"
208
+ };
114
209
  var ERC8004 = {
115
210
  identity: "0x8004A818BFB912233c491871b3d84c89A494BD9e",
116
211
  reputation: "0x8004B663056A597Dffe9eCcC1965A193B7388713",
@@ -206,6 +301,19 @@ var GATEWAY = {
206
301
  mintGas6: 2975n,
207
302
  settlementVerifiedOn: "2026-09-11"
208
303
  };
304
+ var DEMOS = {
305
+ 5042002: {
306
+ owner: "0x736159a06C89Ea5b12eD88BE658741edCa64324D",
307
+ root: "0xd08820db0e1cd58426ba9dc8e78513b05d244b42cdaf841070b0a00d49b901ad",
308
+ agentId: "894124"
309
+ },
310
+ 11155111: {
311
+ owner: "0x9F846D2054689a439DA8D0619f37F6c70Db03597",
312
+ root: "0x7a64b3d4db9223e41fa3e5ddd3c4512a44f47a9d48218f5a7224b1cf80dc18ff",
313
+ agentId: "10521"
314
+ }
315
+ };
316
+ var DEMO = DEMOS[5042002];
209
317
  var PENDING_ADDRESS = "pending \u2014 written by the deploy script";
210
318
  var PENDING_MANDATE = "pending \u2014 no mandate signed yet";
211
319
  var MANDATE = {
@@ -342,6 +450,10 @@ var STRUCTURING = {
342
450
  // ../daemon/src/config.ts
343
451
  var ConfigError = class extends Error {
344
452
  };
453
+ function isLoopback(bind) {
454
+ const host = bind.trim().replace(/^\[|\]$/g, "").toLowerCase();
455
+ return host === "localhost" || host === "::1" || /^127\./.test(host);
456
+ }
345
457
  var ENV = {
346
458
  required: {
347
459
  CORDON_VAULT: "TreeVault address, from deployments/<chainId>.json",
@@ -352,13 +464,15 @@ var ENV = {
352
464
  optional: {
353
465
  CORDON_RECORD: "ConductRecord, from deployments/<chainId>.json. Without it, refusals are enforced but never published",
354
466
  CORDON_IDENTITY: `ERC-8004 Identity; defaults to ${ERC8004.identity}`,
355
- CORDON_RPC: `defaults to ${ARC.rpc}`,
356
- CORDON_CHAIN_ID: `defaults to ${ARC.chainId}`,
357
- CORDON_USDC: "the 6-decimal ERC-20 view; defaults to Arc's",
467
+ CORDON_RPC: `defaults to ${DEFAULT_CHAIN.rpc}`,
468
+ CORDON_CHAIN_ID: `defaults to ${DEFAULT_CHAIN.chainId} (${DEFAULT_CHAIN.name})`,
469
+ CORDON_USDC: `the 6-decimal ERC-20 view; defaults to ${DEFAULT_CHAIN.name}'s`,
358
470
  CORDON_NETWORKS: "CAIP-2 ids this daemon will settle on",
359
471
  CORDON_ASSETS: "assets it will pay in",
360
472
  CORDON_PORT: "defaults to 8402",
361
- CORDON_POLL_MS: "receipt polling interval; defaults to 250, matched to Arc's finality rather than to viem's 4,000",
473
+ CORDON_BIND: "the interface to listen on; defaults to 127.0.0.1. Anything else requires CORDON_TOKEN",
474
+ CORDON_TOKEN: "a shared secret every request must send as `Authorization: Bearer <token>`. Optional on loopback, required off it",
475
+ CORDON_POLL_MS: "receipt polling interval; 250 on Arc, matched to its sub-second finality, and 4,000 elsewhere \u2014 asking a public endpoint forty times per twelve-second block is how an address gets rate limited",
362
476
  CORDON_KEY_FILE: "where a spawned child's key is written, before the spawn is sent; defaults to ~/.cordon/cordon.env"
363
477
  }
364
478
  };
@@ -385,18 +499,27 @@ function load(env = process.env) {
385
499
  keys.push({ node: value, keyEnv });
386
500
  }
387
501
  if (keys.length === 0) throw new ConfigError("no CORDON_NODE_<label> is set");
502
+ const bind = env.CORDON_BIND ?? "127.0.0.1";
503
+ const token = env.CORDON_TOKEN || void 0;
504
+ if (!isLoopback(bind) && !token) {
505
+ throw new ConfigError(
506
+ `CORDON_BIND is ${bind}, which is reachable from off this machine, and CORDON_TOKEN is not set. This process holds the operator keys: bind to 127.0.0.1, or set a token.`
507
+ );
508
+ }
388
509
  return {
389
- rpcUrl: env.CORDON_RPC ?? ARC.rpc,
390
- chainId: Number(env.CORDON_CHAIN_ID ?? ARC.chainId),
510
+ rpcUrl: env.CORDON_RPC ?? DEFAULT_CHAIN.rpc,
511
+ chainId: Number(env.CORDON_CHAIN_ID ?? DEFAULT_CHAIN.chainId),
391
512
  vault: address(env, "CORDON_VAULT"),
392
513
  registry: address(env, "CORDON_REGISTRY"),
393
- usdc: env.CORDON_USDC ?? ARC.erc20,
514
+ usdc: env.CORDON_USDC ?? DEFAULT_CHAIN.erc20,
394
515
  record: env.CORDON_RECORD ? address(env, "CORDON_RECORD") : void 0,
395
516
  identity: env.CORDON_IDENTITY ?? ERC8004.identity,
396
- networks: (env.CORDON_NETWORKS ?? `eip155:${ARC.chainId}`).split(",").map((s) => s.trim()),
397
- assets: (env.CORDON_ASSETS ?? ARC.erc20).split(",").map((s) => s.trim()),
517
+ networks: (env.CORDON_NETWORKS ?? `eip155:${DEFAULT_CHAIN.chainId}`).split(",").map((s) => s.trim()),
518
+ assets: (env.CORDON_ASSETS ?? DEFAULT_CHAIN.erc20).split(",").map((s) => s.trim()),
398
519
  port: Number(env.CORDON_PORT ?? 8402),
399
- pollMs: Number(env.CORDON_POLL_MS ?? 250),
520
+ bind,
521
+ token,
522
+ pollMs: Number(env.CORDON_POLL_MS ?? (Number(env.CORDON_CHAIN_ID ?? DEFAULT_CHAIN.chainId) === ARC.chainId ? 250 : 4e3)),
400
523
  keys,
401
524
  keyFile: env.CORDON_KEY_FILE ?? join(homedir(), ".cordon", "cordon.env")
402
525
  };
@@ -498,6 +621,25 @@ var TreeVaultAbi = [
498
621
  ],
499
622
  "stateMutability": "view"
500
623
  },
624
+ {
625
+ "type": "function",
626
+ "name": "delivered",
627
+ "inputs": [
628
+ {
629
+ "name": "node",
630
+ "type": "bytes32",
631
+ "internalType": "bytes32"
632
+ }
633
+ ],
634
+ "outputs": [
635
+ {
636
+ "name": "",
637
+ "type": "uint128",
638
+ "internalType": "uint128"
639
+ }
640
+ ],
641
+ "stateMutability": "view"
642
+ },
501
643
  {
502
644
  "type": "function",
503
645
  "name": "draw",
@@ -740,6 +882,25 @@ var TreeVaultAbi = [
740
882
  "outputs": [],
741
883
  "stateMutability": "nonpayable"
742
884
  },
885
+ {
886
+ "type": "function",
887
+ "name": "releasedSpent",
888
+ "inputs": [
889
+ {
890
+ "name": "node",
891
+ "type": "bytes32",
892
+ "internalType": "bytes32"
893
+ }
894
+ ],
895
+ "outputs": [
896
+ {
897
+ "name": "",
898
+ "type": "uint128",
899
+ "internalType": "uint128"
900
+ }
901
+ ],
902
+ "stateMutability": "view"
903
+ },
743
904
  {
744
905
  "type": "function",
745
906
  "name": "treasury6",
@@ -983,6 +1144,12 @@ var TreeVaultAbi = [
983
1144
  "type": "uint128",
984
1145
  "indexed": false,
985
1146
  "internalType": "uint128"
1147
+ },
1148
+ {
1149
+ "name": "released6",
1150
+ "type": "uint128",
1151
+ "indexed": false,
1152
+ "internalType": "uint128"
986
1153
  }
987
1154
  ],
988
1155
  "anonymous": false
@@ -1023,6 +1190,22 @@ var TreeVaultAbi = [
1023
1190
  }
1024
1191
  ]
1025
1192
  },
1193
+ {
1194
+ "type": "error",
1195
+ "name": "BranchIsCut",
1196
+ "inputs": [
1197
+ {
1198
+ "name": "id",
1199
+ "type": "uint256",
1200
+ "internalType": "uint256"
1201
+ },
1202
+ {
1203
+ "name": "cutAt",
1204
+ "type": "bytes32",
1205
+ "internalType": "bytes32"
1206
+ }
1207
+ ]
1208
+ },
1026
1209
  {
1027
1210
  "type": "error",
1028
1211
  "name": "InsufficientTreasury",
@@ -1128,6 +1311,19 @@ var MandateRegistryAbi = [
1128
1311
  ],
1129
1312
  "stateMutability": "view"
1130
1313
  },
1314
+ {
1315
+ "type": "function",
1316
+ "name": "MAX_TREE_DEPTH",
1317
+ "inputs": [],
1318
+ "outputs": [
1319
+ {
1320
+ "name": "",
1321
+ "type": "uint8",
1322
+ "internalType": "uint8"
1323
+ }
1324
+ ],
1325
+ "stateMutability": "view"
1326
+ },
1131
1327
  {
1132
1328
  "type": "function",
1133
1329
  "name": "exists",
@@ -2371,6 +2567,48 @@ var Recorder = class {
2371
2567
  }
2372
2568
  };
2373
2569
 
2570
+ // ../daemon/src/repeats.ts
2571
+ var keyOf = (b) => `${b.node.toLowerCase()}|${b.counterparty.toLowerCase()}|${b.amount}|${b.reason}`;
2572
+ var RefusalRepeats = class {
2573
+ seen = /* @__PURE__ */ new Map();
2574
+ now;
2575
+ constructor(options = {}) {
2576
+ this.now = options.now ?? Date.now;
2577
+ }
2578
+ /**
2579
+ * The refusal already on chain for exactly this bound, while it still
2580
+ * stands. A window that has rolled is a different window, and a refusal in
2581
+ * it is a new fact about a fresh budget — so the memory expires with it.
2582
+ */
2583
+ recall(bound) {
2584
+ const hit = this.seen.get(keyOf(bound));
2585
+ if (!hit) return void 0;
2586
+ if (hit.until <= this.now()) {
2587
+ this.seen.delete(keyOf(bound));
2588
+ return void 0;
2589
+ }
2590
+ return hit.published;
2591
+ }
2592
+ /** Remember what was published, for as long as it stays the current answer. */
2593
+ remember(bound, published, holdsForMs) {
2594
+ if (holdsForMs <= 0) return;
2595
+ this.seen.set(keyOf(bound), { published, until: this.now() + holdsForMs });
2596
+ }
2597
+ /**
2598
+ * Drop what has expired. Nothing depends on this being called — `recall`
2599
+ * checks expiry itself — but a daemon runs for weeks and a tree that has
2600
+ * refused many distinct bounds should not keep all of them.
2601
+ */
2602
+ prune() {
2603
+ const now = this.now();
2604
+ for (const [key, hit] of this.seen) if (hit.until <= now) this.seen.delete(key);
2605
+ }
2606
+ /** How many bounds are being remembered. For tests and for `/status`. */
2607
+ get size() {
2608
+ return this.seen.size;
2609
+ }
2610
+ };
2611
+
2374
2612
  // ../fixtures/src/serial.ts
2375
2613
  function serialiseByKey() {
2376
2614
  const tails = /* @__PURE__ */ new Map();
@@ -2413,6 +2651,10 @@ var Gate = class {
2413
2651
  * said it was in bounds. Keyed by node, so two nodes still draw at once.
2414
2652
  */
2415
2653
  inTurn = serialiseByKey();
2654
+ /** Identical refusals already on chain, so the loop pays for one. */
2655
+ repeats = new RefusalRepeats();
2656
+ /** `windowSeconds` per node, which is immutable once the mandate exists. */
2657
+ windowCache = /* @__PURE__ */ new Map();
2416
2658
  /**
2417
2659
  * The record. It borrows this gate's signer rather than holding a copy of a
2418
2660
  * key, so there is still exactly one place in the process where an operator
@@ -2421,10 +2663,20 @@ var Gate = class {
2421
2663
  recorder;
2422
2664
  constructor(config2) {
2423
2665
  this.config = config2;
2666
+ const facts = chainFacts(config2.chainId);
2667
+ if (!facts) {
2668
+ throw new Error(
2669
+ `chain ${config2.chainId} is not one this build knows. Chains live in packages/fixtures/src/index.ts and nowhere else.`
2670
+ );
2671
+ }
2424
2672
  const chain = defineChain({
2425
- id: config2.chainId,
2426
- name: `chain-${config2.chainId}`,
2427
- nativeCurrency: { name: "USDC", symbol: "USDC", decimals: 18 },
2673
+ id: facts.chainId,
2674
+ name: facts.name,
2675
+ nativeCurrency: {
2676
+ name: facts.nativeName,
2677
+ symbol: facts.nativeSymbol,
2678
+ decimals: facts.nativeDecimals
2679
+ },
2428
2680
  rpcUrls: { default: { http: [config2.rpcUrl] } }
2429
2681
  });
2430
2682
  this.chain = chain;
@@ -2557,6 +2809,31 @@ var Gate = class {
2557
2809
  args: [node2]
2558
2810
  });
2559
2811
  }
2812
+ /**
2813
+ * How long a refusal for this node stays the current answer.
2814
+ *
2815
+ * The window, because that is exactly when the budget it was refused
2816
+ * against is replaced — a refusal in a fresh window is a new fact. Read
2817
+ * once per node and kept: `windowSeconds` is set when the mandate is opened
2818
+ * and no function raises it.
2819
+ *
2820
+ * A read that fails returns 0, which suppresses nothing. A daemon that
2821
+ * cannot reach the registry should send the transaction, not invent a
2822
+ * silence.
2823
+ */
2824
+ async windowMs(node2) {
2825
+ const key = node2.toLowerCase();
2826
+ const cached = this.windowCache.get(key);
2827
+ if (cached !== void 0) return cached;
2828
+ try {
2829
+ const m = await this.mandate(node2);
2830
+ const ms = Number(m.windowSeconds) * 1e3;
2831
+ this.windowCache.set(key, ms);
2832
+ return ms;
2833
+ } catch {
2834
+ return 0;
2835
+ }
2836
+ }
2560
2837
  async mandate(node2) {
2561
2838
  return this.publicClient.readContract({
2562
2839
  address: this.config.registry,
@@ -2650,11 +2927,36 @@ var Gate = class {
2650
2927
  args: [node2, counterparty, amount],
2651
2928
  account: wallet.account
2652
2929
  });
2653
- const [wouldRelease] = result;
2930
+ const [wouldRelease, , reasonCode] = result;
2931
+ if (!wouldRelease) {
2932
+ const bound = { node: node2, counterparty, amount, reason: REASONS[reasonCode] ?? UNRECOGNISED };
2933
+ const already = this.repeats.recall(bound);
2934
+ if (already) {
2935
+ return {
2936
+ released: false,
2937
+ reason: already.reason,
2938
+ breachedAt: already.breachedAt,
2939
+ refusalId: already.refusalId,
2940
+ txHash: already.txHash
2941
+ };
2942
+ }
2943
+ }
2654
2944
  const txHash = await wallet.writeContract(request);
2655
2945
  const outcome = { ...await this.outcomeFrom(txHash), txHash };
2656
2946
  if (!wouldRelease) {
2657
2947
  await this.publish(node2, outcome);
2948
+ if (outcome.refusalId !== void 0 && outcome.txHash !== void 0) {
2949
+ this.repeats.remember(
2950
+ { node: node2, counterparty, amount, reason: outcome.reason },
2951
+ {
2952
+ refusalId: outcome.refusalId,
2953
+ txHash: outcome.txHash,
2954
+ reason: outcome.reason,
2955
+ breachedAt: outcome.breachedAt
2956
+ },
2957
+ await this.windowMs(node2)
2958
+ );
2959
+ }
2658
2960
  }
2659
2961
  return outcome;
2660
2962
  }
@@ -2769,6 +3071,87 @@ var Gate = class {
2769
3071
  // ../daemon/src/settle.ts
2770
3072
  import { parseAbi as parseAbi2 } from "viem";
2771
3073
 
3074
+ // ../daemon/src/challenge.ts
3075
+ var ChallengeError = class extends Error {
3076
+ };
3077
+ var ADDRESS = /^0x[0-9a-fA-F]{40}$/;
3078
+ var CAIP2 = /^[-a-z0-9]{3,8}:[-_a-zA-Z0-9]{1,32}$/;
3079
+ function parseChallenge(status, body) {
3080
+ if (status !== 402) return null;
3081
+ if (typeof body !== "object" || body === null) {
3082
+ throw new ChallengeError("402 with no JSON body");
3083
+ }
3084
+ const raw = body;
3085
+ const version = raw.x402Version;
3086
+ if (typeof version !== "number") {
3087
+ throw new ChallengeError("402 body has no x402Version");
3088
+ }
3089
+ const accepts = raw.accepts;
3090
+ if (!Array.isArray(accepts) || accepts.length === 0) {
3091
+ throw new ChallengeError("402 body offers no way to pay");
3092
+ }
3093
+ return { x402Version: version, offers: accepts.map(parseOffer) };
3094
+ }
3095
+ function parseOffer(input, index) {
3096
+ if (typeof input !== "object" || input === null) {
3097
+ throw new ChallengeError(`accepts[${index}] is not an object`);
3098
+ }
3099
+ const o = input;
3100
+ const scheme = str(o.scheme, `accepts[${index}].scheme`);
3101
+ const network = str(o.network, `accepts[${index}].network`);
3102
+ if (!CAIP2.test(network)) {
3103
+ throw new ChallengeError(`accepts[${index}].network is not a CAIP-2 id: ${network}`);
3104
+ }
3105
+ const payTo = str(o.payTo, `accepts[${index}].payTo`);
3106
+ const asset = str(o.asset, `accepts[${index}].asset`);
3107
+ const amountRaw = o.amount;
3108
+ if (typeof amountRaw !== "string" || !/^\d+$/.test(amountRaw)) {
3109
+ throw new ChallengeError(`accepts[${index}].amount is not a base-unit string`);
3110
+ }
3111
+ const amount = BigInt(amountRaw);
3112
+ if (amount <= 0n) {
3113
+ throw new ChallengeError(`accepts[${index}].amount is not positive`);
3114
+ }
3115
+ return {
3116
+ scheme,
3117
+ network,
3118
+ asset,
3119
+ payTo,
3120
+ amount,
3121
+ maxTimeoutSeconds: typeof o.maxTimeoutSeconds === "number" ? o.maxTimeoutSeconds : void 0,
3122
+ extra: typeof o.extra === "object" && o.extra !== null ? o.extra : void 0
3123
+ };
3124
+ }
3125
+ function str(v, where) {
3126
+ if (typeof v !== "string" || v.length === 0) {
3127
+ throw new ChallengeError(`${where} is missing`);
3128
+ }
3129
+ return v;
3130
+ }
3131
+ var MAX_AUTH_SECONDS = 600;
3132
+ function selectOffer(challenge, ok) {
3133
+ const scheme = ok.scheme ?? "exact";
3134
+ const networks = new Set(ok.networks);
3135
+ const assets = new Set(ok.assets.map((a) => a.toLowerCase()));
3136
+ const match = challenge.offers.find(
3137
+ (o) => o.scheme === scheme && networks.has(o.network) && assets.has(o.asset.toLowerCase())
3138
+ );
3139
+ if (!match) {
3140
+ throw new ChallengeError(
3141
+ `no offer this daemon can settle: seller accepts ${challenge.offers.map((o) => `${o.scheme}/${o.network}`).join(", ")}`
3142
+ );
3143
+ }
3144
+ if (match.network.startsWith("eip155:") && !ADDRESS.test(match.payTo)) {
3145
+ throw new ChallengeError(`payTo is not an address: ${match.payTo}`);
3146
+ }
3147
+ if (match.maxTimeoutSeconds !== void 0 && match.maxTimeoutSeconds > MAX_AUTH_SECONDS) {
3148
+ throw new ChallengeError(
3149
+ `seller asks for an authorisation valid ${match.maxTimeoutSeconds}s; this daemon signs none longer than ${MAX_AUTH_SECONDS}s`
3150
+ );
3151
+ }
3152
+ return match;
3153
+ }
3154
+
2772
3155
  // ../daemon/src/gateway.ts
2773
3156
  import { pad, toHex, parseAbi } from "viem";
2774
3157
  var GATEWAY_EIP712_DOMAIN = { name: "GatewayWallet", version: "1" };
@@ -2986,56 +3369,83 @@ var CircleSettler = class {
2986
3369
  * one that surfaces as a seller's revert against a signature that verified.
2987
3370
  */
2988
3371
  async assertCanPay(payment, operator) {
2989
- const read = this.options.balanceOf ?? ((token, holder) => this.options.publicClient.readContract({
2990
- address: token,
2991
- abi: ERC20,
2992
- functionName: "balanceOf",
2993
- args: [holder]
2994
- }));
2995
- const held = await read(payment.asset, operator);
3372
+ const held = await readBalance(this.options, payment.asset, operator);
2996
3373
  if (held >= payment.value) return;
2997
3374
  throw new SettlementError(
2998
3375
  `${operator} holds ${held} base units and this purchase authorises ${payment.value}. The burn returned the price less Circle's ${GATEWAY.baseFee6} fee, so an operator needs that difference in its own float \u2014 the same place its gas comes from, and outside the mandate either way. The draw has already happened, so the window is debited.`
2999
3376
  );
3000
3377
  }
3001
3378
  /** The x402 `exact` payload: an EIP-3009 authorisation the seller submits. */
3002
- async authorise(wallet, operator, payment, token) {
3003
- const now = Math.floor((this.options.now?.() ?? Date.now()) / 1e3);
3004
- const authorization = {
3005
- from: operator,
3006
- to: payment.to,
3007
- value: payment.value,
3008
- validAfter: 0n,
3009
- validBefore: BigInt(now + (payment.maxTimeoutSeconds ?? 300)),
3010
- nonce: this.options.nonce?.() ?? randomNonce()
3011
- };
3012
- const signature = await wallet.signTypedData({
3013
- account: wallet.account,
3014
- domain: token,
3015
- types: TRANSFER_WITH_AUTHORIZATION_TYPES,
3016
- primaryType: "TransferWithAuthorization",
3017
- message: authorization
3018
- });
3019
- return Buffer.from(
3020
- JSON.stringify({
3021
- x402Version: 2,
3022
- scheme: "exact",
3023
- network: payment.network,
3024
- asset: payment.asset,
3025
- payload: {
3026
- signature,
3027
- authorization: {
3028
- ...authorization,
3029
- value: authorization.value.toString(),
3030
- validAfter: authorization.validAfter.toString(),
3031
- validBefore: authorization.validBefore.toString()
3032
- }
3033
- }
3034
- }),
3035
- "utf8"
3036
- ).toString("base64");
3379
+ authorise(wallet, operator, payment, token) {
3380
+ return authoriseExact(wallet, operator, payment, token, this.options);
3037
3381
  }
3038
3382
  };
3383
+ var DirectSettler = class {
3384
+ options;
3385
+ constructor(options) {
3386
+ this.options = options;
3387
+ }
3388
+ async settle(node2, payment) {
3389
+ const wallet = this.options.walletFor(node2);
3390
+ const operator = wallet.account.address;
3391
+ const token = tokenDomain(payment, this.options.chainId);
3392
+ const held = await readBalance(this.options, payment.asset, operator);
3393
+ if (held < payment.value) {
3394
+ throw new SettlementError(
3395
+ `${operator} holds ${held} base units and this purchase authorises ${payment.value}. On this chain the vault deposits into the operator's own balance, so a draw that returned should have covered it \u2014 a shortfall here means the money left again. The draw has already happened, so the window is debited.`
3396
+ );
3397
+ }
3398
+ return { proof: await authoriseExact(wallet, operator, payment, token, this.options) };
3399
+ }
3400
+ };
3401
+ function readBalance(options, token, holder) {
3402
+ if (options.balanceOf) return options.balanceOf(token, holder);
3403
+ return options.publicClient.readContract({
3404
+ address: token,
3405
+ abi: ERC20,
3406
+ functionName: "balanceOf",
3407
+ args: [holder]
3408
+ });
3409
+ }
3410
+ async function authoriseExact(wallet, operator, payment, token, clock) {
3411
+ const now = Math.floor((clock.now?.() ?? Date.now()) / 1e3);
3412
+ const authorization = {
3413
+ from: operator,
3414
+ to: payment.to,
3415
+ value: payment.value,
3416
+ validAfter: 0n,
3417
+ /* Clamped at the signature, not only at the offer: this is the last point
3418
+ before a bearer instrument exists, and it is the one that must hold
3419
+ however the payment got here. */
3420
+ validBefore: BigInt(now + Math.min(payment.maxTimeoutSeconds ?? 300, MAX_AUTH_SECONDS)),
3421
+ nonce: clock.nonce?.() ?? randomNonce()
3422
+ };
3423
+ const signature = await wallet.signTypedData({
3424
+ account: wallet.account,
3425
+ domain: token,
3426
+ types: TRANSFER_WITH_AUTHORIZATION_TYPES,
3427
+ primaryType: "TransferWithAuthorization",
3428
+ message: authorization
3429
+ });
3430
+ return Buffer.from(
3431
+ JSON.stringify({
3432
+ x402Version: 2,
3433
+ scheme: "exact",
3434
+ network: payment.network,
3435
+ asset: payment.asset,
3436
+ payload: {
3437
+ signature,
3438
+ authorization: {
3439
+ ...authorization,
3440
+ value: authorization.value.toString(),
3441
+ validAfter: authorization.validAfter.toString(),
3442
+ validBefore: authorization.validBefore.toString()
3443
+ }
3444
+ }
3445
+ }),
3446
+ "utf8"
3447
+ ).toString("base64");
3448
+ }
3039
3449
  function tokenDomain(payment, chainId) {
3040
3450
  const extra = payment.extra ?? {};
3041
3451
  const name = typeof extra.name === "string" ? extra.name : void 0;
@@ -3158,16 +3568,31 @@ CORDON_NODE_${label}=
3158
3568
  }
3159
3569
 
3160
3570
  // ../daemon/src/released.ts
3161
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
3571
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, renameSync as renameSync2, writeFileSync as writeFileSync2 } from "node:fs";
3162
3572
  import { dirname as dirname2 } from "node:path";
3163
3573
  var ChainReleases = class {
3164
3574
  options;
3165
3575
  spent;
3576
+ /**
3577
+ * One claim at a time per node.
3578
+ *
3579
+ * `claimInTurn` reads the chain twice between finding an unspent release and
3580
+ * marking it spent, and an `await` is where another request gets to run. Two
3581
+ * concurrent `POST /fetch` for the same purchase both passed the `spent`
3582
+ * check, both reserved the same release, and one owner signature paid twice.
3583
+ *
3584
+ * `gate.draw` was already serialised per node for the nonce; this was not,
3585
+ * and `cordonFetch` calls it first. Same queue, same key.
3586
+ */
3587
+ inTurn = serialiseByKey();
3166
3588
  constructor(options) {
3167
3589
  this.options = options;
3168
3590
  this.spent = existsSync2(options.file) ? new Set(JSON.parse(readFileSync2(options.file, "utf8")).map(String)) : /* @__PURE__ */ new Set();
3169
3591
  }
3170
- async claim(node2, payTo, amount6) {
3592
+ claim(node2, payTo, amount6) {
3593
+ return this.inTurn(node2, () => this.claimInTurn(node2, payTo, amount6));
3594
+ }
3595
+ async claimInTurn(node2, payTo, amount6) {
3171
3596
  const count = await this.options.count();
3172
3597
  const scan = BigInt(this.options.scan ?? 256);
3173
3598
  const floor = count > scan ? count - scan : 0n;
@@ -3189,9 +3614,18 @@ var ChainReleases = class {
3189
3614
  unclaim(refusalId) {
3190
3615
  if (this.spent.delete(refusalId.toString())) this.save();
3191
3616
  }
3617
+ /**
3618
+ * Written beside and renamed over, the way `keyfile.ts` writes.
3619
+ *
3620
+ * A crash during `writeFileSync` leaves a truncated file, and this one is
3621
+ * parsed in the constructor — so a half-written ledger of spent releases
3622
+ * does not cost a purchase, it stops the daemon starting at all.
3623
+ */
3192
3624
  save() {
3193
3625
  mkdirSync2(dirname2(this.options.file), { recursive: true });
3194
- writeFileSync2(this.options.file, JSON.stringify([...this.spent], null, 2) + "\n");
3626
+ const staging = `${this.options.file}.tmp-${process.pid}`;
3627
+ writeFileSync2(staging, JSON.stringify([...this.spent], null, 2) + "\n");
3628
+ renameSync2(staging, this.options.file);
3195
3629
  }
3196
3630
  };
3197
3631
 
@@ -3203,80 +3637,9 @@ import { z } from "zod";
3203
3637
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3204
3638
  import { generatePrivateKey } from "viem/accounts";
3205
3639
 
3206
- // ../daemon/src/challenge.ts
3207
- var ChallengeError = class extends Error {
3208
- };
3209
- var ADDRESS = /^0x[0-9a-fA-F]{40}$/;
3210
- var CAIP2 = /^[-a-z0-9]{3,8}:[-_a-zA-Z0-9]{1,32}$/;
3211
- function parseChallenge(status, body) {
3212
- if (status !== 402) return null;
3213
- if (typeof body !== "object" || body === null) {
3214
- throw new ChallengeError("402 with no JSON body");
3215
- }
3216
- const raw = body;
3217
- const version = raw.x402Version;
3218
- if (typeof version !== "number") {
3219
- throw new ChallengeError("402 body has no x402Version");
3220
- }
3221
- const accepts = raw.accepts;
3222
- if (!Array.isArray(accepts) || accepts.length === 0) {
3223
- throw new ChallengeError("402 body offers no way to pay");
3224
- }
3225
- return { x402Version: version, offers: accepts.map(parseOffer) };
3226
- }
3227
- function parseOffer(input, index) {
3228
- if (typeof input !== "object" || input === null) {
3229
- throw new ChallengeError(`accepts[${index}] is not an object`);
3230
- }
3231
- const o = input;
3232
- const scheme = str(o.scheme, `accepts[${index}].scheme`);
3233
- const network = str(o.network, `accepts[${index}].network`);
3234
- if (!CAIP2.test(network)) {
3235
- throw new ChallengeError(`accepts[${index}].network is not a CAIP-2 id: ${network}`);
3236
- }
3237
- const payTo = str(o.payTo, `accepts[${index}].payTo`);
3238
- const asset = str(o.asset, `accepts[${index}].asset`);
3239
- const amountRaw = o.amount;
3240
- if (typeof amountRaw !== "string" || !/^\d+$/.test(amountRaw)) {
3241
- throw new ChallengeError(`accepts[${index}].amount is not a base-unit string`);
3242
- }
3243
- const amount = BigInt(amountRaw);
3244
- if (amount <= 0n) {
3245
- throw new ChallengeError(`accepts[${index}].amount is not positive`);
3246
- }
3247
- return {
3248
- scheme,
3249
- network,
3250
- asset,
3251
- payTo,
3252
- amount,
3253
- maxTimeoutSeconds: typeof o.maxTimeoutSeconds === "number" ? o.maxTimeoutSeconds : void 0,
3254
- extra: typeof o.extra === "object" && o.extra !== null ? o.extra : void 0
3255
- };
3256
- }
3257
- function str(v, where) {
3258
- if (typeof v !== "string" || v.length === 0) {
3259
- throw new ChallengeError(`${where} is missing`);
3260
- }
3261
- return v;
3262
- }
3263
- function selectOffer(challenge, ok) {
3264
- const scheme = ok.scheme ?? "exact";
3265
- const networks = new Set(ok.networks);
3266
- const assets = new Set(ok.assets.map((a) => a.toLowerCase()));
3267
- const match = challenge.offers.find(
3268
- (o) => o.scheme === scheme && networks.has(o.network) && assets.has(o.asset.toLowerCase())
3269
- );
3270
- if (!match) {
3271
- throw new ChallengeError(
3272
- `no offer this daemon can settle: seller accepts ${challenge.offers.map((o) => `${o.scheme}/${o.network}`).join(", ")}`
3273
- );
3274
- }
3275
- if (match.network.startsWith("eip155:") && !ADDRESS.test(match.payTo)) {
3276
- throw new ChallengeError(`payTo is not an address: ${match.payTo}`);
3277
- }
3278
- return match;
3279
- }
3640
+ // ../daemon/src/fetch.ts
3641
+ import http2 from "node:http";
3642
+ import https from "node:https";
3280
3643
 
3281
3644
  // ../daemon/src/egress.ts
3282
3645
  import { lookup } from "node:dns/promises";
@@ -3342,11 +3705,65 @@ async function assertPublicUrl(raw, options = {}) {
3342
3705
  }
3343
3706
  return url;
3344
3707
  }
3708
+ var publicAddressesOnly = (address2) => !isPrivateAddress(address2);
3709
+ function pinnedLookup(allow = publicAddressesOnly) {
3710
+ return (hostname, _options, callback) => {
3711
+ const literal = isIP(hostname);
3712
+ if (literal) {
3713
+ if (!allow(hostname)) {
3714
+ return callback(
3715
+ Object.assign(new EgressError(`${hostname} is not an address this daemon connects to`), {
3716
+ code: "ECONNREFUSED"
3717
+ })
3718
+ );
3719
+ }
3720
+ return callback(null, hostname, literal);
3721
+ }
3722
+ lookup(hostname, { all: true, verbatim: true }).then(
3723
+ (answers) => {
3724
+ for (const answer of answers) {
3725
+ if (!allow(answer.address)) {
3726
+ return callback(
3727
+ Object.assign(
3728
+ new EgressError(
3729
+ `${hostname} resolves to ${answer.address}, which is on this machine or its private network. The connection was refused at the socket, so no request was sent to it.`
3730
+ ),
3731
+ { code: "ECONNREFUSED" }
3732
+ )
3733
+ );
3734
+ }
3735
+ }
3736
+ const chosen = answers[0];
3737
+ if (!chosen) {
3738
+ return callback(
3739
+ Object.assign(new EgressError(`${hostname} does not resolve, so there is nothing to ask`), {
3740
+ code: "ENOTFOUND"
3741
+ })
3742
+ );
3743
+ }
3744
+ return callback(null, chosen.address, chosen.family);
3745
+ },
3746
+ (error) => callback(Object.assign(error, { code: "ENOTFOUND" }))
3747
+ );
3748
+ };
3749
+ }
3345
3750
 
3346
3751
  // ../daemon/src/fetch.ts
3752
+ var PURCHASE_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "POST"]);
3753
+ var MethodRefused = class extends Error {
3754
+ };
3755
+ function assertPurchaseMethod(raw) {
3756
+ const method = raw.toUpperCase();
3757
+ if (!PURCHASE_METHODS.has(method)) {
3758
+ throw new MethodRefused(
3759
+ `${method} is not a method this daemon fetches with. A purchase is a request answered with 402 and retried with a signature, so GET, HEAD and POST are what that needs. A verb whose point is to leave state on someone else's server is an egress channel, and this tool is not one.`
3760
+ );
3761
+ }
3762
+ return method;
3763
+ }
3347
3764
  async function cordonFetch(request, deps) {
3348
3765
  const { gate: gate2, settler, acceptable, transport } = deps;
3349
- const method = request.method ?? "GET";
3766
+ const method = assertPurchaseMethod(request.method ?? "GET");
3350
3767
  const headers = { ...request.headers };
3351
3768
  const first = await transport(request.url, { method, headers, body: request.body });
3352
3769
  const challenge = parseChallenge(first.status, first.body);
@@ -3393,35 +3810,67 @@ async function cordonFetch(request, deps) {
3393
3810
  };
3394
3811
  }
3395
3812
  var MAX_REDIRECTS = 3;
3396
- var httpTransport = (url, init) => httpTransportWith(assertPublicUrl)(url, init);
3397
- var httpTransportWith = (assert) => async (url, init) => {
3813
+ var httpTransport = (url, init) => httpTransportWith(assertPublicUrl, publicAddressesOnly)(url, init);
3814
+ function requestOnce(target, init, allow) {
3815
+ const secure = target.protocol === "https:";
3816
+ const driver = secure ? https : http2;
3817
+ const agent = new driver.Agent({ lookup: pinnedLookup(allow) });
3818
+ return new Promise((resolve, reject) => {
3819
+ const req = driver.request(
3820
+ {
3821
+ protocol: target.protocol,
3822
+ hostname: target.hostname,
3823
+ port: target.port || (secure ? 443 : 80),
3824
+ path: `${target.pathname}${target.search}`,
3825
+ method: init.method,
3826
+ headers: init.headers,
3827
+ agent,
3828
+ /* TLS is still verified against the NAME, not the pinned address: the
3829
+ certificate has to be for the host the daemon meant to reach. */
3830
+ ...secure ? { servername: target.hostname } : {}
3831
+ },
3832
+ (res) => {
3833
+ const chunks = [];
3834
+ res.on("data", (chunk) => {
3835
+ chunks.push(chunk);
3836
+ if (chunks.reduce((n, c) => n + c.length, 0) > MAX_BODY_BYTES) {
3837
+ req.destroy(new EgressError(`${target.origin} sent more than ${MAX_BODY_BYTES} bytes`));
3838
+ }
3839
+ });
3840
+ res.on("end", () => {
3841
+ const headers = {};
3842
+ for (const [name, value] of Object.entries(res.headers)) {
3843
+ if (value === void 0) continue;
3844
+ headers[name] = Array.isArray(value) ? value.join(", ") : value;
3845
+ }
3846
+ resolve({ status: res.statusCode ?? 0, headers, body: Buffer.concat(chunks).toString("utf8") });
3847
+ });
3848
+ res.on("error", reject);
3849
+ }
3850
+ );
3851
+ req.setTimeout(3e4, () => req.destroy(new EgressError(`${target.origin} did not answer in 30s`)));
3852
+ req.on("error", reject);
3853
+ if (init.body !== void 0) req.write(init.body);
3854
+ req.end();
3855
+ });
3856
+ }
3857
+ var MAX_BODY_BYTES = 8 * 1024 * 1024;
3858
+ var httpTransportWith = (assert, allow) => async (url, init) => {
3398
3859
  let target = await assert(url);
3399
3860
  const origin = target.origin;
3400
3861
  const carriesPayment = Object.keys(init.headers).some(
3401
3862
  (name) => name.toLowerCase() === "payment-signature" || name.toLowerCase() === "x-payment"
3402
3863
  );
3403
3864
  for (let hop = 0; ; hop++) {
3404
- const response = await fetch(target, {
3405
- method: init.method,
3406
- headers: init.headers,
3407
- body: init.body,
3408
- /* Followed below instead, so each hop is checked. */
3409
- redirect: "manual",
3410
- signal: AbortSignal.timeout(3e4)
3411
- });
3412
- const location = response.headers.get("location");
3865
+ const response = await requestOnce(target, init, allow);
3866
+ const location = response.headers["location"];
3413
3867
  if (response.status < 300 || response.status >= 400 || !location) {
3414
- const text2 = await response.text();
3415
- let body = text2;
3868
+ let body = response.body;
3416
3869
  try {
3417
- body = JSON.parse(text2);
3870
+ body = JSON.parse(response.body);
3418
3871
  } catch {
3419
3872
  }
3420
- return {
3421
- status: response.status,
3422
- headers: Object.fromEntries(response.headers.entries()),
3423
- body
3424
- };
3873
+ return { status: response.status, headers: response.headers, body };
3425
3874
  }
3426
3875
  if (hop >= MAX_REDIRECTS) {
3427
3876
  throw new EgressError(`${url} redirected more than ${MAX_REDIRECTS} times`);
@@ -3480,11 +3929,58 @@ function renderPaid(outcome, offer, body) {
3480
3929
  ].filter(Boolean).join("\n");
3481
3930
  }
3482
3931
 
3932
+ // package.json
3933
+ var package_default = {
3934
+ name: "cordon-mcp",
3935
+ version: "0.2.0",
3936
+ description: "Cordon inside an MCP client: an agent that can buy things and cannot spend past the mandate its owner signed, bounded by contracts it cannot reach.",
3937
+ license: "MIT",
3938
+ author: "Youvandra Febrial",
3939
+ homepage: "https://getcordon.xyz",
3940
+ bugs: {
3941
+ url: "https://github.com/youvandra/cordon/issues"
3942
+ },
3943
+ repository: {
3944
+ type: "git",
3945
+ url: "git+https://github.com/youvandra/cordon.git",
3946
+ directory: "packages/mcp"
3947
+ },
3948
+ keywords: ["mcp", "mcp-server", "x402", "agents", "ai-agents", "agent-payments", "spending-limits", "arc", "sepolia", "circle", "usdc", "erc-8004", "ens", "ensv2"],
3949
+ type: "module",
3950
+ bin: {
3951
+ "cordon-mcp": "dist/cordon-mcp.js"
3952
+ },
3953
+ files: ["dist", "SKILL.md", "README.md", "LICENSE"],
3954
+ engines: {
3955
+ node: ">=22"
3956
+ },
3957
+ publishConfig: {
3958
+ access: "public"
3959
+ },
3960
+ scripts: {
3961
+ build: "tsc --noEmit",
3962
+ bundle: "esbuild src/main.ts --bundle --platform=node --format=esm --target=node22 --outfile=dist/cordon-mcp.js --external:viem --external:@modelcontextprotocol/sdk --external:zod",
3963
+ prepack: "npm run bundle",
3964
+ test: 'node --test "test/*.test.ts"',
3965
+ start: "node src/main.ts"
3966
+ },
3967
+ dependencies: {
3968
+ "@modelcontextprotocol/sdk": "^1.12.0",
3969
+ viem: "^2.21.55",
3970
+ zod: "^4.5.4"
3971
+ },
3972
+ devDependencies: {
3973
+ "@types/node": "^22.10.2",
3974
+ esbuild: "^0.25.12",
3975
+ typescript: "^5.7.2"
3976
+ }
3977
+ };
3978
+
3483
3979
  // src/server.ts
3484
3980
  var text = (s) => ({ content: [{ type: "text", text: s }] });
3485
3981
  function createMcpServer(deps) {
3486
3982
  const server2 = new McpServer(
3487
- { name: "cordon", version: "0.1.0" },
3983
+ { name: "cordon", version: package_default.version },
3488
3984
  {
3489
3985
  instructions: "Cordon bounds what this agent can spend. Use cordon_fetch for any URL that may charge; it pays through a contract that can refuse. There is no tool here that sends money to an address, and a refusal is a final answer rather than an error to retry."
3490
3986
  }
@@ -3627,6 +4123,19 @@ if (!node) {
3627
4123
  );
3628
4124
  process.exit(1);
3629
4125
  }
4126
+ function settlerFor(chainId) {
4127
+ const options = {
4128
+ publicClient: gate.publicClientForSettlement,
4129
+ walletFor: (node2) => gate.signerFor(node2),
4130
+ chainId
4131
+ };
4132
+ return chainId === ARC.chainId ? new CircleSettler(options) : new DirectSettler(options);
4133
+ }
4134
+ function explorerFor(chainId) {
4135
+ if (chainId === ARC.chainId) return ARC.explorer;
4136
+ if (chainId === SEPOLIA.chainId) return SEPOLIA.explorer;
4137
+ return void 0;
4138
+ }
3630
4139
  var server = createMcpServer({
3631
4140
  gate,
3632
4141
  keyFile: keyFileAt(config.keyFile),
@@ -3638,14 +4147,10 @@ var server = createMcpServer({
3638
4147
  live: (node2) => gate.isLive(node2),
3639
4148
  file: join2(dirname3(config.keyFile), "released-spent.json")
3640
4149
  }),
3641
- settler: new CircleSettler({
3642
- publicClient: gate.publicClientForSettlement,
3643
- walletFor: (node2) => gate.signerFor(node2),
3644
- chainId: config.chainId
3645
- }),
4150
+ settler: settlerFor(config.chainId),
3646
4151
  acceptable: { networks: config.networks, assets: config.assets },
3647
4152
  node,
3648
- explorer: config.chainId === ARC.chainId ? ARC.explorer : void 0
4153
+ explorer: explorerFor(config.chainId)
3649
4154
  });
3650
4155
  console.error(`cordon mcp: node ${node} on chain ${config.chainId}`);
3651
4156
  await server.connect(new StdioServerTransport());