@piprail/sdk 2.16.2 → 3.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 (37) hide show
  1. package/CHANGELOG.md +448 -0
  2. package/README.md +1 -0
  3. package/dist/{algorand-Q3TUQLPK.cjs → algorand-4FTTEV7X.cjs} +212 -51
  4. package/dist/{algorand-25FMBCT3.js → algorand-KJ5XHSMT.js} +164 -3
  5. package/dist/{aptos-VBJONBFY.cjs → aptos-5QBXJ6NV.cjs} +255 -39
  6. package/dist/{aptos-SBV6SGDP.js → aptos-H5VLH2QL.js} +217 -1
  7. package/dist/{chunk-QONQSZHJ.cjs → chunk-6XTNI2OQ.cjs} +44 -44
  8. package/dist/{chunk-OXEFPLZA.cjs → chunk-6ZRAIQXF.cjs} +4 -4
  9. package/dist/chunk-GVCGUSTE.js +30 -0
  10. package/dist/{chunk-V2IJ5HUW.cjs → chunk-MZAJQYM3.cjs} +1 -0
  11. package/dist/chunk-MZXVXC3C.cjs +30 -0
  12. package/dist/{chunk-2CX7XRZK.js → chunk-TZDVZCTC.js} +1 -0
  13. package/dist/{chunk-C52H5TYB.js → chunk-YXBQKBDH.js} +2 -2
  14. package/dist/index.cjs +2906 -1262
  15. package/dist/index.d.cts +679 -21
  16. package/dist/index.d.ts +679 -21
  17. package/dist/index.js +2678 -1034
  18. package/dist/{ledger-Crc1bZox.d.cts → ledger-DkHUORUe.d.cts} +7 -0
  19. package/dist/{ledger-Crc1bZox.d.ts → ledger-DkHUORUe.d.ts} +7 -0
  20. package/dist/{near-OLKCMTBI.js → near-5X2CQGML.js} +197 -1
  21. package/dist/{near-5LTTDU6G.cjs → near-XSQXEIHT.cjs} +242 -46
  22. package/dist/node.d.cts +2 -2
  23. package/dist/node.d.ts +2 -2
  24. package/dist/{solana-AI2G7V33.cjs → solana-TIEJV742.cjs} +218 -53
  25. package/dist/{solana-HTKDRTD3.js → solana-WRXL54MR.js} +174 -9
  26. package/dist/{stellar-E2KWEV2E.cjs → stellar-5GMZJBTA.cjs} +200 -30
  27. package/dist/{stellar-EUFZLX6J.js → stellar-YQLOIFDD.js} +172 -2
  28. package/dist/{sui-KCIITCYH.js → sui-JFQSDNSZ.js} +165 -3
  29. package/dist/{sui-Q3NJOJZS.cjs → sui-WIZEAO7E.cjs} +185 -23
  30. package/dist/{ton-J7TQWRN4.cjs → ton-AGTKLQA5.cjs} +264 -22
  31. package/dist/{ton-WH2JVQOO.js → ton-OZKFNRLT.js} +244 -2
  32. package/dist/{tron-FXBXDNEY.js → tron-HG3IOOCG.js} +283 -1
  33. package/dist/{tron-HIPMOX7S.cjs → tron-S4WX7OHY.cjs} +323 -41
  34. package/dist/{xrpl-RUOB37QH.js → xrpl-5K444RGX.js} +238 -6
  35. package/dist/{xrpl-SVVS445B.cjs → xrpl-G2FKFXRI.cjs} +289 -57
  36. package/package.json +12 -2
  37. package/dist/{chunk-QU25LSVS.js → chunk-5GRBEMCA.js} +44 -44
package/dist/index.js CHANGED
@@ -40,7 +40,7 @@ import {
40
40
  parseUptoPaymentHeader,
41
41
  pickAccept,
42
42
  readPaymentIdentifier
43
- } from "./chunk-C52H5TYB.js";
43
+ } from "./chunk-YXBQKBDH.js";
44
44
  import {
45
45
  DIRECTORY_INFO,
46
46
  REGISTER_ATTRIBUTION,
@@ -56,7 +56,16 @@ import {
56
56
  scoreResource,
57
57
  searchOpenIndexes,
58
58
  verify402IndexDomain
59
- } from "./chunk-2CX7XRZK.js";
59
+ } from "./chunk-TZDVZCTC.js";
60
+ import {
61
+ AGENT_MODES,
62
+ DEFAULT_AGENT_MODE,
63
+ DEFAULT_SLIPPAGE_BPS,
64
+ MAX_SLIPPAGE_BPS,
65
+ applySlippage,
66
+ resolveSlippageBps,
67
+ summarizeSwap
68
+ } from "./chunk-GVCGUSTE.js";
60
69
  import {
61
70
  ConfirmationTimeoutError,
62
71
  InsufficientFundsError,
@@ -85,7 +94,7 @@ import {
85
94
  parseUnits,
86
95
  rejectForeignToken,
87
96
  toInsufficientFundsError
88
- } from "./chunk-QU25LSVS.js";
97
+ } from "./chunk-5GRBEMCA.js";
89
98
 
90
99
  // src/drivers/registry.ts
91
100
  var byFamily = /* @__PURE__ */ new Map();
@@ -330,6 +339,33 @@ var CHAINS = {
330
339
  tokens: {
331
340
  USDT: { address: "0xd077A400968890Eacc75cdc901F0356c943e4fDb", decimals: 6, symbol: "USDT" }
332
341
  }
342
+ },
343
+ /**
344
+ * Robinhood Chain — Arbitrum Orbit L2 for tokenized equities, mainnet since 2026-07-01.
345
+ * viem ships no preset for it (it is not in chainid.network either), so the chain is
346
+ * defined inline from the values Robinhood publishes.
347
+ *
348
+ * ONE stablecoin on purpose: USDG, the Paxos-issued Global Dollar, which is the asset the
349
+ * chain's own markets quote against. Circle does NOT issue native USDC here (verified
350
+ * against Circle's contract list 2026-09-08), so no USDC/USDT preset ships — a bridged
351
+ * one would break the "issuer-native only" token rule.
352
+ */
353
+ robinhood: {
354
+ chain: defineChain({
355
+ id: 4663,
356
+ name: "Robinhood Chain",
357
+ nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
358
+ rpcUrls: { default: { http: ["https://rpc.mainnet.chain.robinhood.com"] } },
359
+ blockExplorers: {
360
+ default: { name: "Blockscout", url: "https://robinhoodchain.blockscout.com" }
361
+ }
362
+ }),
363
+ tokens: {
364
+ // Paxos Global Dollar. EIP-3009 AND EIP-2612 verified on-chain 2026-09-08 (exact-payable);
365
+ // the EIP-712 domain name is "Global Dollar" (NOT "USDG") — the buyer re-derives it from
366
+ // the contract, so the symbol below is display-only, same as Ethereum's EURC.
367
+ USDG: { address: "0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168", decimals: 6, symbol: "USDG" }
368
+ }
333
369
  }
334
370
  };
335
371
  function isViemChain(input) {
@@ -365,6 +401,11 @@ function resolveChain(input, rpcUrlOverride) {
365
401
  }
366
402
  return { chain: input, chainId: input.id, rpcUrl: rpcUrl2, tokens: knownTokensForId(input.id) };
367
403
  }
404
+ if (!Number.isSafeInteger(input.id) || input.id <= 0) {
405
+ throw new Error(
406
+ `resolveChain: chain id must be a positive safe integer (EIP-155), got ${String(input.id)}.`
407
+ );
408
+ }
368
409
  const rpcUrl = rpcUrlOverride ?? input.rpcUrl;
369
410
  if (!rpcUrl) {
370
411
  throw new Error(`resolveChain: chain ${input.id} needs an rpcUrl.`);
@@ -383,7 +424,7 @@ import {
383
424
  createWalletClient,
384
425
  http
385
426
  } from "viem";
386
- import { privateKeyToAccount } from "viem/accounts";
427
+ import { privateKeyToAccount, nonceManager } from "viem/accounts";
387
428
  function createWalletAdapter(config, resolved) {
388
429
  assertNoLegacyWalletKey(config, "EVM");
389
430
  if ("key" in config) {
@@ -396,7 +437,7 @@ function createWalletAdapter(config, resolved) {
396
437
  }
397
438
  let account;
398
439
  try {
399
- account = privateKeyToAccount(key);
440
+ account = privateKeyToAccount(key, { nonceManager });
400
441
  } catch (err) {
401
442
  throw new WrongFamilyError(
402
443
  `chain is EVM; the wallet { key } is not a valid 0x\u2026 32-byte hex private key: ${err instanceof Error ? err.message : String(err)}.`
@@ -446,6 +487,259 @@ async function payEvm(input) {
446
487
  });
447
488
  }
448
489
 
490
+ // src/drivers/evm/swap.ts
491
+ var KYBER_API = "https://aggregator-api.kyberswap.com";
492
+ var KYBER_CHAIN = {
493
+ 1: "ethereum",
494
+ 10: "optimism",
495
+ 56: "bsc",
496
+ 137: "polygon",
497
+ 8453: "base",
498
+ 42161: "arbitrum",
499
+ 43114: "avalanche",
500
+ 59144: "linea",
501
+ 4663: "robinhood"
502
+ };
503
+ var NATIVE_SENTINEL = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";
504
+ var ERC20_ALLOWANCE_ABI = [
505
+ {
506
+ type: "function",
507
+ name: "allowance",
508
+ stateMutability: "view",
509
+ inputs: [
510
+ { name: "owner", type: "address" },
511
+ { name: "spender", type: "address" }
512
+ ],
513
+ outputs: [{ type: "uint256" }]
514
+ },
515
+ {
516
+ type: "function",
517
+ name: "approve",
518
+ stateMutability: "nonpayable",
519
+ inputs: [
520
+ { name: "spender", type: "address" },
521
+ { name: "amount", type: "uint256" }
522
+ ],
523
+ outputs: [{ type: "bool" }]
524
+ }
525
+ ];
526
+ function sourceFor(needsApproval, extraFee) {
527
+ const charged = !!(extraFee?.feeAmount && extraFee.feeAmount !== "0" && extraFee.feeReceiver);
528
+ return {
529
+ kind: "provider",
530
+ name: "KyberSwap",
531
+ note: "Third-party EVM DEX aggregator (aggregator-api.kyberswap.com), used keyless. It routes; your own wallet calls the router contract directly and nobody takes possession of the funds. " + (charged ? `\u26A0\uFE0F THIS QUOTE CARRIES AN INTEGRATOR FEE: ${extraFee.feeAmount} to ${extraFee.feeReceiver}. PipRail did not set one. ` : "This quote carries no integrator fee, and PipRail adds none. ") + (needsApproval ? "This swap needs TWO transactions: an ERC-20 approve to the router, then the swap." : "Native-in, so no ERC-20 approval is needed.")
532
+ };
533
+ }
534
+ function addrFor(t) {
535
+ return t.asset === "native" ? NATIVE_SENTINEL : t.asset;
536
+ }
537
+ function side(t, amount, nativeSymbol) {
538
+ return {
539
+ asset: t.asset,
540
+ symbol: t.symbol ?? (t.asset === "native" ? nativeSymbol : t.asset),
541
+ decimals: t.decimals,
542
+ amount: amount.toString(),
543
+ amountFormatted: formatUnits(amount, t.decimals)
544
+ };
545
+ }
546
+ async function getJson(url, init) {
547
+ const ctrl = new AbortController();
548
+ const timer = setTimeout(() => ctrl.abort(), 12e3);
549
+ try {
550
+ const res = await fetch(url, { ...init ?? {}, signal: ctrl.signal });
551
+ if (!res.ok) return null;
552
+ return await res.json();
553
+ } catch {
554
+ return null;
555
+ } finally {
556
+ clearTimeout(timer);
557
+ }
558
+ }
559
+ async function quoteEvmSwap(p) {
560
+ const chainPath = KYBER_CHAIN[p.chainId];
561
+ if (!chainPath) return null;
562
+ if (p.wantAmount <= 0n) return null;
563
+ const tokenIn = addrFor(p.from);
564
+ const tokenOut = addrFor(p.to);
565
+ if (tokenIn.toLowerCase() === tokenOut.toLowerCase()) return null;
566
+ const route = async (amountIn) => getJson(
567
+ `${KYBER_API}/${chainPath}/api/v1/routes?tokenIn=${tokenIn}&tokenOut=${tokenOut}&amountIn=${amountIn.toString()}`
568
+ );
569
+ const probeIn = 10n ** BigInt(Math.max(p.from.decimals - 2, 1));
570
+ const probe = await route(probeIn);
571
+ const probeOut = probe?.data?.routeSummary?.amountOut;
572
+ if (!probeOut) return null;
573
+ let needIn;
574
+ try {
575
+ const out = BigInt(probeOut);
576
+ if (out <= 0n) return null;
577
+ needIn = (probeIn * p.wantAmount + out - 1n) / out;
578
+ needIn = applySlippage(needIn, p.slippageBps);
579
+ } catch {
580
+ return null;
581
+ }
582
+ const MAX_REFINEMENTS = 3;
583
+ let real = await route(needIn);
584
+ let summary = real?.data?.routeSummary;
585
+ for (let attempt = 0; attempt < MAX_REFINEMENTS; attempt++) {
586
+ if (!summary?.amountOut) break;
587
+ let out;
588
+ try {
589
+ out = BigInt(summary.amountOut);
590
+ } catch {
591
+ return null;
592
+ }
593
+ if (out >= p.wantAmount) break;
594
+ if (out <= 0n) return null;
595
+ const next = applySlippage((needIn * p.wantAmount + out - 1n) / out, p.slippageBps);
596
+ if (next <= needIn) break;
597
+ needIn = next;
598
+ real = await route(needIn);
599
+ summary = real?.data?.routeSummary;
600
+ }
601
+ if (!summary?.amountOut || !real?.data?.routerAddress) return null;
602
+ try {
603
+ if (BigInt(summary.amountOut) < p.wantAmount) return null;
604
+ } catch {
605
+ return null;
606
+ }
607
+ const needsApproval = p.from.asset !== "native";
608
+ const spend = needIn;
609
+ return {
610
+ source: sourceFor(needsApproval, summary.extraFee),
611
+ network: p.network,
612
+ from: side(p.from, spend, p.nativeSymbol),
613
+ // What we will actually receive: at least the invoice amount, usually a little more.
614
+ to: side(p.to, BigInt(summary.amountOut), p.nativeSymbol),
615
+ // Exact-in means the input IS the cap: it cannot spend more than this.
616
+ maxSpend: spend.toString(),
617
+ maxSpendFormatted: formatUnits(spend, p.from.decimals),
618
+ slippageBps: p.slippageBps,
619
+ route: {
620
+ chainPath,
621
+ routeSummary: summary,
622
+ routerAddress: real.data.routerAddress,
623
+ needsApproval,
624
+ tokenIn
625
+ }
626
+ };
627
+ }
628
+ async function swapEvm(p) {
629
+ const { publicClient, walletClient, account, chain, quote } = p;
630
+ const route = quote.route;
631
+ if (!route?.routerAddress || !route.routeSummary) {
632
+ throw new Error("EVM: swap quote is missing its routing data \u2014 re-quote before swapping.");
633
+ }
634
+ const router = route.routerAddress;
635
+ const spend = BigInt(quote.maxSpend);
636
+ if (route.needsApproval) {
637
+ const token = route.tokenIn;
638
+ let allowance = 0n;
639
+ try {
640
+ allowance = await publicClient.readContract({
641
+ address: token,
642
+ abi: ERC20_ALLOWANCE_ABI,
643
+ functionName: "allowance",
644
+ args: [account.address, router]
645
+ });
646
+ } catch {
647
+ allowance = 0n;
648
+ }
649
+ if (allowance < spend) {
650
+ if (allowance > 0n) {
651
+ const resetHash = await walletClient.writeContract({
652
+ address: token,
653
+ abi: ERC20_ALLOWANCE_ABI,
654
+ functionName: "approve",
655
+ args: [router, 0n],
656
+ account,
657
+ chain
658
+ });
659
+ await publicClient.waitForTransactionReceipt({ hash: resetHash });
660
+ }
661
+ const approveHash = await walletClient.writeContract({
662
+ address: token,
663
+ abi: ERC20_ALLOWANCE_ABI,
664
+ functionName: "approve",
665
+ args: [router, spend],
666
+ account,
667
+ chain
668
+ });
669
+ await publicClient.waitForTransactionReceipt({ hash: approveHash });
670
+ }
671
+ }
672
+ const built = await getJson(
673
+ `${KYBER_API}/${route.chainPath}/api/v1/route/build`,
674
+ {
675
+ method: "POST",
676
+ headers: { "content-type": "application/json" },
677
+ body: JSON.stringify({
678
+ routeSummary: route.routeSummary,
679
+ sender: account.address,
680
+ recipient: account.address,
681
+ // ← ourselves: the funds never leave the wallet
682
+ slippageTolerance: quote.slippageBps
683
+ })
684
+ }
685
+ );
686
+ if (!built?.data?.data) {
687
+ throw new InsufficientFundsError(
688
+ "KyberSwap could not build this swap (the route went stale, or the market moved). Nothing was spent \u2014 re-quote and retry."
689
+ );
690
+ }
691
+ const execRouter = built.data.routerAddress ?? router;
692
+ if (route.needsApproval && execRouter.toLowerCase() !== router.toLowerCase()) {
693
+ const token = route.tokenIn;
694
+ const approveHash = await walletClient.writeContract({
695
+ address: token,
696
+ abi: ERC20_ALLOWANCE_ABI,
697
+ functionName: "approve",
698
+ args: [execRouter, spend],
699
+ account,
700
+ chain
701
+ });
702
+ await publicClient.waitForTransactionReceipt({ hash: approveHash });
703
+ }
704
+ try {
705
+ const hash = await walletClient.sendTransaction({
706
+ account,
707
+ chain,
708
+ to: execRouter,
709
+ data: built.data.data,
710
+ value: route.tokenIn === NATIVE_SENTINEL ? spend : 0n
711
+ });
712
+ const receipt = await publicClient.waitForTransactionReceipt({ hash });
713
+ if (receipt.status === "reverted") {
714
+ throw new InsufficientFundsError(
715
+ `EVM swap reverted on-chain (tx ${hash}). Nothing was swapped and only gas was spent. The usual cause is the route going stale between the quote and inclusion, so the router's minimum-output check fails. Re-quote and retry; on a fast-block chain a wider \`slippageBps\` helps.`
716
+ );
717
+ }
718
+ return {
719
+ transaction: hash,
720
+ network: quote.network,
721
+ source: quote.source,
722
+ from: quote.from,
723
+ to: quote.to
724
+ };
725
+ } catch (err) {
726
+ const msg = String(err?.message ?? err);
727
+ if (/TRANSFER_FROM_FAILED/i.test(msg)) {
728
+ throw new InsufficientFundsError(
729
+ "EVM swap reverted moving the token: the router allowance was rejected or the balance is short. Nothing was swapped. Some tokens refuse a non-zero to non-zero approve; re-quote and retry, which resets the allowance first.",
730
+ { cause: err }
731
+ );
732
+ }
733
+ if (/insufficient funds|exceeds balance/i.test(msg)) {
734
+ throw new InsufficientFundsError(
735
+ `EVM swap failed: the wallet can't cover it (token balance or native gas). (${msg.slice(0, 160)})`,
736
+ { cause: err }
737
+ );
738
+ }
739
+ throw err;
740
+ }
741
+ }
742
+
449
743
  // src/drivers/evm/verify.ts
450
744
  import {
451
745
  decodeEventLog,
@@ -1947,6 +2241,11 @@ function makeEvmNetwork(resolved) {
1947
2241
  });
1948
2242
  }
1949
2243
  },
2244
+ /** The bound wallet's own address — where THIS wallet gets paid. Derived from the key
2245
+ * material only: no RPC, nothing moved. See {@link ResolvedNetwork.addressOf}. */
2246
+ async addressOf(wallet) {
2247
+ return wallet._native.account.address;
2248
+ },
1950
2249
  async balanceOf(wallet, asset) {
1951
2250
  const owner = wallet._native.account.address;
1952
2251
  const native = await publicClient.getBalance({ address: owner }).catch(() => null);
@@ -1985,6 +2284,31 @@ function makeEvmNetwork(resolved) {
1985
2284
  signReceipt(wallet, input) {
1986
2285
  return signReceiptEvm(wallet, input);
1987
2286
  },
2287
+ /* ---- swap (OPTIONAL, opt-in): via KyberSwap, keyless + no integrator fee. See ./swap.ts ---- */
2288
+ async quoteSwap({ from, to, wantAmount, slippageBps, wallet }) {
2289
+ const a = wallet._native;
2290
+ return quoteEvmSwap({
2291
+ publicClient,
2292
+ chainId: resolved.chainId,
2293
+ network,
2294
+ nativeSymbol: resolved.chain.nativeCurrency?.symbol ?? "ETH",
2295
+ owner: a.account.address,
2296
+ from,
2297
+ to,
2298
+ wantAmount,
2299
+ slippageBps
2300
+ });
2301
+ },
2302
+ async swap(wallet, quote) {
2303
+ const a = wallet._native;
2304
+ return swapEvm({
2305
+ publicClient,
2306
+ walletClient: a.walletClient,
2307
+ account: a.account,
2308
+ chain: resolved.chain,
2309
+ quote
2310
+ });
2311
+ },
1988
2312
  async verify(ref, accept) {
1989
2313
  return verifyEvm({
1990
2314
  publicClient,
@@ -2138,7 +2462,7 @@ var loaders = {
2138
2462
  solana: async () => {
2139
2463
  let mod;
2140
2464
  try {
2141
- mod = await import("./solana-HTKDRTD3.js");
2465
+ mod = await import("./solana-WRXL54MR.js");
2142
2466
  } catch (cause) {
2143
2467
  throw new MissingDriverError(
2144
2468
  `Solana selected, but its packages aren't installed. Run: npm install @solana/web3.js @solana/spl-token bs58`,
@@ -2150,7 +2474,7 @@ var loaders = {
2150
2474
  ton: async () => {
2151
2475
  let mod;
2152
2476
  try {
2153
- mod = await import("./ton-WH2JVQOO.js");
2477
+ mod = await import("./ton-OZKFNRLT.js");
2154
2478
  } catch (cause) {
2155
2479
  throw new MissingDriverError(
2156
2480
  `TON selected, but its packages aren't installed. Run: npm install @ton/ton @ton/core @ton/crypto`,
@@ -2162,7 +2486,7 @@ var loaders = {
2162
2486
  stellar: async () => {
2163
2487
  let mod;
2164
2488
  try {
2165
- mod = await import("./stellar-EUFZLX6J.js");
2489
+ mod = await import("./stellar-YQLOIFDD.js");
2166
2490
  } catch (cause) {
2167
2491
  throw new MissingDriverError(
2168
2492
  `Stellar selected, but its package isn't installed. Run: npm install @stellar/stellar-sdk`,
@@ -2174,7 +2498,7 @@ var loaders = {
2174
2498
  xrpl: async () => {
2175
2499
  let mod;
2176
2500
  try {
2177
- mod = await import("./xrpl-RUOB37QH.js");
2501
+ mod = await import("./xrpl-5K444RGX.js");
2178
2502
  } catch (cause) {
2179
2503
  throw new MissingDriverError(
2180
2504
  `XRPL selected, but its package isn't installed. Run: npm install xrpl`,
@@ -2186,7 +2510,7 @@ var loaders = {
2186
2510
  tron: async () => {
2187
2511
  let mod;
2188
2512
  try {
2189
- mod = await import("./tron-FXBXDNEY.js");
2513
+ mod = await import("./tron-HG3IOOCG.js");
2190
2514
  } catch (cause) {
2191
2515
  throw new MissingDriverError(
2192
2516
  `Tron selected, but its package isn't installed. Run: npm install tronweb`,
@@ -2198,7 +2522,7 @@ var loaders = {
2198
2522
  sui: async () => {
2199
2523
  let mod;
2200
2524
  try {
2201
- mod = await import("./sui-KCIITCYH.js");
2525
+ mod = await import("./sui-JFQSDNSZ.js");
2202
2526
  } catch (cause) {
2203
2527
  throw new MissingDriverError(
2204
2528
  `Sui selected, but its package isn't installed. Run: npm install @mysten/sui`,
@@ -2210,7 +2534,7 @@ var loaders = {
2210
2534
  near: async () => {
2211
2535
  let mod;
2212
2536
  try {
2213
- mod = await import("./near-OLKCMTBI.js");
2537
+ mod = await import("./near-5X2CQGML.js");
2214
2538
  } catch (cause) {
2215
2539
  throw new MissingDriverError(
2216
2540
  `NEAR selected, but its package isn't installed. Run: npm install near-api-js`,
@@ -2222,7 +2546,7 @@ var loaders = {
2222
2546
  aptos: async () => {
2223
2547
  let mod;
2224
2548
  try {
2225
- mod = await import("./aptos-SBV6SGDP.js");
2549
+ mod = await import("./aptos-H5VLH2QL.js");
2226
2550
  } catch (cause) {
2227
2551
  throw new MissingDriverError(
2228
2552
  `Aptos selected, but its package isn't installed. Run: npm install @aptos-labs/ts-sdk`,
@@ -2234,7 +2558,7 @@ var loaders = {
2234
2558
  algorand: async () => {
2235
2559
  let mod;
2236
2560
  try {
2237
- mod = await import("./algorand-25FMBCT3.js");
2561
+ mod = await import("./algorand-KJ5XHSMT.js");
2238
2562
  } catch (cause) {
2239
2563
  throw new MissingDriverError(
2240
2564
  `Algorand selected, but its package isn't installed. Run: npm install algosdk`,
@@ -2273,6 +2597,7 @@ var BUILTIN_DENOMS = {
2273
2597
  USDT: "USD",
2274
2598
  USD1: "USD",
2275
2599
  FDUSD: "USD",
2600
+ USDG: "USD",
2276
2601
  U: "USD",
2277
2602
  // United Stables — BNB EIP-3009 stablecoin (1:1 USDC/USDT/USD1-backed)
2278
2603
  RLUSD: "USD",
@@ -2543,8 +2868,39 @@ var SpendLedger = class {
2543
2868
  }
2544
2869
  }
2545
2870
  /** Running total (base units) already spent on this (network, asset). */
2871
+ /*
2872
+ * ── IN-FLIGHT RESERVATIONS ─────────────────────────────────────────────────────────
2873
+ *
2874
+ * A cap is read when a quote is priced and written when the payment settles, and a whole
2875
+ * network round trip sits between the two. Without a reservation, N concurrent payments all
2876
+ * price against the same "spent so far", all pass, and all settle: an agent with a 2.50 cap
2877
+ * spends 4.00 and every individual check was correct. It is the same read-await-write shape
2878
+ * that let one proof be redeemed N times, on the other side of the wire.
2879
+ *
2880
+ * A reservation is taken SYNCHRONOUSLY by the client before it pays, counts toward every
2881
+ * total below while it is outstanding, and is released the moment the payment either settles
2882
+ * (the real record replaces it) or fails (so a refused payment never consumes the leash).
2883
+ */
2884
+ pending = /* @__PURE__ */ new Map();
2885
+ pendingSeq = 0;
2886
+ /** Reserve budget for a payment about to be attempted. Returns the token to settle it with. */
2887
+ reserve(network, asset, amountBase, decimals, denom) {
2888
+ const token = `r${++this.pendingSeq}`;
2889
+ const scaled = denom ? scaleToDenom(amountBase, decimals) ?? 0n : 0n;
2890
+ this.pending.set(token, { network, asset, amountBase, denom: denom?.toUpperCase(), scaled, at: Date.now() });
2891
+ return token;
2892
+ }
2893
+ /** Drop a reservation: the payment settled (its real record now counts) or it failed. */
2894
+ release(token) {
2895
+ if (token) this.pending.delete(token);
2896
+ }
2897
+ pendingFor(network, asset) {
2898
+ let sum = 0n;
2899
+ for (const p of this.pending.values()) if (p.network === network && p.asset === asset) sum += p.amountBase;
2900
+ return sum;
2901
+ }
2546
2902
  totalFor(network, asset) {
2547
- return this.buckets.get(keyFor(network, asset))?.total ?? 0n;
2903
+ return (this.buckets.get(keyFor(network, asset))?.total ?? 0n) + this.pendingFor(network, asset);
2548
2904
  }
2549
2905
  /**
2550
2906
  * Running grand total for a DENOMINATION, scaled to {@link DENOM_PRECISION} (so
@@ -2553,11 +2909,14 @@ var SpendLedger = class {
2553
2909
  * `0n` for a denomination never spent on. Case-insensitive.
2554
2910
  */
2555
2911
  totalForDenom(denom) {
2556
- return this.denomTotals.get(denom.toUpperCase()) ?? 0n;
2912
+ const key = denom.toUpperCase();
2913
+ let pending = 0n;
2914
+ for (const p of this.pending.values()) if (p.denom === key) pending += p.scaled;
2915
+ return (this.denomTotals.get(key) ?? 0n) + pending;
2557
2916
  }
2558
2917
  /** Total number of settled payments (across every chain + token). Powers `maxPayments`. */
2559
2918
  count() {
2560
- return this.records.length;
2919
+ return this.records.length + this.pending.size;
2561
2920
  }
2562
2921
  /** Mark a `warnAtFraction` threshold key as fired; returns `true` the FIRST time (so the
2563
2922
  * caller emits the `budget-threshold` event once) and `false` thereafter. Shared across
@@ -2575,6 +2934,7 @@ var SpendLedger = class {
2575
2934
  */
2576
2935
  countSince(sinceMs) {
2577
2936
  let n = 0;
2937
+ for (const p of this.pending.values()) if (p.at >= sinceMs) n += 1;
2578
2938
  for (const r of this.records) {
2579
2939
  const t = Date.parse(r.at);
2580
2940
  if (Number.isNaN(t) || t >= sinceMs) n += 1;
@@ -2590,6 +2950,9 @@ var SpendLedger = class {
2590
2950
  */
2591
2951
  totalSince(network, asset, sinceMs) {
2592
2952
  let sum = 0n;
2953
+ for (const p of this.pending.values()) {
2954
+ if (p.network === network && p.asset === asset && p.at >= sinceMs) sum += p.amountBase;
2955
+ }
2593
2956
  for (const r of this.records) {
2594
2957
  if (r.network !== network || r.asset !== asset) continue;
2595
2958
  const t = Date.parse(r.at);
@@ -2645,6 +3008,299 @@ var SpendLedger = class {
2645
3008
  }
2646
3009
  };
2647
3010
 
3011
+ // src/swapProviders.ts
3012
+ var SWAP_PROVIDERS = [
3013
+ {
3014
+ id: "stellar-sdex",
3015
+ name: "Stellar SDEX",
3016
+ kind: "protocol",
3017
+ url: "https://developers.stellar.org/docs/build/guides/transactions/path-payments",
3018
+ networks: ["stellar:pubnet"],
3019
+ keyless: true,
3020
+ fee: "No trading fee on the order books (spread only); the protocol liquidity pools charge a fixed 0.30%. Nobody can add an integrator fee, because there is no field for one.",
3021
+ mechanism: "A PathPaymentStrictReceive addressed to your own account: one atomic operation, routed across the order books and liquidity pools.",
3022
+ proofs: [
3023
+ {
3024
+ network: "stellar:pubnet",
3025
+ tx: "f7b784d4758ea83a022921c7202051b4643fefb4fa61530a49010a4a9348a0aa",
3026
+ date: "2026-09-08",
3027
+ summary: "0.2652 XLM \u2192 0.05 USDC",
3028
+ covers: "native in, and the ledger records source and destination as the same account"
3029
+ },
3030
+ {
3031
+ network: "stellar:pubnet",
3032
+ tx: "3aa2ca9e35ff7d7c3d0aaff3b24d53b4d2ea009067debf058c30ed8cad445f03",
3033
+ date: "2026-09-08",
3034
+ summary: "USDC \u2192 0.2 XLM",
3035
+ covers: "the reverse direction, token in"
3036
+ },
3037
+ {
3038
+ network: "stellar:pubnet",
3039
+ tx: "27f50b2c2c57a64a8b50c1b14f310ec99b4ccbbb4bc72c479a5f6f9f1180e385",
3040
+ date: "2026-09-08",
3041
+ summary: "0.1064 XLM \u2192 0.02 USDC",
3042
+ covers: "run straight from the committed example, so the documented flow is proven too"
3043
+ }
3044
+ ]
3045
+ },
3046
+ {
3047
+ id: "xrpl-dex",
3048
+ name: "XRPL DEX + AMM",
3049
+ kind: "protocol",
3050
+ url: "https://xrpl.org/docs/concepts/tokens/decentralized-exchange/automated-market-makers",
3051
+ networks: ["xrpl:0"],
3052
+ keyless: true,
3053
+ fee: "No protocol fee on the order books (spread only); AMM pools charge a per-pool fee set by liquidity providers, 0% to 1%, paid to the pool. No integrator fee is expressible.",
3054
+ mechanism: "A cross-currency Payment addressed to your own account, which the ledger permits precisely for currency conversion. Auto-bridged through XRP where that is cheaper.",
3055
+ proofs: [
3056
+ {
3057
+ network: "xrpl:0",
3058
+ tx: "F703B271E35BD3C2408DB64F693461513DD5166C15EC61DBAB0B2A9F726ED97E",
3059
+ date: "2026-09-08",
3060
+ summary: "0.072 XRP \u2192 0.1 RLUSD",
3061
+ covers: "native in; delivered_amount matched the requested amount exactly"
3062
+ },
3063
+ {
3064
+ network: "xrpl:0",
3065
+ tx: "24EC9DDD2B5D00FD7CD56882999FCF59ABC274AA32E35119EADE397B335EEB63",
3066
+ date: "2026-09-08",
3067
+ summary: "RLUSD \u2192 0.02 XRP",
3068
+ covers: "the reverse direction, token in"
3069
+ }
3070
+ ]
3071
+ },
3072
+ {
3073
+ id: "jupiter",
3074
+ name: "Jupiter",
3075
+ kind: "provider",
3076
+ url: "https://jup.ag",
3077
+ networks: ["solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp"],
3078
+ keyless: true,
3079
+ fee: "No platform fee. Verified per quote rather than assumed: PipRail never sets platformFeeBps, and every quote is inspected so the note says so if one ever appears.",
3080
+ mechanism: "Jupiter routes and builds a serialized transaction; your own keypair signs it locally and this process broadcasts it. Jupiter never signs and never holds the funds.",
3081
+ proofs: [
3082
+ {
3083
+ network: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
3084
+ tx: "2rXpQYVerRzrwUfmXMYcm6ijkm8x7LHU91onSZjrng34kr1nfcxZj1A8Ek7inSw7ZBzk3phAaYnvHwdSJTKxJ4rq",
3085
+ date: "2026-09-08",
3086
+ summary: "0.1031 USDC \u2192 0.001 SOL",
3087
+ covers: "token in, exact output"
3088
+ },
3089
+ {
3090
+ network: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
3091
+ tx: "UEeXBp3sPZgEHmahMasAbFhbTootXGCcpoNh7bN1sWsjoPDc1QE7uGaHjivT3iTroxLVJeWGVp2jZT63d2BVTk5",
3092
+ date: "2026-09-08",
3093
+ summary: "0.000485 SOL \u2192 0.05 USDC",
3094
+ covers: "the reverse direction, native in"
3095
+ }
3096
+ ]
3097
+ },
3098
+ {
3099
+ id: "kyberswap",
3100
+ name: "KyberSwap",
3101
+ kind: "provider",
3102
+ url: "https://kyberswap.com",
3103
+ // Every one live-probed with a real stablecoin route before being listed. Celo and
3104
+ // Scroll are deliberately ABSENT: the API answers but returns no route even for the
3105
+ // most liquid pair, so listing them would advertise a swap that cannot execute.
3106
+ networks: [
3107
+ "eip155:1",
3108
+ "eip155:10",
3109
+ "eip155:56",
3110
+ "eip155:137",
3111
+ "eip155:8453",
3112
+ "eip155:42161",
3113
+ "eip155:43114",
3114
+ "eip155:59144",
3115
+ "eip155:4663"
3116
+ ],
3117
+ keyless: true,
3118
+ fee: "No integrator fee. Verified per quote rather than assumed: PipRail sends no fee parameters, and the returned extraFee block is inspected so the note says so if one ever appears.",
3119
+ mechanism: "KyberSwap routes; your own wallet calls the router contract directly. An ERC-20 swap costs two transactions (an approve, then the swap); a native-in swap costs one.",
3120
+ proofs: [
3121
+ {
3122
+ network: "eip155:56",
3123
+ tx: "0x1748b6b27b1920aafd1d2730d250378b08cd4c0851a29e5e785dea960e0c8d4d",
3124
+ date: "2026-09-08",
3125
+ summary: "0.0101 USDT \u2192 USDC on BNB Chain",
3126
+ covers: "the ERC-20 path, which needs an approval first"
3127
+ },
3128
+ {
3129
+ network: "eip155:4663",
3130
+ tx: "0x424299e67be623313c679e659fbfe9fb8707800fe145cddf7fac5a9bdbb78f10",
3131
+ date: "2026-09-08",
3132
+ summary: "native ETH \u2192 0.360518 USDG on Robinhood Chain",
3133
+ covers: "a 100ms-block chain, where a route goes stale fast: the default 50 bps slippage reverted first, 300 bps cleared"
3134
+ },
3135
+ {
3136
+ network: "eip155:56",
3137
+ tx: "0xe27f957b40285acf0b846aea23808d49cc6a44fccd755aa88656abf31204fb00",
3138
+ date: "2026-09-08",
3139
+ summary: "native BNB \u2192 0.02 USDC",
3140
+ covers: "the native-in path, which needs no approval"
3141
+ },
3142
+ {
3143
+ network: "eip155:8453",
3144
+ tx: "0xb28b802f530945eba1ea1662890792bfa1dfba64f5094caa58297f9693604032",
3145
+ date: "2026-09-08",
3146
+ summary: "0.00117 USDC \u2192 EURC on Base",
3147
+ covers: "a second EVM chain on the same implementation"
3148
+ },
3149
+ {
3150
+ network: "eip155:8453",
3151
+ tx: "0x99a0842cd6c3fb33ddc03ddf776c2981c629debcd8ac47678a2e5e29e4496242",
3152
+ date: "2026-09-08",
3153
+ summary: "EURC \u2192 USDC on Base",
3154
+ covers: "the reverse direction"
3155
+ }
3156
+ ]
3157
+ },
3158
+ {
3159
+ id: "aftermath",
3160
+ name: "Aftermath",
3161
+ kind: "provider",
3162
+ url: "https://aftermath.finance",
3163
+ networks: ["sui:mainnet"],
3164
+ keyless: true,
3165
+ fee: "No fee added by the router: the route reports its own coinIn/coinOut tradeFee as zero, and the only cost is the underlying pool fee (about 0.014% on the probed route). PipRail passes no fee parameter.",
3166
+ mechanism: "Aftermath routes and returns a COMPLETE serialized programmable transaction block; your own keypair signs it locally and this process broadcasts it. It never signs and never holds the funds.",
3167
+ proofs: [
3168
+ {
3169
+ network: "sui:mainnet",
3170
+ tx: "GtykrnLxejyrjmTL8oKVoEtTAiP9eY69CGne7Djw3Cha",
3171
+ date: "2026-09-08",
3172
+ summary: "0.0657 SUI to 0.0505 USDC",
3173
+ covers: "native in; the balance changes on chain match the quote exactly"
3174
+ }
3175
+ ]
3176
+ },
3177
+ {
3178
+ id: "ref-finance",
3179
+ name: "Ref Finance",
3180
+ kind: "provider",
3181
+ url: "https://app.ref.finance",
3182
+ networks: ["near:mainnet"],
3183
+ keyless: true,
3184
+ fee: "The pool's own total_fee, read live per pool (0.01% on the route used). There is no integrator fee for PipRail to set or waive.",
3185
+ mechanism: "One ft_transfer_call receipt chain: token contract to AMM and back to you, inside a single transaction you sign. No solver takes possession, which is exactly why this is used instead of an intent network.",
3186
+ proofs: [
3187
+ {
3188
+ network: "near:mainnet",
3189
+ tx: "FAb28z1tCcecd2teFTxJGkgaLMr1mpctuiFhUeCKjszT",
3190
+ date: "2026-09-08",
3191
+ summary: "0.0505 USDT to 0.05 USDC",
3192
+ covers: "token to token across a stable pool, 14 receipts, all successful"
3193
+ }
3194
+ ]
3195
+ },
3196
+ {
3197
+ id: "vestige",
3198
+ name: "Vestige",
3199
+ kind: "provider",
3200
+ url: "https://vestige.fi",
3201
+ networks: ["algorand:wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qzkkit8="],
3202
+ keyless: true,
3203
+ fee: "The pool's own fee (0.30% on the probed route) plus the group's network fee. PipRail sends no fee parameter.",
3204
+ mechanism: "Vestige returns an UNSIGNED atomic transaction group in which every signer is you. The SDK asserts that at runtime before signing anything, then signs locally and submits.",
3205
+ proofs: [
3206
+ {
3207
+ network: "algorand:wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qzkkit8=",
3208
+ tx: "5UV7SWSJODHJDGMXODTOMBNXMK4YGZIZKR4VOM6XHML7JS7A32UA",
3209
+ date: "2026-09-08",
3210
+ summary: "0.5094 ALGO to 0.05 USDC",
3211
+ covers: "a four-transaction atomic group, every signer the sender"
3212
+ }
3213
+ ]
3214
+ },
3215
+ {
3216
+ id: "hyperion",
3217
+ name: "Hyperion",
3218
+ kind: "provider",
3219
+ url: "https://hyperion.xyz",
3220
+ networks: ["aptos:1"],
3221
+ keyless: true,
3222
+ fee: "The pool's own fee, read off-chain-state per route (0.01% on the stable pool used here). No API key, no integrator fee: the entry function has no fee parameter to set.",
3223
+ mechanism: "Called contract-to-contract with NO API in the middle: the quote is an on-chain `view` and the swap is a Move entry function your own account signs. Uniquely, `exact_output_swap_entry` takes the amount you want OUT plus an on-chain input cap, so an x402 invoice is priced exactly rather than approximated.",
3224
+ proofs: [
3225
+ {
3226
+ network: "aptos:1",
3227
+ tx: "0x927293cf8584320f1e265c564ab3096262b675a61996e0da955a2b3c27af1adc",
3228
+ date: "2026-09-08",
3229
+ summary: "0.049997 USDC to exactly 0.05 USDT",
3230
+ covers: "the stable pair, and the first swap in this SDK to deliver an EXACT output rather than at-least"
3231
+ },
3232
+ {
3233
+ network: "aptos:1",
3234
+ tx: "0xae849e031cb50a660f8c9f286c1f3a9caeff08b427efd9428154d884f1e3059e",
3235
+ date: "2026-09-08",
3236
+ summary: "0.040012 USDT to exactly 0.04 USDC",
3237
+ covers: "the reverse direction, which takes the opposite on-chain price bound"
3238
+ },
3239
+ {
3240
+ network: "aptos:1",
3241
+ tx: "0x7921118d64ce1e4ebe7911ed22fef2222597e1b9e20caf0fd4f89bce07c07981",
3242
+ date: "2026-09-08",
3243
+ summary: "0.0459 APT to exactly 0.03 USDC",
3244
+ covers: "native APT in, on a different fee tier"
3245
+ },
3246
+ {
3247
+ network: "aptos:1",
3248
+ tx: "0x640bce4b1e5f688d4494d0c343247b2951d22bc26d371b81f7d2c04655a7a707",
3249
+ date: "2026-09-08",
3250
+ summary: "0.012842 USDC to exactly 0.02 APT",
3251
+ covers: "native APT out, where the balance delta nets off gas and only the deposit event proves the exact amount"
3252
+ }
3253
+ ]
3254
+ },
3255
+ {
3256
+ id: "stonfi",
3257
+ name: "STON.fi",
3258
+ kind: "provider",
3259
+ url: "https://ston.fi",
3260
+ networks: ["tvm:-239"],
3261
+ keyless: true,
3262
+ fee: "The pool's own fee, reported per route by the simulation (about 0.10% on the routes used here). No API key and no referral field.",
3263
+ mechanism: "Exact-output route, priced by STON.fi's keyless `reverse_swap` simulation, which fixes the ASK side and returns the offer required. PipRail pads the request so the router's on-chain floor (`min_ask_units`) is at or above the invoice: you may receive slightly more, never less. The swap is one message your own wallet signs to the router the simulation named. TON settles asynchronously, so arrival is a balance check rather than one atomic receipt.",
3264
+ proofs: [
3265
+ {
3266
+ network: "tvm:-239",
3267
+ tx: "1f6d58d073bd988b47272658297ebabc4748725ca919a4a9c863f28cef8eeb3f",
3268
+ date: "2026-09-08",
3269
+ summary: "0.035528 TON for 0.05 USDT",
3270
+ covers: "native TON in, through the pTON proxy jetton, on the v2.2 router. The wallet transaction that STARTED the swap (opcode 0x01f3835d), not the gas refund that lands moments later"
3271
+ },
3272
+ {
3273
+ network: "tvm:-239",
3274
+ tx: "f76ae97e107b7852ea105d3b3b0caf095ec77bd2541f40417303c08566a87947",
3275
+ date: "2026-09-08",
3276
+ summary: "0.028171 USDT for 0.02 TON",
3277
+ covers: "the jetton-to-TON direction, which the simulation routed through a DIFFERENT router version (v2.1). A TEP-74 jetton transfer (opcode 0x0f8a7ea5) carrying the swap as its forward payload"
3278
+ }
3279
+ ]
3280
+ },
3281
+ {
3282
+ id: "sunswap",
3283
+ name: "SunSwap V2",
3284
+ kind: "provider",
3285
+ url: "https://sunswap.com",
3286
+ networks: ["tron:mainnet"],
3287
+ keyless: true,
3288
+ fee: "The pool's own 0.30% constant-product fee. No API key and no integrator fee: the router has no such parameter. Tron's ENERGY charge is the dominant cost, see below.",
3289
+ mechanism: "Called contract-to-contract with NO API: `getAmountsIn` prices an exact output and `swapETHForExactTokens` executes it with the input capped on-chain. Selling native TRX is one transaction; selling a TRC-20 is two, because the router moves the input with `transferFrom` and needs an approval first. SunSwap's own front end uses an undocumented, obfuscated hostname, which a payments SDK should not depend on, so this skips the service entirely.",
3290
+ unproven: "Shipped without a mainnet proof, deliberately and openly. The route is real, and every part of it that can be checked without spending has been: both directions quote live through the SDK, and the approve and the router call both execute cleanly in constant-call simulations against the real contracts. It has NOT been broadcast because Tron charges about 230,629 ENERGY per swap, which without staked energy is roughly 23 TRX (about $7.79) regardless of trade size, plus about 10 TRX more the first time a TRC-20 is approved. The project test wallets hold 8.1 TRX. A user with energy can swap today; we simply have not paid to prove it.",
3291
+ proofs: []
3292
+ }
3293
+ ];
3294
+ function swapProvidersFor(network) {
3295
+ return SWAP_PROVIDERS.filter((p) => p.networks.includes(network));
3296
+ }
3297
+ function canSwapOn(network) {
3298
+ return swapProvidersFor(network).length > 0;
3299
+ }
3300
+ function swappableNetworks() {
3301
+ return [...new Set(SWAP_PROVIDERS.flatMap((p) => p.networks))].sort();
3302
+ }
3303
+
2648
3304
  // src/util/exactRecovery.ts
2649
3305
  function exactSettleCheckHint(family, payerFrom, nonce) {
2650
3306
  switch (family) {
@@ -2706,6 +3362,33 @@ var PipRailClient = class {
2706
3362
  this.assertPolicyAmountCaps(opts.policy);
2707
3363
  this.assertPolicyTimeOptions(opts.policy);
2708
3364
  this.assertPolicySpendControls(opts.policy);
3365
+ this.assertModeIsHonest(opts);
3366
+ this.sealAuthority();
3367
+ }
3368
+ /**
3369
+ * Pin the three authority accessors to THIS instance, non-writable and non-configurable.
3370
+ *
3371
+ * `paymentTools()` decides which tools a model is handed by calling `canAgentSell()` /
3372
+ * `canAgentSwap()`, which read `mode()`. Those were plain prototype methods, so any code
3373
+ * holding the client could reassign one — `client.mode = () => 'sovereign'` turned a
3374
+ * budgeted client's eight tools into sovereign's fourteen.
3375
+ *
3376
+ * A MODEL could never do that (it sends JSON tool arguments; it does not hold the object),
3377
+ * so this is not a path a model can walk. It is defence in depth for the case where a
3378
+ * client passes through code that is not the operator's own: an agent framework, a plugin,
3379
+ * some middleware that wraps or proxies objects. Authority is set once, by whoever
3380
+ * provisioned the key, and nothing downstream gets to revise it.
3381
+ */
3382
+ sealAuthority() {
3383
+ const mode = this.opts.mode ?? DEFAULT_AGENT_MODE;
3384
+ const sovereign = mode === "sovereign";
3385
+ for (const [name, fn] of [
3386
+ ["mode", () => mode],
3387
+ ["canAgentSell", () => sovereign],
3388
+ ["canAgentSwap", () => sovereign]
3389
+ ]) {
3390
+ Object.defineProperty(this, name, { value: fn, writable: false, configurable: false, enumerable: false });
3391
+ }
2709
3392
  }
2710
3393
  /**
2711
3394
  * Fail LOUDLY at construction on a malformed amount cap — a security boundary
@@ -3055,6 +3738,32 @@ var PipRailClient = class {
3055
3738
  spent() {
3056
3739
  return this.ledger.summary();
3057
3740
  }
3741
+ /**
3742
+ * Each mode must be able to KEEP its promise, checked once at construction.
3743
+ *
3744
+ * `'supervised'` means a human approves each payment, and the only thing that can pause a
3745
+ * payment for a human is `onBeforePay`. Without it the mode was a label: the client happily
3746
+ * paid without asking anyone, while the name, the docs and the operator's mental model all
3747
+ * said otherwise. A safety control that silently does nothing is worse than an absent one,
3748
+ * because the operator has already stopped worrying about it.
3749
+ *
3750
+ * The mirror of the sovereign rule, which refuses to unlock swapping without a ceiling. Both
3751
+ * fail at CONSTRUCTION rather than at the first payment, so the mistake surfaces while
3752
+ * somebody is reading their own config instead of mid-spend.
3753
+ */
3754
+ assertModeIsHonest(opts) {
3755
+ const mode = opts.mode ?? DEFAULT_AGENT_MODE;
3756
+ if (mode === "supervised" && typeof opts.onBeforePay !== "function") {
3757
+ throw new TypeError(
3758
+ "mode: 'supervised' needs an `onBeforePay` hook \u2014 it is the only thing that can pause a payment for a human, so without it this client would pay without asking anyone, which is exactly what 'supervised' promises not to do. Add onBeforePay, or use 'budgeted' if the policy is meant to be the consent. (@piprail/mcp wires this for you from PIPRAIL_MODE.)"
3759
+ );
3760
+ }
3761
+ if (mode === "sovereign" && opts.swapPolicy?.maxPerSwap === void 0) {
3762
+ throw new TypeError(
3763
+ "mode: 'sovereign' needs `swapPolicy.maxPerSwap` \u2014 it hands a model the swap tools, and your payment caps do NOT bound a swap (they count payments; a swap is not one), so without a ceiling nothing limits what one swap may spend. Set a ceiling, e.g. swapPolicy: { maxPerSwap: '25.00' }. Selling needs no ceiling: it takes money rather than spending it."
3764
+ );
3765
+ }
3766
+ }
3058
3767
  /** The CONFIGURED spend policy, read back unchanged — so an agent can self-check
3059
3768
  * its WHOLE leash (caps, allowlists, time, denom + count limits) without hitting a
3060
3769
  * decline. `undefined` when no policy is set. Pure; never throws. */
@@ -3062,40 +3771,143 @@ var PipRailClient = class {
3062
3771
  return this.opts.policy;
3063
3772
  }
3064
3773
  /**
3065
- * Read-only budget + time leash for a Mode-A (headless) agent — the policy IS
3066
- * the consent, and this is how the agent SEES what's left of it before paying.
3067
- * Composes the ledger with the configured policy: the per-asset money leash
3068
- * (`byAsset`), the cross-token GRAND TOTAL per denomination (`byDenom`, present
3069
- * from the start), and the payment-COUNT leash (`counts`). Never throws, moves no
3070
- * funds. The money/count figures persist if a `spendStore` is set; the time
3071
- * envelope is process-scoped (see {@link SessionBudget}).
3774
+ * Who is answerable for this wallet: `'supervised'`, `'budgeted'` (default) or
3775
+ * `'sovereign'`. READ-ONLY on purpose. A model asks what authority it has; it can
3776
+ * never grant itself more, exactly as an agent cannot widen its own permissions in
3777
+ * its host. Set once at construction by whoever provisioned the key.
3072
3778
  */
3073
- budget() {
3074
- const view = this.sessionView();
3075
- const start = new Date(this.ledger.sessionStart).toISOString();
3076
- return {
3077
- session: {
3078
- start,
3079
- expiresAt: view?.expiresAt != null ? new Date(view.expiresAt).toISOString() : null,
3080
- secondsRemaining: view?.secondsRemaining ?? null
3081
- },
3082
- byAsset: this.remaining(),
3083
- byDenom: this.denomRemaining(),
3084
- counts: this.countStatus()
3085
- };
3779
+ mode() {
3780
+ return this.opts.mode ?? DEFAULT_AGENT_MODE;
3781
+ }
3782
+ /** The configured swap guardrails, read back unchanged. `undefined` when none is set. */
3783
+ swapPolicy() {
3784
+ return this.opts.swapPolicy;
3086
3785
  }
3087
3786
  /**
3088
- * The cross-token GRAND-TOTAL leashone row per denomination capped by
3089
- * `policy.maxTotalPerDenom`. Unlike `remaining()`, rows exist from the START
3090
- * (the cap is a single declared number, so headroom is previewable before any
3091
- * spend). `[]` when no `maxTotalPerDenom` is set. Pure; never throws; never a
3092
- * price-converted figure (tokens grouped as one unit, each 1:1).
3787
+ * This wallet's OWN addresswhere it gets paid.
3788
+ *
3789
+ * The receiving half of a wallet, and the one thing an agent handed a key cannot work
3790
+ * out for itself: the key is set by whoever provisioned it, so without this the agent
3791
+ * can spend but can never tell a buyer where to send anything. `sell` defaults its
3792
+ * `payTo` to exactly this.
3793
+ *
3794
+ * Derived from the key, not the network — no RPC read, nothing moved. Throws
3795
+ * {@link WalletRequiredError} on a read-only client (no wallet, no address).
3093
3796
  */
3094
- denomRemaining() {
3095
- const caps = this.opts.policy?.maxTotalPerDenom;
3096
- if (!caps) return [];
3097
- return Object.entries(caps).map(([rawDenom, capStr]) => {
3098
- const denom = rawDenom.toUpperCase();
3797
+ /** The chain this client is configured for, exactly as it was given. A sold offer
3798
+ * defaults to it, so an agent prices on the chain it actually holds funds on. */
3799
+ chain() {
3800
+ return this.opts.chain;
3801
+ }
3802
+ /**
3803
+ * What this wallet actually HOLDS, per asset — the balance sheet, not the leash.
3804
+ *
3805
+ * `budget()` answers "how much of my allowance is left", a different question and the only
3806
+ * one an agent could previously ask. An agent that OWNS a wallet has to answer "what do I
3807
+ * have?" before it can decide anything: whether to sell, whether to swap, whether it needs
3808
+ * topping up and in which denomination.
3809
+ *
3810
+ * RPC-read-only and never throws for a read problem: an asset whose read was unavailable
3811
+ * comes back `null` (unknown), never `0`, because a rate-limited read that reads as "broke"
3812
+ * would make an agent behave as though it had been drained. A symbol this chain does not ship
3813
+ * is reported as unknown rather than guessed at.
3814
+ */
3815
+ async balanceOf(assets = ["native"]) {
3816
+ const { net, wallet } = await this.ensure();
3817
+ if (!wallet) {
3818
+ throw new WalletRequiredError("balanceOf needs a wallet \u2014 a read-only client holds nothing.");
3819
+ }
3820
+ const out = [];
3821
+ for (const symbol of assets) {
3822
+ let asset;
3823
+ let decimals;
3824
+ let resolvedSymbol;
3825
+ try {
3826
+ const t = net.resolveToken(symbol);
3827
+ asset = t.asset;
3828
+ decimals = t.decimals;
3829
+ resolvedSymbol = t.symbol;
3830
+ } catch {
3831
+ out.push({ symbol, asset: null, decimals: null, known: false, amount: null, amountFormatted: null });
3832
+ continue;
3833
+ }
3834
+ const bal = await net.balanceOf(wallet, asset).catch(() => ({ token: null, native: null }));
3835
+ out.push({
3836
+ symbol: resolvedSymbol ?? symbol,
3837
+ asset,
3838
+ decimals,
3839
+ known: true,
3840
+ amount: bal.token === null ? null : bal.token.toString(),
3841
+ amountFormatted: bal.token === null ? null : formatUnits(bal.token, decimals)
3842
+ });
3843
+ }
3844
+ return out;
3845
+ }
3846
+ async address() {
3847
+ const { net, wallet } = await this.ensure();
3848
+ if (!wallet) {
3849
+ throw new WalletRequiredError(
3850
+ "address() needs a wallet \u2014 a read-only client has no address to be paid at."
3851
+ );
3852
+ }
3853
+ return net.addressOf(wallet);
3854
+ }
3855
+ /**
3856
+ * May a model SELL on this wallet — price its own offers and collect payment for them?
3857
+ * True only in `'sovereign'` mode, the same authority test as {@link canAgentSwap}:
3858
+ * earning is the other half of owning a wallet, and an agent that answers for its own
3859
+ * balance answers for how that balance is filled.
3860
+ *
3861
+ * Taking money is not the risk that gates this. Committing to DELIVER something is, and
3862
+ * so is publishing an address as an open invitation to pay it — neither is a supervised
3863
+ * agent's call to make alone.
3864
+ */
3865
+ canAgentSell() {
3866
+ return this.mode() === "sovereign";
3867
+ }
3868
+ /**
3869
+ * May a model move this wallet's own funds between denominations? True only in
3870
+ * `'sovereign'` mode. The payment tools consult this, so the answer lives in ONE place
3871
+ * rather than being re-derived by every surface that asks.
3872
+ */
3873
+ canAgentSwap() {
3874
+ return this.mode() === "sovereign";
3875
+ }
3876
+ /**
3877
+ * Read-only budget + time leash for a Mode-A (headless) agent — the policy IS
3878
+ * the consent, and this is how the agent SEES what's left of it before paying.
3879
+ * Composes the ledger with the configured policy: the per-asset money leash
3880
+ * (`byAsset`), the cross-token GRAND TOTAL per denomination (`byDenom`, present
3881
+ * from the start), and the payment-COUNT leash (`counts`). Never throws, moves no
3882
+ * funds. The money/count figures persist if a `spendStore` is set; the time
3883
+ * envelope is process-scoped (see {@link SessionBudget}).
3884
+ */
3885
+ budget() {
3886
+ const view = this.sessionView();
3887
+ const start = new Date(this.ledger.sessionStart).toISOString();
3888
+ return {
3889
+ session: {
3890
+ start,
3891
+ expiresAt: view?.expiresAt != null ? new Date(view.expiresAt).toISOString() : null,
3892
+ secondsRemaining: view?.secondsRemaining ?? null
3893
+ },
3894
+ byAsset: this.remaining(),
3895
+ byDenom: this.denomRemaining(),
3896
+ counts: this.countStatus()
3897
+ };
3898
+ }
3899
+ /**
3900
+ * The cross-token GRAND-TOTAL leash — one row per denomination capped by
3901
+ * `policy.maxTotalPerDenom`. Unlike `remaining()`, rows exist from the START
3902
+ * (the cap is a single declared number, so headroom is previewable before any
3903
+ * spend). `[]` when no `maxTotalPerDenom` is set. Pure; never throws; never a
3904
+ * price-converted figure (tokens grouped as one unit, each 1:1).
3905
+ */
3906
+ denomRemaining() {
3907
+ const caps = this.opts.policy?.maxTotalPerDenom;
3908
+ if (!caps) return [];
3909
+ return Object.entries(caps).map(([rawDenom, capStr]) => {
3910
+ const denom = rawDenom.toUpperCase();
3099
3911
  const spentScaled = this.ledger.totalForDenom(denom);
3100
3912
  const capScaled = floorUnits(capStr, DENOM_PRECISION);
3101
3913
  const remainingScaled = capScaled > spentScaled ? capScaled - spentScaled : 0n;
@@ -3210,6 +4022,126 @@ var PipRailClient = class {
3210
4022
  const plan = await this.planPayment(url, init);
3211
4023
  return plan == null ? true : plan.payable;
3212
4024
  }
4025
+ /* --------------------------- swap (OPTIONAL helper) --------------------------- */
4026
+ /**
4027
+ * Price a same-chain swap — "I hold the wrong token". READ-ONLY: no funds move,
4028
+ * nothing is signed, nothing is committed to.
4029
+ *
4030
+ * ⚠️ **This is a convenience, not part of paying.** Nothing calls it for you.
4031
+ * `fetch()` never swaps, `planPayment()` never swaps, and there is deliberately no
4032
+ * `autoSwap` option: converting one asset into another is a priced, irreversible
4033
+ * act, and a payment library should not do that on your behalf because it noticed
4034
+ * you were short. If you would rather bridge or swap somewhere else entirely, or
4035
+ * just top the wallet up by hand, that is a perfectly good answer and this method
4036
+ * costs you nothing by existing.
4037
+ *
4038
+ * 🔴 **The rate is not PipRail's.** Read `quote.source` — it names who produced the
4039
+ * number. Today both implementations are `kind: 'protocol'`, meaning the chain's
4040
+ * own order books priced it and no company is involved.
4041
+ *
4042
+ * Returns `null` — never throws — when this chain has no swap support, the pair
4043
+ * can't be routed, there's no liquidity, or a read failed. `null` means "no quote",
4044
+ * never "no funds".
4045
+ */
4046
+ async quoteSwap(req) {
4047
+ const slippageBps = resolveSlippageBps(req.slippageBps);
4048
+ const sp = this.opts.swapPolicy;
4049
+ if (sp?.maxSlippageBps !== void 0 && slippageBps > sp.maxSlippageBps) {
4050
+ throw new PaymentDeclinedError(
4051
+ `slippageBps ${slippageBps} exceeds this agent's swapPolicy.maxSlippageBps of ${sp.maxSlippageBps}.`,
4052
+ { reasonCode: "POLICY" }
4053
+ );
4054
+ }
4055
+ const { net, wallet } = await this.ensure();
4056
+ if (!net.quoteSwap || !wallet) return null;
4057
+ let from;
4058
+ let to;
4059
+ let wantAmount;
4060
+ try {
4061
+ from = net.resolveToken(req.from);
4062
+ to = net.resolveToken(req.to);
4063
+ wantAmount = parseUnits(req.wantAmount, to.decimals);
4064
+ } catch {
4065
+ return null;
4066
+ }
4067
+ try {
4068
+ return await net.quoteSwap({ from, to, wantAmount, slippageBps, wallet });
4069
+ } catch {
4070
+ return null;
4071
+ }
4072
+ }
4073
+ /**
4074
+ * Execute a swap you have already quoted and chosen to accept. Signs from your own
4075
+ * wallet (one transaction, or two where a token must be approved first); on every route
4076
+ * the funds never leave your account, they just change denomination.
4077
+ *
4078
+ * Pass the {@link SwapQuote} from {@link quoteSwap} unmodified — it carries the
4079
+ * route and the on-chain slippage cap. Re-quote rather than reusing an old one: a
4080
+ * stale route is how you get a worse price than you were shown.
4081
+ *
4082
+ * Throws {@link WalletRequiredError} without a wallet, {@link UnsupportedNetworkError}
4083
+ * when the chain has no swap support, and {@link InsufficientFundsError} when the
4084
+ * wallet can't cover it (which includes the market moving past your slippage cap —
4085
+ * nothing is swapped in that case, though chains that charge for a reverted
4086
+ * transaction, such as EVM, Aptos and Tron, still take the gas).
4087
+ */
4088
+ async swap(quote) {
4089
+ const { net, wallet } = await this.ensure();
4090
+ if (!wallet) {
4091
+ throw new WalletRequiredError(
4092
+ "swap needs a wallet \u2014 it signs a transaction. This client is read-only."
4093
+ );
4094
+ }
4095
+ if (!net.swap) {
4096
+ const venues = SWAP_PROVIDERS.map((p) => p.name).join(", ");
4097
+ throw new UnsupportedNetworkError(
4098
+ `Swapping isn't supported on ${net.network}. PipRail ships swaps only where an OPEN, KEYLESS route exists (${venues}). On other chains, move funds with a tool you choose.`
4099
+ );
4100
+ }
4101
+ if (quote.network !== net.network) {
4102
+ throw new UnsupportedNetworkError(
4103
+ `This swap quote is for ${quote.network} but the client is bound to ${net.network}.`
4104
+ );
4105
+ }
4106
+ const sp = this.opts.swapPolicy;
4107
+ if (sp?.maxPerSwap !== void 0) {
4108
+ const cap = parseUnits(sp.maxPerSwap, quote.from.decimals);
4109
+ if (BigInt(quote.maxSpend) > cap) {
4110
+ throw new PaymentDeclinedError(
4111
+ `This swap could spend up to ${quote.maxSpendFormatted} ${quote.from.symbol}, over this agent's swapPolicy.maxPerSwap of ${sp.maxPerSwap}. Nothing was signed.`,
4112
+ { reasonCode: "POLICY" }
4113
+ );
4114
+ }
4115
+ }
4116
+ if (sp?.allowTo?.length) {
4117
+ const want = quote.to.symbol;
4118
+ if (!sp.allowTo.some((t) => t.toUpperCase() === want.toUpperCase())) {
4119
+ throw new PaymentDeclinedError(
4120
+ `This agent may only swap into ${sp.allowTo.join(", ")}, not ${want}. Nothing was signed.`,
4121
+ { reasonCode: "POLICY" }
4122
+ );
4123
+ }
4124
+ }
4125
+ const approve = this.opts.onBeforeSwap;
4126
+ if (approve) {
4127
+ let okToSwap;
4128
+ try {
4129
+ okToSwap = await approve(quote);
4130
+ } catch (err) {
4131
+ throw new PaymentDeclinedError(
4132
+ "onBeforeSwap threw \u2014 refusing to swap. Nothing was signed.",
4133
+ { reasonCode: "APPROVAL", cause: err }
4134
+ );
4135
+ }
4136
+ if (okToSwap !== true) {
4137
+ throw new PaymentDeclinedError(
4138
+ `onBeforeSwap declined this swap of up to ${quote.maxSpendFormatted} ${quote.from.symbol} for ${quote.to.symbol}. Nothing was signed.`,
4139
+ { reasonCode: "APPROVAL" }
4140
+ );
4141
+ }
4142
+ }
4143
+ return net.swap(wallet, quote);
4144
+ }
3213
4145
  /* ------------------------- discovery (find + list) ------------------------- */
3214
4146
  /**
3215
4147
  * Find payable resources on the OPEN x402 indexes — WITHOUT paying. Reads the
@@ -3399,22 +4331,35 @@ var PipRailClient = class {
3399
4331
  quote = plan.best.quote;
3400
4332
  }
3401
4333
  this.safeEmit({ kind: "payment-required", challenge, accept });
3402
- await this.authorize(quote);
4334
+ const reservation = await this.authorize(quote);
3403
4335
  if (accept.scheme === "upto") {
3404
- return this.payUptoRail(net, wallet, accept, url, init, quote);
4336
+ try {
4337
+ return await this.payUptoRail(net, wallet, accept, url, init, quote, reservation);
4338
+ } finally {
4339
+ this.ledger.release(reservation);
4340
+ }
3405
4341
  }
3406
4342
  if (accept.scheme === "exact") {
3407
- return this.payExactRail(net, wallet, accept, url, init, quote, challenge.x402Version);
4343
+ try {
4344
+ return await this.payExactRail(net, wallet, accept, url, init, quote, challenge.x402Version, reservation);
4345
+ } finally {
4346
+ this.ledger.release(reservation);
4347
+ }
3408
4348
  }
3409
4349
  if (accept.scheme !== "onchain-proof") {
4350
+ this.ledger.release(reservation);
3410
4351
  throw new UnsupportedSchemeError(
3411
4352
  `internal: unrouted accept scheme '${accept.scheme}' reached the onchain-proof pay path.`
3412
4353
  );
3413
4354
  }
3414
- const { ref, confirmed } = await this.payAndConfirm(net, wallet, accept);
3415
- const response = await this.retryWithProof(url, init, accept, ref, confirmed);
3416
- this.recordSpend(quote, ref);
3417
- return response;
4355
+ try {
4356
+ const { ref, confirmed } = await this.payAndConfirm(net, wallet, accept);
4357
+ const response = await this.retryWithProof(url, init, accept, ref, confirmed);
4358
+ this.recordSpend(quote, ref, void 0, reservation);
4359
+ return response;
4360
+ } finally {
4361
+ this.ledger.release(reservation);
4362
+ }
3418
4363
  }
3419
4364
  /* ------------------------- internals ------------------------- */
3420
4365
  /**
@@ -3603,9 +4548,11 @@ var PipRailClient = class {
3603
4548
  shortfall.token = formatUnits(amount - bal.token, quote.decimals);
3604
4549
  }
3605
4550
  } else if (isNative) {
3606
- if (nativeKnown && bal.native < amount + fee) {
4551
+ const spendable = bal.token ?? bal.native;
4552
+ const spendableKnown = spendable != null;
4553
+ if (spendableKnown && spendable < amount + fee) {
3607
4554
  blockers.push("INSUFFICIENT_TOKEN");
3608
- shortfall.token = formatUnits(amount + fee - bal.native, quote.decimals);
4555
+ shortfall.token = formatUnits(amount + fee - spendable, quote.decimals);
3609
4556
  }
3610
4557
  } else {
3611
4558
  if (tokenKnown && bal.token < amount) {
@@ -3757,18 +4704,28 @@ var PipRailClient = class {
3757
4704
  quote
3758
4705
  });
3759
4706
  }
4707
+ const reservation = this.ledger.reserve(
4708
+ quote.network,
4709
+ quote.asset,
4710
+ BigInt(quote.amount),
4711
+ quote.decimals,
4712
+ denomOf(quote.symbol, quote.asset, this.opts.policy)
4713
+ );
3760
4714
  const hook = this.opts.onBeforePay;
3761
- if (!hook) return;
4715
+ if (!hook) return reservation;
3762
4716
  let approved;
3763
4717
  try {
3764
4718
  approved = await hook(quote);
3765
4719
  } catch (err) {
4720
+ this.ledger.release(reservation);
3766
4721
  this.refuse("onBeforePay threw \u2014 refusing to pay.", { reasonCode: "APPROVAL", quote, cause: err });
3767
4722
  }
3768
4723
  if (!approved) {
4724
+ this.ledger.release(reservation);
3769
4725
  const reason = `onBeforePay declined ${quote.amountFormatted} ${quote.symbol ?? ""}`.trimEnd() + ` on ${quote.network}.`;
3770
4726
  this.refuse(reason, { reasonCode: "APPROVAL", quote });
3771
4727
  }
4728
+ return reservation;
3772
4729
  }
3773
4730
  /**
3774
4731
  * Refuse a payment BEFORE any send: emit BOTH the legacy `payment-failed` (so existing
@@ -3807,7 +4764,7 @@ var PipRailClient = class {
3807
4764
  * spend (POL-1). So the cap-bearing `amountBase` is the MAX; the clamped actual is surfaced
3808
4765
  * separately on `settledBase`/`settledFormatted` for transparency (it equals the receipt's
3809
4766
  * amount). When absent (onchain-proof/exact) this is byte-identical to before. */
3810
- recordSpend(quote, ref, settledAmountBase) {
4767
+ recordSpend(quote, ref, settledAmountBase, reservation) {
3811
4768
  const denom = denomOf(quote.symbol, quote.asset, this.opts.policy);
3812
4769
  const amountBase = quote.amount;
3813
4770
  const amountFormatted = quote.amountFormatted;
@@ -3838,6 +4795,7 @@ var PipRailClient = class {
3838
4795
  at: (/* @__PURE__ */ new Date()).toISOString()
3839
4796
  };
3840
4797
  this.ledger.record(record, quote.decimals, denom);
4798
+ this.ledger.release(reservation);
3841
4799
  const budget = this.budget();
3842
4800
  if (this.opts.onSpend) {
3843
4801
  try {
@@ -4009,7 +4967,7 @@ var PipRailClient = class {
4009
4967
  * • a 200 whose SettleResponse says `success:false` → a rejection, NEVER a spend;
4010
4968
  * • the spend is recorded EXACTLY ONCE, on an affirmative settlement only.
4011
4969
  */
4012
- async payExactRail(net, wallet, accept, url, init, quote, x402Version = 2) {
4970
+ async payExactRail(net, wallet, accept, url, init, quote, x402Version = 2, reservation) {
4013
4971
  if (!net.payExact) {
4014
4972
  throw new UnsupportedSchemeError(
4015
4973
  `the ${net.family} family can't pay a standard 'exact' rail (supported on EVM, Solana, Algorand, Aptos + NEAR today).`
@@ -4073,7 +5031,7 @@ var PipRailClient = class {
4073
5031
  this.captureReceipt(response, url);
4074
5032
  this.safeEmit({ kind: "payment-settled", receipt, ...settle ? { settle } : {} });
4075
5033
  const ref = settle?.transaction || receipt?.transaction || `${net.family === "evm" ? "eip3009" : net.family}-nonce:${nonce}`;
4076
- this.recordSpend(quote, ref);
5034
+ this.recordSpend(quote, ref, void 0, reservation);
4077
5035
  return response;
4078
5036
  }
4079
5037
  if (response.status >= 500) {
@@ -4104,7 +5062,7 @@ var PipRailClient = class {
4104
5062
  * `settle.amount` FAILS SAFE to the MAX (over-counts, never under-counts). The buyer SIGNS, the
4105
5063
  * merchant self-settles — the buyer never broadcasts.
4106
5064
  */
4107
- async payUptoRail(net, wallet, accept, url, init, quote) {
5065
+ async payUptoRail(net, wallet, accept, url, init, quote, reservation) {
4108
5066
  if (!net.payUpto) {
4109
5067
  throw new UnsupportedSchemeError(
4110
5068
  `the ${net.family} family can't pay a standard 'upto' rail (EVM-Permit2 only today).`
@@ -4158,7 +5116,7 @@ var PipRailClient = class {
4158
5116
  this.safeEmit({ kind: "payment-settled", receipt, ...settle ? { settle } : {} });
4159
5117
  const ref = settle?.transaction || receipt?.transaction || `upto-nonce:${nonce}`;
4160
5118
  const settledAmount = settle?.amount ?? receipt?.amount;
4161
- this.recordSpend(quote, ref, settledAmount);
5119
+ this.recordSpend(quote, ref, settledAmount, reservation);
4162
5120
  return response;
4163
5121
  }
4164
5122
  if (response.status >= 500) {
@@ -4601,6 +5559,71 @@ var MultiChainPayer = class _MultiChainPayer {
4601
5559
  policy() {
4602
5560
  return this._clients[0].policy();
4603
5561
  }
5562
+ /*
5563
+ * ── SWAP, ACROSS A MULTI-CHAIN PAYER ────────────────────────────────────────────
5564
+ *
5565
+ * A swap is SAME-CHAIN by definition, so there is nothing to route: it delegates to the
5566
+ * PRIMARY client, which is the first one given, and the constructor already documents
5567
+ * that order as your chain preference. Without this the MCP could never expose swapping
5568
+ * at all, because it always wraps its accounts in a MultiChainPayer even for one chain.
5569
+ */
5570
+ mode() {
5571
+ return this._clients[0].mode();
5572
+ }
5573
+ canAgentSwap() {
5574
+ return this._clients[0].canAgentSwap();
5575
+ }
5576
+ /*
5577
+ * ── SELLING, ACROSS A MULTI-CHAIN PAYER ─────────────────────────────────────────
5578
+ *
5579
+ * Same reasoning as the swap block above, and the same necessity: the MCP always wraps
5580
+ * its accounts in a MultiChainPayer, so without these the seller tools would be
5581
+ * unreachable through the MCP no matter what mode the operator set.
5582
+ *
5583
+ * The address is the PRIMARY chain's — a wallet has one address per family, not one
5584
+ * overall, so "where do I get paid?" is only answerable per chain. An offer priced on
5585
+ * another chain names its own payTo; this is the default, not the only option.
5586
+ */
5587
+ canAgentSell() {
5588
+ return this._clients[0].canAgentSell();
5589
+ }
5590
+ async address() {
5591
+ return this._clients[0].address();
5592
+ }
5593
+ chain() {
5594
+ return this._clients[0].chain();
5595
+ }
5596
+ /**
5597
+ * Holdings across EVERY chain this payer owns, not just the primary. A multi-chain agent's
5598
+ * balance sheet is the union: reporting only the first chain would tell it it was broke while
5599
+ * it held funds one client along, which is exactly the wrong answer to "what do I have?".
5600
+ */
5601
+ async balanceOf(assets = ["native"]) {
5602
+ const out = [];
5603
+ for (const c of this._clients) {
5604
+ const rows = await c.balanceOf(assets).catch(() => []);
5605
+ for (const r of rows) out.push({ ...r, chain: String(c.chain()) });
5606
+ }
5607
+ return out;
5608
+ }
5609
+ async quoteSwap(req) {
5610
+ return this._clients[0].quoteSwap(req);
5611
+ }
5612
+ async swap(quote) {
5613
+ let firstRefusal;
5614
+ for (const c of this._clients) {
5615
+ try {
5616
+ return await c.swap(quote);
5617
+ } catch (err) {
5618
+ if (err instanceof WrongChainError || err?.name === "UnsupportedNetworkError") {
5619
+ firstRefusal ??= err;
5620
+ continue;
5621
+ }
5622
+ throw err;
5623
+ }
5624
+ }
5625
+ throw firstRefusal ?? new Error("MultiChainPayer: no client could execute this swap quote.");
5626
+ }
4604
5627
  };
4605
5628
  function isBodyInit(value) {
4606
5629
  if (typeof value === "string") return true;
@@ -4741,6 +5764,22 @@ var PIPRAIL_AGENT_GUIDE = `# Paying with PipRail \u2014 the agent contract
4741
5764
  You can pay for x402 "402 Payment Required" resources autonomously. Money moves
4742
5765
  straight from your wallet to the server; PipRail custodies nothing. Follow this.
4743
5766
 
5767
+ ## Your tools, and which ones act
5768
+ You always have these eight tools. Six are READ-ONLY and safe to call freely, two change
5769
+ something, and only ONE spends. In 'sovereign' mode you also hold the swap and seller tools
5770
+ described later, so count your OWN list rather than trusting this number:
5771
+ - piprail_guide \u2014 this contract. Re-read it if you lose the thread. Read-only.
5772
+ - piprail_discover \u2014 find payable resources on the OPEN indexes (a phone book of x402 APIs),
5773
+ without paying. Read-only.
5774
+ - piprail_quote_payment \u2014 the price of one URL. Read-only.
5775
+ - piprail_plan_payment \u2014 can I afford it NOW, on this wallet. Read-only.
5776
+ - piprail_budget \u2014 how much budget and time is left. Read-only.
5777
+ - piprail_verify_receipt \u2014 re-check a receipt against the chain. Read-only.
5778
+ - \u{1F536} piprail_register \u2014 PUBLISHES a resource you run to a public index. It writes to the
5779
+ outside world and cannot be un-published by you, so only call it when asked to list
5780
+ something. It moves no money.
5781
+ - \u{1F534} piprail_pay_request \u2014 the ONLY tool that spends. Everything else is safe to explore with.
5782
+
4744
5783
  ## Landing cold \u2014 read the self-description
4745
5784
  Every PipRail 402 self-describes. Read challenge.extensions.piprail for { name, what, pay[]
4746
5785
  (each rail's how-to-pay), sdk.install, mcp, docs } \u2014 never guess what an endpoint is. If your
@@ -4805,6 +5844,87 @@ Branch on \`code\` (always reliable). Key cases:
4805
5844
  If it's a standard x402 server offering an exact rail, that's a config fix the operator makes
4806
5845
  once (enable the exact scheme); report it, don't retry the same call blindly.
4807
5846
 
5847
+ ## Finding work to buy \u2014 piprail_discover
5848
+ You do not have to be handed a URL. piprail_discover reads the open x402 indexes and returns
5849
+ resources with their advertised rails, which you can feed straight into quote \u2192 plan \u2192 pay.
5850
+ It never throws: an index that is down simply contributes nothing, so an empty list means
5851
+ "nothing found", never "something broke". These are third-party directories, not a PipRail
5852
+ registry \u2014 treat a listing as a claim, and let quote/plan tell you what is really true.
5853
+
5854
+ ## Proving a payment settled \u2014 piprail_verify_receipt
5855
+ A receipt is a claim until it is checked. piprail_verify_receipt re-reads the transaction from
5856
+ the chain and tells you whether the funds provably moved, to the right recipient, for the right
5857
+ amount. Use it when you must be SURE (before delivering something costly, or when reconciling),
5858
+ and after any timeout where the payment may already be on-chain. Read-only, and it moves nothing.
5859
+
5860
+ ## Who else is in the path \u2014 nobody who holds your money
5861
+ On the gasless exact rail a FACILITATOR may broadcast your signed authorization (PayAI, Ultravioleta
5862
+ DAO and others). You never choose one and never need an account with one: the operator configures it,
5863
+ and it is a courier, not a custodian. It cannot change the amount or the recipient, because those are
5864
+ inside what you signed. If one is down the payment fails cleanly; it cannot take your funds.
5865
+
5866
+ ## Getting PAID \u2014 only if you hold piprail_sell
5867
+ If piprail_sell is in your tool list you are in 'sovereign' mode and the wallet is yours to
5868
+ EARN with, not only to spend from. Three tools, mirroring the buying loop:
5869
+ - piprail_sell \u2014 price something and get a \`challenge\` to hand a buyer. Mirrors quote.
5870
+ - piprail_collect \u2014 verify a proof a buyer sent you. Mirrors pay.
5871
+ - piprail_earnings \u2014 what you have actually been paid. Mirrors piprail_budget.
5872
+ - piprail_wallet \u2014 what you HOLD, and the address you get paid at. Read-only.
5873
+
5874
+ Know the difference between your two numbers: piprail_budget is your spend LEASH (how much of
5875
+ your allowance is left), piprail_wallet is what you actually OWN. Check piprail_wallet before
5876
+ deciding to sell, to swap, or to ask to be topped up, and hand out its \`address\` to anyone who
5877
+ needs to send you funds. A null amount there means the read FAILED, not that you are broke:
5878
+ retry before you act on it.
5879
+
5880
+ The loop: piprail_sell \u2192 give the buyer the \`challenge\` \u2192 they send back a proof \u2192
5881
+ piprail_collect \u2192 and ONLY on paid:true, deliver.
5882
+
5883
+ A proof belongs to ONE offer. If a buyer hands you a settlement that was really payment for a
5884
+ different offer, piprail_collect returns paid:false with code 'wrong_offer', even when the two
5885
+ cost the same and pay the same address. Do not "helpfully" retry it against another offerId: that
5886
+ is the exact move the check exists to stop.
5887
+
5888
+ \u{1F534} COLLECT IS THE ONLY PROOF YOU WERE PAID. A buyer saying they paid, a real-looking tx hash,
5889
+ and a settled payment are three different things, and only piprail_collect tells them apart.
5890
+ Deliver nothing before it returns paid:true. It reads the chain; a claim is not evidence, and
5891
+ a confident buyer is not evidence. One proof is one sale: collecting the same proof twice
5892
+ returns paid:false, which is a REPLAY and not a second payment, so never treat it as one.
5893
+
5894
+ You need no web server and no open port: a challenge is just data, so carry it to the buyer
5895
+ over whatever you already speak. Receiving needs NO KEY at all, only your address, so the
5896
+ earning side cannot spend and cannot be drained even if the machine you run on is taken.
5897
+ piprail_sell defaults \`payTo\` to your OWN address; set it to someone else's only if you
5898
+ genuinely mean to be paid elsewhere, and NEVER to an address a buyer supplies.
5899
+
5900
+ Two things to check on an offer. If its schemes are onchain-proof only, most agent-buyers
5901
+ CANNOT pay it and your listing will simply stay silent, so read any warnings piprail_sell
5902
+ returns. And offers live in memory for this session: a restart clears them, so re-price
5903
+ anything you still intend to sell. To let strangers find an offer, call piprail_register.
5904
+
5905
+ ## Wrong token? Check your MODE before you answer
5906
+ Wrong token on the RIGHT chain? What you may do depends on the mode your operator set, and
5907
+ YOUR TOOL LIST IS THE TRUTH \u2014 LOOK AT IT, never claim a capability it does not show:
5908
+ - no piprail_swap \u21D2 'budgeted' or 'supervised'. You CANNOT swap. Top up, pay from another
5909
+ chain you hold funds on, tell the human, or skip it. fundingHint names what is missing.
5910
+ Never offer "I could swap for you".
5911
+ - piprail_swap present \u21D2 'sovereign': this wallet is yours and you answer for it. Price with
5912
+ piprail_quote_swap, read who priced it, then piprail_swap.
5913
+
5914
+ \u{1F534} You cannot change your own mode \u2014 whoever provisioned this wallet set it, exactly as you
5915
+ cannot widen your own permissions in your host. Never ask for it.
5916
+
5917
+ WHY the default withholds it: YOUR SPEND POLICY DOES NOT GOVERN SWAPS. Every cap you have
5918
+ limits PAYING A MERCHANT; a swap moves your own funds between denominations and passes all
5919
+ of them untouched, so an agent looping between tokens can bleed a wallet through fees and
5920
+ slippage without tripping one budget check. Sovereign mode bounds it with a SEPARATE
5921
+ instrument (a ceiling per swap and on slippage), so a refusal there is your swapPolicy, not
5922
+ your budget.
5923
+
5924
+ True in EVERY mode: a swap is SAME-CHAIN only and can never move funds between chains, and
5925
+ PipRail prices nothing itself, so every quote NAMES the venue that priced it \u2014 read
5926
+ \`source\` before you trust a number. docs.piprail.com/making-payments/swapping/
5927
+
4808
5928
  ## Knowing your leash \u2014 call piprail_budget
4809
5929
  piprail_budget tells you how much budget and time you have left: per (network,
4810
5930
  asset) remaining, the cross-token GRAND TOTAL per denomination (e.g. how much
@@ -4812,13 +5932,15 @@ USD you can still spend across every stablecoin and chain), the payment-count
4812
5932
  leash, the session time envelope, your spend so far, and the configured policy
4813
5933
  read back. Read-only; moves no funds. Use it in Mode A to self-check before paying.
4814
5934
 
4815
- ## Two modes
4816
- - Mode A (headless, default): you run FREE inside a pre-set budget + time
4817
- envelope. The policy IS the consent \u2014 there is no per-payment prompt. Stay
4818
- inside it; piprail_budget shows what's left.
4819
- - Mode B (supervised): the host may ask a human to approve each payment. A
4820
- decline/cancel/timeout comes back as declined:true (reasonCode:'APPROVAL') \u2014
4821
- do NOT retry it as if it were a transient error.
5935
+ ## Two modes of CONSENT \u2014 a different axis from the mode above
5936
+ Your 'supervised'/'budgeted'/'sovereign' mode says what you may DO. This says how each
5937
+ payment is agreed. Both are in force at once, so read them together:
5938
+ - Mode A (headless \u2014 how 'budgeted' and 'sovereign' behave): you run FREE inside a pre-set
5939
+ budget + time envelope. The policy IS the consent, and there is no per-payment prompt.
5940
+ Stay inside it; piprail_budget shows what's left.
5941
+ - Mode B (supervised \u2014 how the 'supervised' mode behaves): the host may ask a human to
5942
+ approve each payment. A decline/cancel/timeout comes back as declined:true
5943
+ (reasonCode:'APPROVAL') \u2014 do NOT retry it as if it were a transient error.
4822
5944
 
4823
5945
  ## Hard facts
4824
5946
  - Per-payment + per-(network, asset) caps always apply. A cross-token GRAND TOTAL per
@@ -4835,493 +5957,78 @@ function agentGuide() {
4835
5957
  return PIPRAIL_AGENT_GUIDE;
4836
5958
  }
4837
5959
 
4838
- // src/agent.ts
4839
- var OPEN_OBJECT = { type: "object", additionalProperties: true };
4840
- async function readBody(res) {
4841
- const text = await res.text();
4842
- if (!text) return null;
5960
+ // src/discovery.ts
5961
+ var GENERATOR = "@piprail/sdk \xB7 https://piprail.com";
5962
+ var POWERED_BY = "PipRail x402 | https://piprail.com";
5963
+ function discoveryHeaders(opts = {}) {
5964
+ return {
5965
+ link: '</openapi.json>; rel="service-desc", </.well-known/x402>; rel="x402-discovery"',
5966
+ ...opts.attribution === false ? {} : { "x-powered-by": POWERED_BY }
5967
+ };
5968
+ }
5969
+ function buildBazaarExtension(descriptor = {}) {
5970
+ const method = (descriptor.method ?? "GET").toUpperCase();
5971
+ const queryParams = descriptor.queryParams ?? {};
5972
+ return {
5973
+ info: {
5974
+ input: { type: "http", method, queryParams },
5975
+ output: descriptor.output ?? { type: "json" }
5976
+ },
5977
+ schema: {
5978
+ $schema: "https://json-schema.org/draft/2020-12/schema",
5979
+ type: "object",
5980
+ properties: {
5981
+ input: {
5982
+ type: "object",
5983
+ properties: {
5984
+ type: { type: "string", const: "http" },
5985
+ method: { type: "string", enum: ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"] },
5986
+ queryParams: { type: "object", properties: queryParams, additionalProperties: false }
5987
+ },
5988
+ required: ["type", "method"],
5989
+ additionalProperties: false
5990
+ }
5991
+ },
5992
+ required: ["input"]
5993
+ }
5994
+ };
5995
+ }
5996
+ function pathOf(url) {
4843
5997
  try {
4844
- return JSON.parse(text);
5998
+ return new URL(url).pathname || "/";
4845
5999
  } catch {
4846
- return text;
6000
+ return url.startsWith("/") ? url : `/${url}`;
4847
6001
  }
4848
6002
  }
4849
- function toToolError(err) {
4850
- if (!(err instanceof PipRailError)) throw err;
4851
- const out = {
4852
- ok: false,
4853
- code: err.code,
4854
- reason: err.message,
4855
- explain: explainDecline(err)
4856
- };
4857
- if (err instanceof PaymentDeclinedError) {
4858
- out.declined = true;
4859
- if (err.reasonCode) out.reasonCode = err.reasonCode;
6003
+ function buildOpenApi(input) {
6004
+ assertResourceList(input?.resources, "buildOpenApi");
6005
+ const paths = {};
6006
+ for (const r of input.resources) {
6007
+ const path = pathOf(r.url);
6008
+ const method = (r.method ?? "GET").toLowerCase();
6009
+ const op = {
6010
+ ...r.description ? { summary: r.description } : {},
6011
+ responses: {
6012
+ "200": { description: "Paid \u2014 the resource." },
6013
+ "402": { description: "Payment required (x402)." }
6014
+ },
6015
+ "x-payment-info": {
6016
+ x402Version: 2,
6017
+ accepts: r.accepts
6018
+ }
6019
+ };
6020
+ paths[path] = { ...paths[path] ?? {}, [method]: op };
4860
6021
  }
4861
- const ref = err.ref;
4862
- if (typeof ref === "string") out.ref = ref;
4863
- return out;
4864
- }
4865
- function paymentTools(client) {
4866
- return [
4867
- {
4868
- name: "piprail_discover",
4869
- description: `Find x402 payment-gated resources on the OPEN indexes (a phone book of payable APIs) WITHOUT paying. Use it to answer "what can I buy?" \u2014 search by topic, then quote/plan/pay a chosen one. By default returns only resources payable on your wallet's chain (network='self'); pass 'any' for every chain. Results are cross-scheme: ALWAYS call piprail_quote_payment on a chosen resource (it re-checks the live price) before piprail_pay_request.`,
4870
- annotations: {
4871
- title: "Discover payable x402 APIs",
4872
- readOnlyHint: true,
4873
- // reads the open indexes only; never pays
4874
- openWorldHint: true
4875
- // reaches external indexes (402 Index, CDP Bazaar)
4876
- },
4877
- parameters: {
4878
- type: "object",
4879
- properties: {
4880
- query: {
4881
- type: "string",
4882
- description: 'Free-text topic to search for (optional). Multi-word queries are fanned out per word and results are ranked by relevance, so "crypto price feed" finds the best matches even when no single listing contains that exact phrase.'
4883
- },
4884
- network: {
4885
- type: "string",
4886
- description: "CAIP-2 id, 'self' (your chain \u2014 default), or 'any' (all chains)."
4887
- },
4888
- category: { type: "string", description: "Keep ONLY this category, e.g. 'ai', 'finance', 'data' (strict)." },
4889
- asset: { type: "string", description: "Keep only resources paying in this token symbol, e.g. 'USDC'." },
4890
- maxPrice: { type: "number", description: "Drop results advertised above this USD price." },
4891
- minReliability: { type: "number", description: "Drop results below this health score (0\u2013100); unscored pass." },
4892
- verified: { type: "boolean", description: "Prefer verified listings (402 Index)." },
4893
- sort: {
4894
- type: "string",
4895
- enum: ["relevance", "reliability", "price", "uptime", "name"],
4896
- description: "Ordering. Default 'relevance' with a query, else first-seen."
4897
- },
4898
- limit: { type: "number", description: "Max results to fetch per index (default 20)." }
4899
- },
4900
- additionalProperties: false
4901
- },
4902
- invoke: async (args) => {
4903
- try {
4904
- const opts = {};
4905
- if (typeof args.query === "string") opts.query = args.query;
4906
- if (typeof args.network === "string") opts.network = args.network;
4907
- if (typeof args.category === "string") opts.category = args.category;
4908
- if (typeof args.asset === "string") opts.asset = args.asset;
4909
- if (typeof args.maxPrice === "number") opts.maxPrice = args.maxPrice;
4910
- if (typeof args.minReliability === "number") opts.minReliability = args.minReliability;
4911
- if (typeof args.verified === "boolean") opts.verified = args.verified;
4912
- if (typeof args.sort === "string") opts.sort = args.sort;
4913
- if (typeof args.limit === "number") opts.limit = args.limit;
4914
- const found = await client.discover(opts);
4915
- return {
4916
- count: found.length,
4917
- resources: found.map((r) => ({
4918
- resource: r.resource,
4919
- name: r.name,
4920
- description: r.description,
4921
- source: r.source,
4922
- category: r.category,
4923
- priceUsd: r.priceUsd,
4924
- reliabilityScore: r.reliabilityScore,
4925
- health: r.health,
4926
- verified: r.verified,
4927
- networks: [...new Set(r.rails.map((rail) => rail.network))]
4928
- }))
4929
- };
4930
- } catch (err) {
4931
- return toToolError(err);
4932
- }
4933
- }
4934
- },
4935
- {
4936
- name: "piprail_quote_payment",
4937
- description: "Get the price of an x402 payment-gated URL WITHOUT paying. Returns the amount, token, chain, recipient, and whether it is within the spend policy. Returns { gated: false } when the URL needs no payment. Call this first to decide whether a resource is worth buying.",
4938
- annotations: {
4939
- title: "Quote an x402 price",
4940
- readOnlyHint: true,
4941
- // reads the 402 challenge; never pays
4942
- openWorldHint: true
4943
- // fetches an arbitrary URL
4944
- },
4945
- parameters: {
4946
- type: "object",
4947
- properties: {
4948
- url: { type: "string", description: "Full URL of the gated resource." }
4949
- },
4950
- required: ["url"],
4951
- additionalProperties: false
4952
- },
4953
- outputSchema: OPEN_OBJECT,
4954
- invoke: async (args) => {
4955
- try {
4956
- const quote = await client.quote(String(args.url));
4957
- return quote ? { gated: true, ...quote } : { gated: false, url: String(args.url) };
4958
- } catch (err) {
4959
- return toToolError(err);
4960
- }
4961
- }
4962
- },
4963
- {
4964
- name: "piprail_plan_payment",
4965
- description: "Check whether you CAN pay an x402-gated URL before paying. Reads your wallet balance, native gas, and whether the recipient can receive \u2014 across every rail the URL offers on your chain \u2014 and returns { gated, payable, best, options, fundingHint }. payable:false means do NOT attempt the payment; fundingHint says exactly what to top up. Call this before piprail_pay_request so you never commit to a payment you cannot finish. Returns { gated: false } when no payment is needed.",
4966
- annotations: {
4967
- title: "Plan an x402 payment",
4968
- readOnlyHint: true,
4969
- // reads balances + the challenge; never pays
4970
- openWorldHint: true
4971
- // fetches a URL and reads chain state
4972
- },
4973
- parameters: {
4974
- type: "object",
4975
- properties: {
4976
- url: { type: "string", description: "Full URL of the gated resource." }
4977
- },
4978
- required: ["url"],
4979
- additionalProperties: false
4980
- },
4981
- outputSchema: OPEN_OBJECT,
4982
- invoke: async (args) => {
4983
- try {
4984
- const plan = await client.planPayment(String(args.url));
4985
- if (plan == null) return { gated: false, url: String(args.url) };
4986
- return {
4987
- gated: true,
4988
- payable: plan.payable,
4989
- status: plan.status,
4990
- fundingHint: plan.fundingHint,
4991
- // One model-readable line distilling the whole plan.
4992
- summary: summarizePlan(plan),
4993
- best: plan.best ? {
4994
- network: plan.best.accept.network,
4995
- symbol: plan.best.quote.symbol,
4996
- amount: plan.best.quote.amountFormatted,
4997
- gasCoin: plan.best.cost.feeSymbol,
4998
- gas: plan.best.cost.feeFormatted
4999
- } : null,
5000
- options: plan.options.map((o) => ({
5001
- network: o.accept.network,
5002
- symbol: o.quote.symbol,
5003
- amount: o.quote.amountFormatted,
5004
- state: o.state,
5005
- blockers: o.blockers,
5006
- warnings: o.warnings,
5007
- recipientReady: o.recipient.ready
5008
- })),
5009
- // The session's time leash, present only when a time policy is configured.
5010
- ...plan.session ? { session: plan.session } : {}
5011
- };
5012
- } catch (err) {
5013
- return toToolError(err);
5014
- }
5015
- }
5016
- },
5017
- {
5018
- name: "piprail_pay_request",
5019
- description: "Fetch an x402 payment-gated URL, automatically making the required payment if needed (subject to the spend policy + approval hook). Pays whichever rail the client is configured for \u2014 PipRail's backendless on-chain rail, or, when enabled, the standard `exact` rail (where the buyer signs and the server settles, so no buyer gas). Returns the HTTP status, the response body, and a payment receipt if one settled. If the payment is refused by policy or the approval hook, returns { declined: true, reason } \u2014 no funds moved.",
5020
- annotations: {
5021
- title: "Pay an x402 request",
5022
- readOnlyHint: false,
5023
- // this is the one tool that MOVES FUNDS
5024
- destructiveHint: true,
5025
- // a payment is value-moving and not reversible
5026
- idempotentHint: false,
5027
- // paying twice = two payments
5028
- openWorldHint: true
5029
- // fetches a URL and settles a payment
5030
- },
5031
- parameters: {
5032
- type: "object",
5033
- properties: {
5034
- url: { type: "string", description: "Full URL to fetch." },
5035
- method: { type: "string", description: "HTTP method, default 'GET'." },
5036
- body: {
5037
- type: ["object", "string"],
5038
- description: "Optional request body for POST/PUT (a JSON object or a string)."
5039
- }
5040
- },
5041
- required: ["url"],
5042
- additionalProperties: false
5043
- },
5044
- invoke: async (args) => {
5045
- const url = String(args.url);
5046
- const method = (args.method ? String(args.method) : "GET").toUpperCase();
5047
- try {
5048
- let res;
5049
- if (method === "GET") {
5050
- res = await client.get(url);
5051
- } else {
5052
- const headers = {};
5053
- let body;
5054
- if (args.body !== void 0 && args.body !== null) {
5055
- if (typeof args.body === "string") {
5056
- body = args.body;
5057
- } else {
5058
- body = JSON.stringify(args.body);
5059
- headers["content-type"] = "application/json";
5060
- }
5061
- }
5062
- res = await client.fetch(url, { method, headers, body });
5063
- }
5064
- const verifiable = parseReceiptExtension(res);
5065
- return {
5066
- status: res.status,
5067
- ok: res.ok,
5068
- body: await readBody(res),
5069
- receipt: parseReceipt(res),
5070
- ...verifiable ? { verifiableReceipt: { ...verifiable, resource: { url } } } : {}
5071
- };
5072
- } catch (err) {
5073
- if (err instanceof PipRailError) {
5074
- const out = {
5075
- ok: false,
5076
- code: err.code,
5077
- reason: err.message,
5078
- explain: explainDecline(err)
5079
- };
5080
- if (err instanceof PaymentDeclinedError) {
5081
- out.declined = true;
5082
- if (err.reasonCode) out.reasonCode = err.reasonCode;
5083
- }
5084
- const ref = err.ref;
5085
- if (typeof ref === "string") out.ref = ref;
5086
- return out;
5087
- }
5088
- throw err;
5089
- }
5090
- }
5091
- },
5092
- {
5093
- name: "piprail_register",
5094
- description: "List an x402 payment-gated resource YOU run on the open indexes so other agents can discover it. Default target is 402 Index \u2014 no auth, no signature, no payment; a self-registered listing is pending review (verify your domain on 402index.io for instant approval). Returns one outcome per index ({ source, ok, detail, visibility, note }); a step the chain can't satisfy comes back ok:false with the reason. Moves no funds; nothing is PipRail-hosted. NOTE: index/agent payers are overwhelmingly standard `exact` clients \u2014 a default onchain-proof-only gate gets listed but they cannot pay it, so add an `exact` rail (and set the gate's `discovery` option, required for x402scan) to be usefully discoverable AND payable.",
5095
- annotations: {
5096
- title: "Register an x402 endpoint",
5097
- readOnlyHint: false,
5098
- // writes a listing to an external index
5099
- destructiveHint: false,
5100
- // adds a listing; nothing is destroyed and no funds move
5101
- openWorldHint: true
5102
- // posts to external indexes (402 Index)
5103
- // idempotentHint intentionally omitted — index dedup behaviour isn't guaranteed.
5104
- },
5105
- parameters: {
5106
- type: "object",
5107
- properties: {
5108
- url: { type: "string", description: "Full URL of the resource to list." },
5109
- name: { type: "string", description: "Display name (defaults to the host)." },
5110
- description: {
5111
- type: "string",
5112
- description: "What the resource offers. Pack the words agents will search for INTO this text \u2014 index search is literal, so a keyword that isn't in the name/description won't be found."
5113
- },
5114
- category: { type: "string", description: "A category, e.g. 'ai', 'finance', 'data' \u2014 the top findability field (most listings have none)." },
5115
- tags: { type: "array", items: { type: "string" }, description: "Keywords; folded into the description so they're searchable." },
5116
- priceUsd: { type: "number", description: "Advertised price in USD (metadata)." },
5117
- network: {
5118
- type: "string",
5119
- description: "Network slug to advertise, e.g. 'base' (defaults to the paying chain). Set it when registering from a multi-chain wallet so the listing names the right chain."
5120
- },
5121
- asset: { type: "string", description: "Payment asset symbol, e.g. 'USDC' (metadata)." },
5122
- provider: { type: "string", description: "Who runs the resource (provider/org name)." },
5123
- contactEmail: { type: "string", description: "Contact email for the listing." }
5124
- },
5125
- required: ["url"],
5126
- additionalProperties: false
5127
- },
5128
- invoke: async (args) => {
5129
- try {
5130
- const opts = {};
5131
- if (typeof args.name === "string") opts.name = args.name;
5132
- if (typeof args.description === "string") opts.description = args.description;
5133
- if (typeof args.category === "string") opts.category = args.category;
5134
- if (Array.isArray(args.tags)) opts.tags = args.tags.filter((t) => typeof t === "string");
5135
- if (typeof args.priceUsd === "number") opts.priceUsd = args.priceUsd;
5136
- if (typeof args.network === "string") opts.network = args.network;
5137
- if (typeof args.asset === "string") opts.asset = args.asset;
5138
- if (typeof args.provider === "string") opts.provider = args.provider;
5139
- if (typeof args.contactEmail === "string") opts.contactEmail = args.contactEmail;
5140
- const outcomes = await client.register(String(args.url), opts);
5141
- return { outcomes };
5142
- } catch (err) {
5143
- return toToolError(err);
5144
- }
5145
- }
5146
- },
5147
- {
5148
- name: "piprail_budget",
5149
- description: "Read how much of your spend budget and time leash is left \u2014 per (network, asset) remaining, the cross-token GRAND TOTAL per denomination (e.g. how much USD you can still spend across every stablecoin and chain), the payment-count leash, the session time envelope, and your spend so far. Use it in Mode A (headless) to self-check BEFORE paying, so you never discover the leash by hitting a decline. Read-only; moves no funds. NOTE: the time envelope is in-memory for THIS process; the money/count totals persist only if a spend store is configured.",
5150
- annotations: {
5151
- title: "Check remaining budget",
5152
- readOnlyHint: true,
5153
- // reads the ledger + policy; never pays
5154
- idempotentHint: true
5155
- // a pure read
5156
- },
5157
- parameters: { type: "object", properties: {}, additionalProperties: false },
5158
- outputSchema: OPEN_OBJECT,
5159
- invoke: async () => {
5160
- try {
5161
- const spent = client.spent();
5162
- const budget = client.budget();
5163
- return {
5164
- spent,
5165
- remaining: budget.byAsset,
5166
- grandTotal: budget.byDenom,
5167
- // cross-token spend cap per denomination (USD/EUR/…)
5168
- counts: budget.counts,
5169
- // payment-count leash (settled + lifetime/window caps)
5170
- session: budget.session,
5171
- policy: client.policy() ?? null,
5172
- // the configured leash, read back
5173
- report: formatSpendReport(spent)
5174
- };
5175
- } catch (err) {
5176
- return toToolError(err);
5177
- }
5178
- }
5179
- },
5180
- {
5181
- name: "piprail_guide",
5182
- description: "Read the PipRail agent contract \u2014 the quote \u2192 plan \u2192 pay loop, how to read a refusal (and which declines are TERMINAL), the never-re-pay rule for broadcast-but-unconfirmed payments, and Mode A (headless) vs Mode B (supervised). Read-only; call it once if unsure how to use these tools.",
5183
- annotations: {
5184
- title: "How to use PipRail",
5185
- readOnlyHint: true,
5186
- idempotentHint: true
5187
- },
5188
- parameters: { type: "object", properties: {}, additionalProperties: false },
5189
- invoke: async () => ({ guide: PIPRAIL_AGENT_GUIDE })
5190
- },
5191
- {
5192
- name: "piprail_verify_receipt",
5193
- description: "Re-verify a PipRail VERIFIABLE RECEIPT against the chain \u2014 confirm a payment REALLY settled (the funds provably moved to payTo for AT LEAST the stated amount) WITHOUT trusting whoever handed you the receipt. Read-only and WALLET-FREE: pass the PipRailReceipt JSON (from a prior piprail_pay_request `verifiableReceipt`, or any third party). Returns { ok, onChain:{payTo,asset,amount,payer}, matchesClaims, ageSeconds, error? }: `ok` = the chain confirms the settlement; `onChain.payer` is RE-DERIVED from the tx and `matchesClaims:false` means the receipt forged the payer; `amount` is a verified lower bound. Pass `rpcUrl` for a chain outside the common presets.",
5194
- annotations: {
5195
- title: "Verify a payment receipt",
5196
- readOnlyHint: true,
5197
- // re-reads the chain; moves nothing, needs no wallet
5198
- idempotentHint: true,
5199
- openWorldHint: true
5200
- // reads an on-chain tx via RPC
5201
- },
5202
- parameters: {
5203
- type: "object",
5204
- properties: {
5205
- receipt: {
5206
- type: "object",
5207
- description: "The PipRailReceipt JSON ({ piprail, receipt, resource, decimals? }) to re-verify."
5208
- },
5209
- rpcUrl: {
5210
- type: "string",
5211
- description: "Optional RPC URL for the receipt's chain (required for chains outside the common presets)."
5212
- }
5213
- },
5214
- required: ["receipt"],
5215
- additionalProperties: false
5216
- },
5217
- invoke: async (args) => {
5218
- const receipt = args.receipt;
5219
- const opts = args.rpcUrl ? { rpcUrl: String(args.rpcUrl) } : void 0;
5220
- return await PipRailClient.verifyReceipt(receipt, opts);
5221
- }
5222
- }
5223
- ];
5224
- }
5225
-
5226
- // src/classify.ts
5227
- function classifyChallenge(challenge, opts) {
5228
- try {
5229
- const accepts = Array.isArray(challenge?.accepts) ? challenge.accepts : [];
5230
- const network = opts?.network;
5231
- const schemes = opts?.schemes ?? [];
5232
- const offeredSchemes = [
5233
- ...new Set(accepts.map((a) => a?.scheme).filter((s) => s != null))
5234
- ];
5235
- const offeredNetworks = [...new Set(accepts.map((a) => a?.network).filter((n) => n != null))];
5236
- const onClientChain = accepts.some((a) => a?.network === network);
5237
- const payableScheme = accepts.some(
5238
- (a) => a?.network === network && schemes.includes(a?.scheme)
5239
- );
5240
- const verdict = accepts.length === 0 ? "NO_RAIL" : payableScheme ? "PAYABLE_RAIL" : onClientChain ? "UNPAYABLE_SCHEME" : "WRONG_CHAIN";
5241
- return { onClientChain, payableScheme, offeredSchemes, offeredNetworks, verdict };
5242
- } catch {
5243
- return {
5244
- onClientChain: false,
5245
- payableScheme: false,
5246
- offeredSchemes: [],
5247
- offeredNetworks: [],
5248
- verdict: "NO_RAIL"
5249
- };
5250
- }
5251
- }
5252
-
5253
- // src/discovery.ts
5254
- var GENERATOR = "@piprail/sdk \xB7 https://piprail.com";
5255
- var POWERED_BY = "PipRail x402 | https://piprail.com";
5256
- function discoveryHeaders(opts = {}) {
5257
- return {
5258
- link: '</openapi.json>; rel="service-desc", </.well-known/x402>; rel="x402-discovery"',
5259
- ...opts.attribution === false ? {} : { "x-powered-by": POWERED_BY }
5260
- };
5261
- }
5262
- function buildBazaarExtension(descriptor = {}) {
5263
- const method = (descriptor.method ?? "GET").toUpperCase();
5264
- const queryParams = descriptor.queryParams ?? {};
5265
- return {
5266
- info: {
5267
- input: { type: "http", method, queryParams },
5268
- output: descriptor.output ?? { type: "json" }
5269
- },
5270
- schema: {
5271
- $schema: "https://json-schema.org/draft/2020-12/schema",
5272
- type: "object",
5273
- properties: {
5274
- input: {
5275
- type: "object",
5276
- properties: {
5277
- type: { type: "string", const: "http" },
5278
- method: { type: "string", enum: ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"] },
5279
- queryParams: { type: "object", properties: queryParams, additionalProperties: false }
5280
- },
5281
- required: ["type", "method"],
5282
- additionalProperties: false
5283
- }
5284
- },
5285
- required: ["input"]
5286
- }
5287
- };
5288
- }
5289
- function pathOf(url) {
5290
- try {
5291
- return new URL(url).pathname || "/";
5292
- } catch {
5293
- return url.startsWith("/") ? url : `/${url}`;
5294
- }
5295
- }
5296
- function buildOpenApi(input) {
5297
- assertResourceList(input?.resources, "buildOpenApi");
5298
- const paths = {};
5299
- for (const r of input.resources) {
5300
- const path = pathOf(r.url);
5301
- const method = (r.method ?? "GET").toLowerCase();
5302
- const op = {
5303
- ...r.description ? { summary: r.description } : {},
5304
- responses: {
5305
- "200": { description: "Paid \u2014 the resource." },
5306
- "402": { description: "Payment required (x402)." }
5307
- },
5308
- "x-payment-info": {
5309
- x402Version: 2,
5310
- accepts: r.accepts
5311
- }
5312
- };
5313
- paths[path] = { ...paths[path] ?? {}, [method]: op };
5314
- }
5315
- return {
5316
- openapi: "3.1.0",
5317
- info: { title: input.title ?? "PipRail x402 resources", version: input.version ?? "1.0.0" },
5318
- servers: [{ url: input.origin }],
5319
- paths,
5320
- // "Built with @piprail/sdk" — default on (opt out with attribution:false). At the
5321
- // document ROOT so `info` stays exactly { title, version }.
5322
- ...input.attribution === false ? {} : { "x-generator": GENERATOR },
5323
- ...input.ownershipProofs && input.ownershipProofs.length > 0 ? { "x-agentcash-provenance": { ownershipProofs: input.ownershipProofs } } : {}
5324
- };
6022
+ return {
6023
+ openapi: "3.1.0",
6024
+ info: { title: input.title ?? "PipRail x402 resources", version: input.version ?? "1.0.0" },
6025
+ servers: [{ url: input.origin }],
6026
+ paths,
6027
+ // "Built with @piprail/sdk" — default on (opt out with attribution:false). At the
6028
+ // document ROOT so `info` stays exactly { title, version }.
6029
+ ...input.attribution === false ? {} : { "x-generator": GENERATOR },
6030
+ ...input.ownershipProofs && input.ownershipProofs.length > 0 ? { "x-agentcash-provenance": { ownershipProofs: input.ownershipProofs } } : {}
6031
+ };
5325
6032
  }
5326
6033
  function buildWellKnownX402(input) {
5327
6034
  assertResourceList(input?.resources, "buildWellKnownX402");
@@ -5423,7 +6130,7 @@ function renderLandingPage(sd) {
5423
6130
  }
5424
6131
 
5425
6132
  // src/facilitator.ts
5426
- async function fetchFacilitatorFeePayer(url, network, timeoutMs = 8e3) {
6133
+ async function fetchFacilitatorFeePayer(url, network, timeoutMs = 15e3) {
5427
6134
  const base2 = url.replace(/\/+$/, "");
5428
6135
  const ctrl = new AbortController();
5429
6136
  const timer = setTimeout(() => ctrl.abort(), timeoutMs);
@@ -5463,7 +6170,7 @@ function parseFacilitatorSupported(body) {
5463
6170
  }
5464
6171
  return out;
5465
6172
  }
5466
- async function facilitatorCoverage(url, timeoutMs = 8e3) {
6173
+ async function facilitatorCoverage(url, timeoutMs = 15e3) {
5467
6174
  const base2 = url.replace(/\/+$/, "");
5468
6175
  const ctrl = new AbortController();
5469
6176
  const timer = setTimeout(() => ctrl.abort(), timeoutMs);
@@ -5506,10 +6213,19 @@ async function post(url, body, headers) {
5506
6213
  }
5507
6214
  async function settleViaFacilitator(input) {
5508
6215
  const base2 = input.url.replace(/\/+$/, "");
6216
+ const v2 = input.x402Version >= 2;
5509
6217
  const body = {
5510
6218
  x402Version: input.x402Version,
5511
6219
  paymentPayload: input.paymentPayload,
5512
- paymentRequirements: input.paymentRequirements
6220
+ paymentRequirements: input.paymentRequirements,
6221
+ ...v2 ? {
6222
+ accepted: input.paymentRequirements,
6223
+ resource: {
6224
+ url: input.resource?.url || "https://piprail.com/x402/resource",
6225
+ description: input.resource?.description || "Paid resource",
6226
+ mimeType: input.resource?.mimeType || "application/json"
6227
+ }
6228
+ } : {}
5513
6229
  };
5514
6230
  const auth = input.authHeaders ? await input.authHeaders() : {};
5515
6231
  let verify;
@@ -5775,6 +6491,13 @@ var KNOWN_FACILITATORS = {
5775
6491
  schemes: ["exact"],
5776
6492
  settles: ["eip3009"],
5777
6493
  note: "Dexter: keyless, sponsors gas (no floor hit at $0.001 here). LIVE-settled on Avalanche 2026-06-18 (tx 0xb2263e9a4ea3917eee6acabcb454d42a50264fdd69a0781ed8fcaec5590e264b)."
6494
+ },
6495
+ {
6496
+ url: "https://facilitator.ultravioletadao.xyz",
6497
+ keyless: true,
6498
+ schemes: ["exact"],
6499
+ settles: ["eip3009"],
6500
+ note: "Ultravioleta DAO: keyless, 100% gas-sponsored (Avalanche native USDC EIP-3009). LIVE-settled on Avalanche 2026-09-07 (tx 0xe9a88e4fa978a16599b6188444d87ea63d43122e5fad16493568c2a596a79436): the buyer held ZERO AVAX and the payment still landed, replay rejected."
5778
6501
  }
5779
6502
  ],
5780
6503
  // Sei (eip155:1329) — PayAI (the only keyless facilitator that lists Sei).
@@ -5813,6 +6536,27 @@ var KNOWN_FACILITATORS = {
5813
6536
  note: "GoPlausible: keyless, 100% gas-sponsored (Algorand USDCa, atomic-group fee pooling; both buyer AND merchant pay 0 ALGO). LIVE-settled on Algorand mainnet 2026-06-17 (tx PDVDVRFGJAG2K6AJ7L26OTSCSRL7AURVKEX4D4KHBAOLNSCYENXA). The only keyless Algorand x402 facilitator."
5814
6537
  }
5815
6538
  ],
6539
+ // Robinhood Chain (eip155:4663) — the tokenized-equity Arbitrum Orbit L2, mainnet since
6540
+ // 2026-07-01. Both facilitators settle the `exact` rail in USDG (Paxos Global Dollar), the
6541
+ // asset the chain's own markets quote against; the address below matched our on-chain read
6542
+ // and Paxos's published list exactly. USDG is EIP-3009 AND EIP-2612 (verified on-chain
6543
+ // 2026-09-08), so the gasless rail is real rather than inferred from the chain being an L2.
6544
+ "eip155:4663": [
6545
+ {
6546
+ url: "https://facilitator.ultravioletadao.xyz",
6547
+ keyless: true,
6548
+ schemes: ["exact"],
6549
+ settles: ["eip3009"],
6550
+ note: "Ultravioleta DAO: keyless, 100% gas-sponsored (Robinhood USDG EIP-3009). LIVE-settled on Robinhood mainnet 2026-09-08 (tx 0x35361e5713918e4ca76c8cf9c25fe118d6473b36db19cde3e00ed2f43f990a88): buyer AND merchant both paid 0 ETH, replay rejected."
6551
+ },
6552
+ {
6553
+ url: "https://x402.dexter.cash",
6554
+ keyless: true,
6555
+ schemes: ["exact"],
6556
+ settles: ["permit2"],
6557
+ note: 'Dexter: keyless, gas-sponsored. Robinhood USDG (EIP-712 domain name "Global Dollar" version "1", which matches the on-chain DOMAIN_SEPARATOR we recomputed). Enforces a ~$0.0066 dynamic floor (minPaymentAmountAtomic 6554), so a sub-cent payment is rejected. LIVE-settled on Robinhood mainnet 2026-09-08 (tx 0x1ba66e3dfd60e1d32fe4e38b68368a064a20b57858d6311feb08b4d0ba988d99): buyer and merchant paid 0 ETH, replay rejected. A 2nd keyless Robinhood facilitator (failover for UVD).'
6558
+ }
6559
+ ],
5816
6560
  // Solana (mainnet-beta). Keyless fee-payer sponsors for the SVM exact rail, each LIVE-settled
5817
6561
  // by us (a real SPL TransferChecked, buyer paid zero SOL) on the dated day — beyond a /supported
5818
6562
  // read. Daydreams + Questflow are intentionally ABSENT: their /supported is public but /verify
@@ -5839,18 +6583,26 @@ var KNOWN_FACILITATORS = {
5839
6583
  settles: ["svm"],
5840
6584
  note: "GoPlausible: keyless fee-payer sponsor (Solana SPL SVM). LIVE-settled on Solana mainnet 2026-09-06 (tx 3DEGg6Lue8471meBH8hbLxV1bLmZGKvmvmSY3tjg7tQFnxzE9GQXUXokjQnZgTqaEU4SaXuY9F7v3m7JsX5xDgrZ): buyer paid 0 SOL, GoPlausible's sponsor 8a8fFNfk\u2026 paid the 10,001-lamport fee, replay rejected."
5841
6585
  }
6586
+ ],
6587
+ // NEAR (near:mainnet) — the first keyless facilitator settlement on NEAR.
6588
+ //
6589
+ // 🔴 A CORRECTION, kept here because the wrong conclusion was recorded for months. This entry
6590
+ // read "DELIBERATELY UNSEEDED — UVD's /verify 400s on near:mainnet, so the listing is aspirational,
6591
+ // not settle-capable (2026-06-18)". That was OUR bug, not theirs. The 400 was
6592
+ // `data did not match any variant of untagged enum VerifyRequestEnvelope`: PipRail sent an x402 v2
6593
+ // body carrying only the v1 `paymentRequirements` key, and a strict v2 facilitator needs `accepted`
6594
+ // and `resource` at the root. We read a deserialization error as a missing chain implementation.
6595
+ // Fixed in facilitator.ts (a v2 body now carries both spellings); NEAR settled first time after.
6596
+ // The lesson: a 400 from a facilitator is a claim about the REQUEST until you have read its body.
6597
+ "near:mainnet": [
6598
+ {
6599
+ url: "https://facilitator.ultravioletadao.xyz",
6600
+ keyless: true,
6601
+ schemes: ["exact"],
6602
+ settles: ["near"],
6603
+ note: "Ultravioleta DAO: keyless fee-payer sponsor (NEAR NEP-141 USDC, NEP-366 meta-transaction). LIVE-settled on NEAR mainnet 2026-09-07 (tx 8pVe4oeykCT2WpCwToNbkxjdgSvwNNtJUrGezDCHKDda): buyer paid 0 NEAR, replay rejected. The first keyless x402 facilitator settlement on NEAR."
6604
+ }
5842
6605
  ]
5843
- // NEAR (near:mainnet) — DELIBERATELY UNSEEDED: no x402 facilitator settles NEAR yet.
5844
- // The NEAR `exact` BUYER payload PipRail builds (drivers/near/exact.ts) is LIVE-PROVEN on mainnet —
5845
- // a real NEP-366 meta-transaction settles a USDC/USDT ft_transfer gaslessly (buyer 0 NEAR, single-use
5846
- // via the access-key nonce; relay txs CMnQJzrLvwk… USDT + BCCnVHbSCMY… USDC, 2026-06-18). What's
5847
- // missing is the FACILITATOR side: the public x402-rs (which Ultravioleta DAO runs) has NO NEAR chain
5848
- // crate (only eip155/solana/aptos), and UVD's `/verify` 400s on a near:mainnet request even though its
5849
- // `/supported` ADVERTISES `near:mainnet` + feePayer `uvd-facilitator.near` — i.e. the listing is
5850
- // aspirational, not settle-capable (verified 2026-06-18). So `exact: true` must NOT auto-pick a NEAR
5851
- // facilitator. Seed here ONLY after a real keyless settle through a facilitator that actually
5852
- // implements scheme_exact_near.md (THE RULE). Merchants can still pass an explicit
5853
- // `exact: { settle: { facilitator } }` for any facilitator they've confirmed settles near:mainnet.
5854
6606
  };
5855
6607
  function knownFacilitatorsFor(network) {
5856
6608
  return KNOWN_FACILITATORS[network] ?? [];
@@ -5940,6 +6692,11 @@ function createPaymentGate(options) {
5940
6692
  net.assertValidPayTo(payTo);
5941
6693
  const { asset, decimals, symbol } = net.resolveToken(a.token);
5942
6694
  const amountBase = parseUnits(a.amount, decimals);
6695
+ if (amountBase <= 0n) {
6696
+ throw new InvalidConfigError(
6697
+ `requirePayment: amount must be greater than zero, got "${a.amount}" on ${net.network}. A gate that charges nothing gates nothing; omit the gate instead.`
6698
+ );
6699
+ }
5943
6700
  const spec = { net, asset, decimals, symbol, amountBase, amountFormatted: a.amount, payTo };
5944
6701
  if (exactOption) {
5945
6702
  const outcome = await resolveExactRail(net, asset);
@@ -6058,6 +6815,11 @@ function createPaymentGate(options) {
6058
6815
  "requirePayment/createPaymentGate: `isUsed` and `markUsed` must be provided TOGETHER \u2014 a custom replay store needs both a read and a write. Supplying only " + (hasIsUsed ? "`isUsed`" : "`markUsed`") + " silently disables replay protection (double-spend). Provide both, or neither (the built-in in-memory store)."
6059
6816
  );
6060
6817
  }
6818
+ if (typeof options.releaseUsed === "function" && !(hasIsUsed && hasMarkUsed)) {
6819
+ throw new Error(
6820
+ "requirePayment/createPaymentGate: `releaseUsed` needs `isUsed` + `markUsed` too \u2014 it releases a reservation a CUSTOM store made, so without one it would never fire. Provide all three, or none (the built-in store already releases on failure)."
6821
+ );
6822
+ }
6061
6823
  const hasCustomStore = hasIsUsed && hasMarkUsed;
6062
6824
  const localUsed = /* @__PURE__ */ new Map();
6063
6825
  const replayWindowMs = maxTimeoutSeconds * 1e3;
@@ -6067,23 +6829,35 @@ function createPaymentGate(options) {
6067
6829
  localUsed.delete(key);
6068
6830
  }
6069
6831
  }
6832
+ const localKey = (ref) => ref.startsWith("pid:") ? ref : ref.toLowerCase();
6070
6833
  async function claimTx(ref) {
6071
- if (hasCustomStore) {
6072
- return options.isUsed ? Boolean(await options.isUsed(ref)) : false;
6073
- }
6074
- const key = ref.startsWith("pid:") ? ref : ref.toLowerCase();
6834
+ const key = localKey(ref);
6075
6835
  const now = Date.now();
6076
6836
  pruneUsed(now);
6077
6837
  if (localUsed.has(key)) return true;
6078
6838
  localUsed.set(key, now + replayWindowMs);
6839
+ if (hasCustomStore) {
6840
+ try {
6841
+ if (options.isUsed && await options.isUsed(ref)) return true;
6842
+ } catch (err) {
6843
+ localUsed.delete(key);
6844
+ throw err;
6845
+ }
6846
+ }
6079
6847
  return false;
6080
6848
  }
6081
6849
  async function settleTx(ref, ok) {
6082
- if (hasCustomStore) {
6083
- if (ok && options.markUsed) await options.markUsed(ref);
6850
+ if (!ok) {
6851
+ localUsed.delete(localKey(ref));
6852
+ if (hasCustomStore && options.releaseUsed) {
6853
+ try {
6854
+ await options.releaseUsed(ref);
6855
+ } catch {
6856
+ }
6857
+ }
6084
6858
  return;
6085
6859
  }
6086
- if (!ok) localUsed.delete(ref.startsWith("pid:") ? ref : ref.toLowerCase());
6860
+ if (hasCustomStore && options.markUsed) await options.markUsed(ref);
6087
6861
  }
6088
6862
  function buildAccept(s, nonce) {
6089
6863
  return {
@@ -6225,520 +6999,1379 @@ function createPaymentGate(options) {
6225
6999
  amountFormatted = formatUnits(BigInt(receipt.amount), spec.decimals);
6226
7000
  } catch {
6227
7001
  }
6228
- return {
6229
- ...receipt,
6230
- decimals: spec.decimals,
6231
- ...spec.symbol ? { symbol: spec.symbol } : {},
6232
- amountFormatted,
6233
- idempotencyKey: receipt.transaction
6234
- };
6235
- }
6236
- function reportOnPaidError(error, receipt) {
6237
- if (!options.onPaidError) return;
6238
- try {
6239
- options.onPaidError(error, receipt);
6240
- } catch {
7002
+ return {
7003
+ ...receipt,
7004
+ decimals: spec.decimals,
7005
+ ...spec.symbol ? { symbol: spec.symbol } : {},
7006
+ amountFormatted,
7007
+ idempotencyKey: receipt.transaction
7008
+ };
7009
+ }
7010
+ function reportOnPaidError(error, receipt) {
7011
+ if (!options.onPaidError) return;
7012
+ try {
7013
+ options.onPaidError(error, receipt);
7014
+ } catch {
7015
+ }
7016
+ }
7017
+ function fireOnPaid(receipt) {
7018
+ if (!options.onPaid) return;
7019
+ let outcome;
7020
+ try {
7021
+ outcome = options.onPaid(receipt);
7022
+ } catch (err) {
7023
+ reportOnPaidError(err, receipt);
7024
+ return;
7025
+ }
7026
+ if (outcome != null && typeof outcome.then === "function") {
7027
+ return Promise.resolve(outcome).catch((err) => reportOnPaidError(err, receipt));
7028
+ }
7029
+ }
7030
+ async function deliverOnPaid(spec, receipt) {
7031
+ const paid = enrichReceipt(spec, receipt);
7032
+ if (options.awaitOnPaid) await fireOnPaid(paid);
7033
+ else void fireOnPaid(paid);
7034
+ }
7035
+ async function buildPaidResult(spec, receipt, nonce) {
7036
+ if (!receiptsOn) {
7037
+ return { kind: "paid", receipt, receiptHeader: buildReceiptHeader(receipt) };
7038
+ }
7039
+ try {
7040
+ const stamped = {
7041
+ ...receipt,
7042
+ ...nonce ? { nonce } : {},
7043
+ // §5.3: a suppressed tx is the empty string on the wire, never a missing key.
7044
+ ...receiptIncludeTxHash ? {} : { transaction: "" }
7045
+ };
7046
+ const attestation = await maybeSignAttestation(spec, stamped);
7047
+ const extensions = buildReceiptExtension({
7048
+ receipt: stamped,
7049
+ resource: { url: receiptResourceUrl },
7050
+ decimals: spec.decimals,
7051
+ ...attestation ? { attestation } : {}
7052
+ });
7053
+ return { kind: "paid", receipt: stamped, receiptHeader: buildReceiptHeader(stamped, extensions) };
7054
+ } catch {
7055
+ return { kind: "paid", receipt, receiptHeader: buildReceiptHeader(receipt) };
7056
+ }
7057
+ }
7058
+ async function maybeSignAttestation(spec, stamped) {
7059
+ if (attestJws) {
7060
+ warnAttestDegrade("JWS attestation is not yet implemented (R3)");
7061
+ return void 0;
7062
+ }
7063
+ if (attestWallet === void 0) return void 0;
7064
+ if (typeof spec.net.signReceipt !== "function") {
7065
+ warnAttestDegrade(`is EVM-only; the ${spec.net.family} rail can't sign an EIP-712 attestation`);
7066
+ return void 0;
7067
+ }
7068
+ try {
7069
+ const wallet = spec.net.bindWallet(attestWallet);
7070
+ return await spec.net.signReceipt(wallet, {
7071
+ payTo: spec.payTo,
7072
+ network: spec.net.network,
7073
+ resourceUrl: receiptResourceUrl,
7074
+ payer: stamped.payer,
7075
+ issuedAt: nowUnixSeconds(stamped.verifiedAt),
7076
+ // §5.3: the signed message carries the empty string for a suppressed tx, never omitted.
7077
+ transaction: receiptIncludeTxHash ? stamped.transaction : ""
7078
+ });
7079
+ } catch {
7080
+ warnAttestDegrade("signing failed");
7081
+ return void 0;
7082
+ }
7083
+ }
7084
+ function reportOnFailedError(error, failure) {
7085
+ if (!options.onFailedError) return;
7086
+ try {
7087
+ options.onFailedError(error, failure);
7088
+ } catch {
7089
+ }
7090
+ }
7091
+ function fireOnFailed(failure) {
7092
+ if (!options.onFailed) return;
7093
+ let outcome;
7094
+ try {
7095
+ outcome = options.onFailed(failure);
7096
+ } catch (err) {
7097
+ reportOnFailedError(err, failure);
7098
+ return;
7099
+ }
7100
+ if (outcome != null && typeof outcome.then === "function") {
7101
+ return Promise.resolve(outcome).catch((err) => reportOnFailedError(err, failure));
7102
+ }
7103
+ }
7104
+ async function deliverOnFailed(result) {
7105
+ const code = result.error;
7106
+ const failure = { code, detail: result.detail, transient: TRANSIENT_VERIFY_CODES.has(code) };
7107
+ if (options.awaitOnFailed) await fireOnFailed(failure);
7108
+ else void fireOnFailed(failure);
7109
+ }
7110
+ function railExtra(full) {
7111
+ const out = {};
7112
+ for (const k of ["assetTransferMethod", "facilitatorAddress", "name", "version"]) {
7113
+ const v = full[k];
7114
+ if (v !== void 0 && v !== "") out[k] = v;
7115
+ }
7116
+ return Object.keys(out).length > 0 ? { extra: out } : {};
7117
+ }
7118
+ async function describe(resourceUrl = "") {
7119
+ const specs = await ready();
7120
+ const accepts = [];
7121
+ for (const s of specs) {
7122
+ const base2 = {
7123
+ network: s.net.network,
7124
+ asset: s.asset,
7125
+ payTo: s.payTo,
7126
+ amount: s.amountBase.toString(),
7127
+ amountFormatted: s.amountFormatted,
7128
+ decimals: s.decimals,
7129
+ maxTimeoutSeconds,
7130
+ ...s.symbol ? { symbol: s.symbol } : {}
7131
+ };
7132
+ if (s.exact) accepts.push({ scheme: "exact", ...base2, ...railExtra(buildExactAccept(s).extra ?? {}) });
7133
+ if (s.upto) accepts.push({ scheme: "upto", ...base2, ...railExtra(buildUptoAccept(s).extra ?? {}) });
7134
+ accepts.push({ scheme: "onchain-proof", ...base2 });
7135
+ }
7136
+ return {
7137
+ url: resourceUrl,
7138
+ ...options.description ? { description: options.description } : {},
7139
+ ...options.mimeType ? { mimeType: options.mimeType } : {},
7140
+ accepts
7141
+ };
7142
+ }
7143
+ async function verifyOnchainProof(sig) {
7144
+ const specs = await ready();
7145
+ const spec = specs.find(
7146
+ (s) => normalizeNetwork(s.net.network) === normalizeNetwork(sig.accepted.network) && s.asset === sig.accepted.asset
7147
+ );
7148
+ if (!spec) {
7149
+ return rejection(
7150
+ "transfer_not_found",
7151
+ `Proof claims ${sig.accepted.asset} on ${sig.accepted.network}, which this resource doesn't accept (offered: ${specs.map((s) => `${s.asset}@${s.net.network}`).join(", ")}).`
7152
+ );
7153
+ }
7154
+ const ref = typeof sig.payload.txHash === "string" ? sig.payload.txHash.trim() : sig.payload.txHash;
7155
+ if (await claimTx(ref)) return rejection("tx_already_used", `Proof ${ref} was already redeemed.`);
7156
+ let result;
7157
+ try {
7158
+ result = await spec.net.verify(ref, buildAccept(spec, sig.payload.nonce));
7159
+ } catch (err) {
7160
+ await settleTx(ref, false);
7161
+ throw err;
7162
+ }
7163
+ if (!result.ok) {
7164
+ await settleTx(ref, false);
7165
+ return rejection(result.error, result.detail);
7166
+ }
7167
+ const verified = result.receipt.transaction;
7168
+ if (verified && verified !== ref) {
7169
+ if (await claimTx(verified)) {
7170
+ await settleTx(ref, false);
7171
+ return rejection("tx_already_used", `Payment ${verified} was already redeemed.`);
7172
+ }
7173
+ await settleTx(ref, false);
7174
+ await settleTx(verified, true);
7175
+ } else {
7176
+ await settleTx(ref, true);
7177
+ }
7178
+ await deliverOnPaid(spec, result.receipt);
7179
+ return await buildPaidResult(spec, result.receipt, sig.payload.nonce);
7180
+ }
7181
+ async function verifyExact(exact) {
7182
+ const specs = await ready();
7183
+ const exactSpecs = specs.filter((s) => s.exact);
7184
+ if (exactSpecs.length === 0) {
7185
+ return rejection("transfer_not_found", "This resource offers no standard `exact` rail.");
7186
+ }
7187
+ const isCaip = exact.network.includes(":");
7188
+ const wantExactNet = normalizeNetwork(exact.network);
7189
+ let candidates = isCaip ? exactSpecs.filter((s) => normalizeNetwork(s.net.network) === wantExactNet) : exactSpecs;
7190
+ if (exact.asset) {
7191
+ candidates = candidates.filter((s) => s.asset.toLowerCase() === exact.asset.toLowerCase());
7192
+ }
7193
+ let spec = candidates[0];
7194
+ if (!isCaip && !exact.asset && exactSpecs.length > 1) spec = void 0;
7195
+ if (!spec && !isCaip && !exact.asset && exactSpecs.length === 1) spec = exactSpecs[0];
7196
+ if (!spec || !spec.exact) {
7197
+ return rejection(
7198
+ "transfer_not_found",
7199
+ `No \`exact\` rail offered for ${exact.network}${exact.asset ? `/${exact.asset}` : ""} (offered: ${exactSpecs.map((s) => `${s.asset}@${s.net.network}`).join(", ")}).`
7200
+ );
7201
+ }
7202
+ let nonce;
7203
+ let evmAuth = null;
7204
+ if ("senderAuth" in exact.payload && "transaction" in exact.payload) {
7205
+ nonce = [exact.payload.transaction, exact.payload.senderAuth].map((t) => {
7206
+ try {
7207
+ return Buffer.from(t, "base64").toString("base64");
7208
+ } catch {
7209
+ return t;
7210
+ }
7211
+ }).join("|");
7212
+ } else if ("transaction" in exact.payload) {
7213
+ try {
7214
+ nonce = Buffer.from(exact.payload.transaction, "base64").toString("base64");
7215
+ } catch {
7216
+ nonce = exact.payload.transaction;
7217
+ }
7218
+ } else if ("paymentGroup" in exact.payload) {
7219
+ nonce = exact.payload.paymentGroup.map((t) => {
7220
+ try {
7221
+ return Buffer.from(t, "base64").toString("base64");
7222
+ } catch {
7223
+ return t;
7224
+ }
7225
+ }).join("|");
7226
+ } else if ("signedDelegateAction" in exact.payload) {
7227
+ try {
7228
+ nonce = Buffer.from(exact.payload.signedDelegateAction, "base64").toString("base64");
7229
+ } catch {
7230
+ nonce = exact.payload.signedDelegateAction;
7231
+ }
7232
+ } else if ("signedTxBlob" in exact.payload) {
7233
+ nonce = exact.payload.signedTxBlob.trim().toUpperCase();
7234
+ } else if ("permit2Authorization" in exact.payload) {
7235
+ evmAuth = exact.payload.permit2Authorization;
7236
+ nonce = evmAuth.nonce;
7237
+ } else {
7238
+ evmAuth = exact.payload.authorization;
7239
+ nonce = evmAuth.nonce;
7240
+ }
7241
+ if (await claimTx(nonce)) {
7242
+ return rejection("tx_already_used", `Authorization ${evmAuth ? `nonce ${nonce}` : "transaction"} was already redeemed.`);
6241
7243
  }
6242
- }
6243
- function fireOnPaid(receipt) {
6244
- if (!options.onPaid) return;
6245
- let outcome;
7244
+ const accept = buildExactAccept(spec);
7245
+ const mode = spec.exact.mode;
7246
+ let result;
6246
7247
  try {
6247
- outcome = options.onPaid(receipt);
7248
+ if (mode.kind === "self") {
7249
+ result = await spec.net.settleExactSelf({ relayer: mode.relayer, payload: exact.payload, accept });
7250
+ } else {
7251
+ const ftMethod = accept.extra?.assetTransferMethod;
7252
+ const needsFeePayer = ftMethod === "svm" || ftMethod === "algorand" || ftMethod === "aptos" || ftMethod === "near";
7253
+ if (needsFeePayer && !accept.extra?.feePayer) {
7254
+ throw new SettlementError(
7255
+ `exact settle: the ${ftMethod} facilitator rail is missing extra.feePayer (the gas sponsor) \u2014 cannot settle.`
7256
+ );
7257
+ }
7258
+ result = await settleViaFacilitator({
7259
+ url: mode.url,
7260
+ ...mode.authHeaders ? { authHeaders: mode.authHeaders } : {},
7261
+ // PipRail always builds a v2-shaped paymentRequirements (CAIP-2 network + `amount`),
7262
+ // so force x402Version:2 — echoing a v1 client's version here would hand the facilitator
7263
+ // a self-inconsistent request (v1 envelope, v2 requirements). The inner payload is
7264
+ // byte-identical across versions, so forwarding it verbatim is fine.
7265
+ x402Version: 2,
7266
+ paymentPayload: exact.raw,
7267
+ paymentRequirements: {
7268
+ scheme: "exact",
7269
+ network: accept.network,
7270
+ asset: accept.asset,
7271
+ amount: accept.amount,
7272
+ payTo: accept.payTo,
7273
+ maxTimeoutSeconds: accept.maxTimeoutSeconds,
7274
+ extra: needsFeePayer ? { feePayer: accept.extra.feePayer } : { name: accept.extra?.name ?? "", version: accept.extra?.version ?? "" }
7275
+ },
7276
+ receipt: { network: accept.network, asset: accept.asset, payTo: accept.payTo, amount: accept.amount },
7277
+ // From the merchant's own config, never the client's echo — same rule as `accept`.
7278
+ resource: {
7279
+ ...receiptResourceUrl ? { url: receiptResourceUrl } : {},
7280
+ ...options.description ? { description: options.description } : {},
7281
+ ...options.mimeType ? { mimeType: options.mimeType } : {}
7282
+ },
7283
+ // The buyer address, for the receipt's `payer` fallback. EVM carries it in the
7284
+ // authorization; SVM doesn't (the facilitator returns the settled payer) → omit it.
7285
+ ...evmAuth ? { payerHint: evmAuth.from } : {}
7286
+ });
7287
+ }
6248
7288
  } catch (err) {
6249
- reportOnPaidError(err, receipt);
6250
- return;
7289
+ await settleTx(nonce, false);
7290
+ throw err;
6251
7291
  }
6252
- if (outcome != null && typeof outcome.then === "function") {
6253
- return Promise.resolve(outcome).catch((err) => reportOnPaidError(err, receipt));
7292
+ if (!result.ok) {
7293
+ await settleTx(nonce, false);
7294
+ return rejection(result.error, result.detail);
6254
7295
  }
7296
+ await settleTx(nonce, true);
7297
+ await deliverOnPaid(spec, result.receipt);
7298
+ return await buildPaidResult(spec, result.receipt);
6255
7299
  }
6256
- async function deliverOnPaid(spec, receipt) {
6257
- const paid = enrichReceipt(spec, receipt);
6258
- if (options.awaitOnPaid) await fireOnPaid(paid);
6259
- else void fireOnPaid(paid);
6260
- }
6261
- async function buildPaidResult(spec, receipt, nonce) {
6262
- if (!receiptsOn) {
6263
- return { kind: "paid", receipt, receiptHeader: buildReceiptHeader(receipt) };
6264
- }
7300
+ function resolveSettleAmount(raw, maxAmount, decimals) {
7301
+ if (typeof raw === "bigint") return raw < 0n ? null : raw;
7302
+ const s = raw.trim();
7303
+ if (s.length === 0) return null;
6265
7304
  try {
6266
- const stamped = {
6267
- ...receipt,
6268
- ...nonce ? { nonce } : {},
6269
- // §5.3: a suppressed tx is the empty string on the wire, never a missing key.
6270
- ...receiptIncludeTxHash ? {} : { transaction: "" }
6271
- };
6272
- const attestation = await maybeSignAttestation(spec, stamped);
6273
- const extensions = buildReceiptExtension({
6274
- receipt: stamped,
6275
- resource: { url: receiptResourceUrl },
6276
- decimals: spec.decimals,
6277
- ...attestation ? { attestation } : {}
6278
- });
6279
- return { kind: "paid", receipt: stamped, receiptHeader: buildReceiptHeader(stamped, extensions) };
7305
+ if (s.endsWith("%")) {
7306
+ const pct = s.slice(0, -1).trim();
7307
+ if (!/^\d+(\.\d+)?$/.test(pct)) return null;
7308
+ const pctScaled = floorUnits(pct, 4);
7309
+ return maxAmount * pctScaled / (100n * 10n ** 4n);
7310
+ }
7311
+ if (s.startsWith("$")) {
7312
+ const amt = s.slice(1).trim();
7313
+ return floorUnits(amt, decimals);
7314
+ }
7315
+ if (/^\d+$/.test(s)) return BigInt(s);
7316
+ return floorUnits(s, decimals);
6280
7317
  } catch {
6281
- return { kind: "paid", receipt, receiptHeader: buildReceiptHeader(receipt) };
7318
+ return null;
6282
7319
  }
6283
7320
  }
6284
- async function maybeSignAttestation(spec, stamped) {
6285
- if (attestJws) {
6286
- warnAttestDegrade("JWS attestation is not yet implemented (R3)");
6287
- return void 0;
7321
+ async function verifyUpto(upto) {
7322
+ const specs = await ready();
7323
+ const uptoSpecs = specs.filter((s) => s.upto);
7324
+ if (uptoSpecs.length === 0) {
7325
+ return rejection("transfer_not_found", "This resource offers no standard `upto` (metered) rail.");
6288
7326
  }
6289
- if (attestWallet === void 0) return void 0;
6290
- if (typeof spec.net.signReceipt !== "function") {
6291
- warnAttestDegrade(`is EVM-only; the ${spec.net.family} rail can't sign an EIP-712 attestation`);
6292
- return void 0;
7327
+ const isCaip = upto.network.includes(":");
7328
+ const wantUptoNet = normalizeNetwork(upto.network);
7329
+ let candidates = isCaip ? uptoSpecs.filter((s) => normalizeNetwork(s.net.network) === wantUptoNet) : uptoSpecs;
7330
+ if (upto.asset) {
7331
+ candidates = candidates.filter((s) => s.asset.toLowerCase() === upto.asset.toLowerCase());
6293
7332
  }
6294
- try {
6295
- const wallet = spec.net.bindWallet(attestWallet);
6296
- return await spec.net.signReceipt(wallet, {
6297
- payTo: spec.payTo,
6298
- network: spec.net.network,
6299
- resourceUrl: receiptResourceUrl,
6300
- payer: stamped.payer,
6301
- issuedAt: nowUnixSeconds(stamped.verifiedAt),
6302
- // §5.3: the signed message carries the empty string for a suppressed tx, never omitted.
6303
- transaction: receiptIncludeTxHash ? stamped.transaction : ""
6304
- });
6305
- } catch {
6306
- warnAttestDegrade("signing failed");
6307
- return void 0;
7333
+ let spec = candidates[0];
7334
+ if (!isCaip && !upto.asset && uptoSpecs.length > 1) spec = void 0;
7335
+ if (!spec && !isCaip && !upto.asset && uptoSpecs.length === 1) spec = uptoSpecs[0];
7336
+ if (!spec || !spec.upto) {
7337
+ return rejection(
7338
+ "transfer_not_found",
7339
+ `No \`upto\` rail offered for ${upto.network}${upto.asset ? `/${upto.asset}` : ""} (offered: ${uptoSpecs.map((s) => `${s.asset}@${s.net.network}`).join(", ")}).`
7340
+ );
6308
7341
  }
6309
- }
6310
- function reportOnFailedError(error, failure) {
6311
- if (!options.onFailedError) return;
6312
- try {
6313
- options.onFailedError(error, failure);
6314
- } catch {
7342
+ const nonce = upto.payload.permit2Authorization.nonce;
7343
+ if (await claimTx(nonce)) {
7344
+ return rejection("tx_already_used", `Authorization nonce ${nonce} was already redeemed.`);
6315
7345
  }
6316
- }
6317
- function fireOnFailed(failure) {
6318
- if (!options.onFailed) return;
6319
- let outcome;
7346
+ const accept = buildUptoAccept(spec);
7347
+ const relayer = spec.upto.relayer;
7348
+ let result;
6320
7349
  try {
6321
- outcome = options.onFailed(failure);
7350
+ const rawAmount = await options.upto.settleAmount({
7351
+ maxAmount: spec.amountBase,
7352
+ asset: spec.asset,
7353
+ network: spec.net.network,
7354
+ decimals: spec.decimals,
7355
+ ...upto.raw ? { request: upto.raw } : {}
7356
+ });
7357
+ const settleAmount = resolveSettleAmount(rawAmount, spec.amountBase, spec.decimals);
7358
+ if (settleAmount === null) {
7359
+ await settleTx(nonce, false);
7360
+ return rejection(
7361
+ "upto_settle_exceeds_max",
7362
+ `The settleAmount callback returned an unparseable amount (${String(rawAmount)}). Return a bigint, a raw/"NN%"/"$X" string, or 0.`
7363
+ );
7364
+ }
7365
+ result = await spec.net.settleUptoSelf({ relayer, payload: upto.payload, accept, settleAmount });
6322
7366
  } catch (err) {
6323
- reportOnFailedError(err, failure);
6324
- return;
7367
+ await settleTx(nonce, false);
7368
+ if (err instanceof SettlementError) throw err;
7369
+ return rejection(
7370
+ "tx_reverted",
7371
+ `upto: metering/settle failed (${err instanceof Error ? err.message : String(err)}).`
7372
+ );
6325
7373
  }
6326
- if (outcome != null && typeof outcome.then === "function") {
6327
- return Promise.resolve(outcome).catch((err) => reportOnFailedError(err, failure));
7374
+ if (!result.ok) {
7375
+ await settleTx(nonce, false);
7376
+ return rejection(result.error, result.detail);
6328
7377
  }
7378
+ await settleTx(nonce, true);
7379
+ await deliverOnPaid(spec, result.receipt);
7380
+ return await buildPaidResult(spec, result.receipt);
6329
7381
  }
6330
- async function deliverOnFailed(result) {
6331
- const code = result.error;
6332
- const failure = { code, detail: result.detail, transient: TRANSIENT_VERIFY_CODES.has(code) };
6333
- if (options.awaitOnFailed) await fireOnFailed(failure);
6334
- else void fireOnFailed(failure);
7382
+ async function verify(paymentSignature) {
7383
+ const raw = normaliseHeader(paymentSignature);
7384
+ const result = await resolveVerdictObject(raw === void 0 ? void 0 : decodeBase64Json(raw));
7385
+ if (result.kind === "invalid") await deliverOnFailed(result);
7386
+ return result;
6335
7387
  }
6336
- function railExtra(full) {
6337
- const out = {};
6338
- for (const k of ["assetTransferMethod", "facilitatorAddress", "name", "version"]) {
6339
- const v = full[k];
6340
- if (v !== void 0 && v !== "") out[k] = v;
7388
+ async function verifyObject(payload) {
7389
+ const result = await resolveVerdictObject(payload);
7390
+ if (result.kind === "invalid") await deliverOnFailed(result);
7391
+ return result;
7392
+ }
7393
+ async function routeVerdictObject(obj) {
7394
+ if (obj === void 0 || obj === null) return asChallenge();
7395
+ const sig = parseSignatureObject(obj);
7396
+ if (sig && sig.accepted && typeof sig.accepted.network === "string" && typeof sig.accepted.asset === "string") {
7397
+ return verifyOnchainProof(sig);
6341
7398
  }
6342
- return Object.keys(out).length > 0 ? { extra: out } : {};
7399
+ const upto = parseUptoObject(obj);
7400
+ if (upto) return verifyUpto(upto);
7401
+ const exact = parseExactObject(obj);
7402
+ if (exact) return verifyExact(exact);
7403
+ return asChallenge();
6343
7404
  }
6344
- async function describe(resourceUrl = "") {
6345
- const specs = await ready();
6346
- const accepts = [];
6347
- for (const s of specs) {
6348
- const base2 = {
6349
- network: s.net.network,
6350
- asset: s.asset,
6351
- payTo: s.payTo,
6352
- amount: s.amountBase.toString(),
6353
- amountFormatted: s.amountFormatted,
6354
- decimals: s.decimals,
6355
- maxTimeoutSeconds,
6356
- ...s.symbol ? { symbol: s.symbol } : {}
7405
+ function echoPaymentIdentifier(result, id) {
7406
+ try {
7407
+ const decoded = decodeBase64Json(result.receiptHeader);
7408
+ if (!decoded) return result;
7409
+ const { extensions: existing, ...receiptOnly } = decoded;
7410
+ const merged = {
7411
+ ...existing ?? {},
7412
+ [EXT_PAYMENT_IDENTIFIER]: { info: { required: false, id } }
6357
7413
  };
6358
- if (s.exact) accepts.push({ scheme: "exact", ...base2, ...railExtra(buildExactAccept(s).extra ?? {}) });
6359
- if (s.upto) accepts.push({ scheme: "upto", ...base2, ...railExtra(buildUptoAccept(s).extra ?? {}) });
6360
- accepts.push({ scheme: "onchain-proof", ...base2 });
7414
+ return { ...result, receiptHeader: buildReceiptHeader(receiptOnly, merged) };
7415
+ } catch {
7416
+ return result;
6361
7417
  }
6362
- return {
6363
- url: resourceUrl,
6364
- ...options.description ? { description: options.description } : {},
6365
- ...options.mimeType ? { mimeType: options.mimeType } : {},
6366
- accepts
6367
- };
6368
7418
  }
6369
- async function verifyOnchainProof(sig) {
6370
- const specs = await ready();
6371
- const spec = specs.find(
6372
- (s) => normalizeNetwork(s.net.network) === normalizeNetwork(sig.accepted.network) && s.asset === sig.accepted.asset
6373
- );
6374
- if (!spec) {
7419
+ async function resolveVerdictObject(obj) {
7420
+ if (!options.paymentIdentifier) return routeVerdictObject(obj);
7421
+ const id = readPaymentIdentifier(obj);
7422
+ if (id !== null && typeof id === "object") {
7423
+ return rejection("signature_invalid", `payment-identifier: ${id.invalid}.`);
7424
+ }
7425
+ if (id === null) return routeVerdictObject(obj);
7426
+ const idKey = "pid:" + id;
7427
+ if (await claimTx(idKey)) {
6375
7428
  return rejection(
6376
- "transfer_not_found",
6377
- `Proof claims ${sig.accepted.asset} on ${sig.accepted.network}, which this resource doesn't accept (offered: ${specs.map((s) => `${s.asset}@${s.net.network}`).join(", ")}).`
7429
+ "tx_already_used",
7430
+ `Idempotency id "${id}" is already bound to a settled payment; use a fresh id for a new payment.`
6378
7431
  );
6379
7432
  }
6380
- const ref = typeof sig.payload.txHash === "string" ? sig.payload.txHash.trim() : sig.payload.txHash;
6381
- if (await claimTx(ref)) return rejection("tx_already_used", `Proof ${ref} was already redeemed.`);
6382
7433
  let result;
6383
7434
  try {
6384
- result = await spec.net.verify(ref, buildAccept(spec, sig.payload.nonce));
7435
+ result = await routeVerdictObject(obj);
6385
7436
  } catch (err) {
6386
- await settleTx(ref, false);
7437
+ await settleTx(idKey, false);
6387
7438
  throw err;
6388
7439
  }
6389
- if (!result.ok) {
6390
- await settleTx(ref, false);
6391
- return rejection(result.error, result.detail);
7440
+ await settleTx(idKey, result.kind === "paid");
7441
+ return result.kind === "paid" ? echoPaymentIdentifier(result, id) : result;
7442
+ }
7443
+ async function selfTest() {
7444
+ try {
7445
+ const specs = await ready();
7446
+ const warnings = [];
7447
+ const rails = specs.map((s) => {
7448
+ if (!s.symbol) {
7449
+ warnings.push(
7450
+ `${s.asset} on ${s.net.network}: custom token (no built-in symbol) \u2014 double-check the address + decimals.`
7451
+ );
7452
+ }
7453
+ return {
7454
+ network: s.net.network,
7455
+ asset: s.asset,
7456
+ ...s.symbol ? { symbol: s.symbol } : {},
7457
+ decimals: s.decimals,
7458
+ amount: s.amountFormatted,
7459
+ payTo: s.payTo,
7460
+ schemes: [
7461
+ ...s.exact ? ["exact"] : [],
7462
+ ...s.upto ? ["upto"] : [],
7463
+ "onchain-proof"
7464
+ ]
7465
+ };
7466
+ });
7467
+ return { ok: true, rails, warnings };
7468
+ } catch (err) {
7469
+ return { ok: false, rails: [], warnings: [], error: err instanceof Error ? err.message : String(err) };
6392
7470
  }
6393
- const verified = result.receipt.transaction;
6394
- if (verified && verified !== ref) {
6395
- if (await claimTx(verified)) {
6396
- await settleTx(ref, false);
6397
- return rejection("tx_already_used", `Payment ${verified} was already redeemed.`);
7471
+ }
7472
+ return { challenge, verify, verifyObject, describe, landingPage, selfTest };
7473
+ }
7474
+ function requirePayment(options) {
7475
+ if (options.upto) {
7476
+ throw new class extends PipRailError {
7477
+ code = "UNSUPPORTED_SCHEME";
7478
+ }(
7479
+ "requirePayment: the 'upto' (metered) rail is unsupported through the Express middleware \u2014 it settles before the route handler serves, so metered usage is unknown at settle time. Call gate.verify() directly (createPaymentGate) and meter inside settleAmount; see docs/accepting-payments/upto-rail-seller.md."
7480
+ );
7481
+ }
7482
+ const gate = createPaymentGate(options);
7483
+ return async (req, res, next) => {
7484
+ let result;
7485
+ try {
7486
+ result = await gate.verify(req.headers[HEADER_SIGNATURE] ?? req.headers[HEADER_SIGNATURE_V1]);
7487
+ } catch (err) {
7488
+ if (err instanceof SettlementError) {
7489
+ res.status(502);
7490
+ res.json({
7491
+ x402Version: 2,
7492
+ error: "settlement_failed",
7493
+ detail: err.message,
7494
+ fallback: "The gasless `exact` settlement failed. This resource also accepts the `onchain-proof` scheme \u2014 retry by paying that rail yourself (you broadcast the transfer and pay the gas). It is the fallback when no facilitator can sponsor the gas."
7495
+ });
7496
+ return;
7497
+ }
7498
+ next(err);
7499
+ return;
7500
+ }
7501
+ switch (result.kind) {
7502
+ case "paid":
7503
+ res.setHeader(HEADER_RESPONSE, result.receiptHeader);
7504
+ res.setHeader(HEADER_RESPONSE_V1, result.receiptHeader);
7505
+ return next();
7506
+ case "challenge":
7507
+ res.setHeader(HEADER_REQUIRED, result.requiredHeader);
7508
+ res.status(result.statusCode);
7509
+ res.json(result.challenge);
7510
+ return;
7511
+ case "invalid":
7512
+ res.setHeader(HEADER_REQUIRED, result.requiredHeader);
7513
+ res.status(result.statusCode);
7514
+ res.json(result.challenge);
7515
+ return;
7516
+ }
7517
+ };
7518
+ }
7519
+ function normaliseHeader(value) {
7520
+ if (Array.isArray(value)) return value[0];
7521
+ return value;
7522
+ }
7523
+
7524
+ // src/agent.ts
7525
+ var OPEN_OBJECT = { type: "object", additionalProperties: true };
7526
+ async function readBody(res) {
7527
+ const text = await res.text();
7528
+ if (!text) return null;
7529
+ try {
7530
+ return JSON.parse(text);
7531
+ } catch {
7532
+ return text;
7533
+ }
7534
+ }
7535
+ function canSwap(client) {
7536
+ return client.canAgentSwap?.() === true && typeof client.quoteSwap === "function";
7537
+ }
7538
+ function canSell(client) {
7539
+ return client.canAgentSell?.() === true && typeof client.address === "function";
7540
+ }
7541
+ function nonceIn(payload) {
7542
+ if (typeof payload !== "object" || payload === null) return void 0;
7543
+ const p = payload;
7544
+ const inner = p.payload;
7545
+ const fromPayload = inner && typeof inner.nonce === "string" ? inner.nonce : void 0;
7546
+ const accepted = p.accepted;
7547
+ const fromAccept = typeof accepted?.extra?.nonce === "string" ? accepted.extra.nonce : void 0;
7548
+ return fromPayload ?? fromAccept;
7549
+ }
7550
+ function refIn(payload) {
7551
+ if (typeof payload !== "object" || payload === null) return void 0;
7552
+ const inner = payload.payload;
7553
+ const ref = inner && typeof inner.txHash === "string" ? inner.txHash.trim() : void 0;
7554
+ return ref ? ref.toLowerCase() : void 0;
7555
+ }
7556
+ function toToolError(err) {
7557
+ if (!(err instanceof PipRailError)) throw err;
7558
+ const out = {
7559
+ ok: false,
7560
+ code: err.code,
7561
+ reason: err.message,
7562
+ explain: explainDecline(err)
7563
+ };
7564
+ if (err instanceof PaymentDeclinedError) {
7565
+ out.declined = true;
7566
+ if (err.reasonCode) out.reasonCode = err.reasonCode;
7567
+ }
7568
+ const ref = err.ref;
7569
+ if (typeof ref === "string") out.ref = ref;
7570
+ return out;
7571
+ }
7572
+ function paymentTools(client) {
7573
+ const base2 = [
7574
+ {
7575
+ name: "piprail_discover",
7576
+ description: `Find x402 payment-gated resources on the OPEN indexes (a phone book of payable APIs) WITHOUT paying. Use it to answer "what can I buy?" \u2014 search by topic, then quote/plan/pay a chosen one. By default returns only resources payable on your wallet's chain (network='self'); pass 'any' for every chain. Results are cross-scheme: ALWAYS call piprail_quote_payment on a chosen resource (it re-checks the live price) before piprail_pay_request.`,
7577
+ annotations: {
7578
+ title: "Discover payable x402 APIs",
7579
+ readOnlyHint: true,
7580
+ // reads the open indexes only; never pays
7581
+ openWorldHint: true
7582
+ // reaches external indexes (402 Index, CDP Bazaar)
7583
+ },
7584
+ parameters: {
7585
+ type: "object",
7586
+ properties: {
7587
+ query: {
7588
+ type: "string",
7589
+ description: 'Free-text topic to search for (optional). Multi-word queries are fanned out per word and results are ranked by relevance, so "crypto price feed" finds the best matches even when no single listing contains that exact phrase.'
7590
+ },
7591
+ network: {
7592
+ type: "string",
7593
+ description: "CAIP-2 id, 'self' (your chain \u2014 default), or 'any' (all chains)."
7594
+ },
7595
+ category: { type: "string", description: "Keep ONLY this category, e.g. 'ai', 'finance', 'data' (strict)." },
7596
+ asset: { type: "string", description: "Keep only resources paying in this token symbol, e.g. 'USDC'." },
7597
+ maxPrice: { type: "number", description: "Drop results advertised above this USD price." },
7598
+ minReliability: { type: "number", description: "Drop results below this health score (0\u2013100); unscored pass." },
7599
+ verified: { type: "boolean", description: "Prefer verified listings (402 Index)." },
7600
+ sort: {
7601
+ type: "string",
7602
+ enum: ["relevance", "reliability", "price", "uptime", "name"],
7603
+ description: "Ordering. Default 'relevance' with a query, else first-seen."
7604
+ },
7605
+ limit: { type: "number", description: "Max results to fetch per index (default 20)." }
7606
+ },
7607
+ additionalProperties: false
7608
+ },
7609
+ invoke: async (args) => {
7610
+ try {
7611
+ const opts = {};
7612
+ if (typeof args.query === "string") opts.query = args.query;
7613
+ if (typeof args.network === "string") opts.network = args.network;
7614
+ if (typeof args.category === "string") opts.category = args.category;
7615
+ if (typeof args.asset === "string") opts.asset = args.asset;
7616
+ if (typeof args.maxPrice === "number") opts.maxPrice = args.maxPrice;
7617
+ if (typeof args.minReliability === "number") opts.minReliability = args.minReliability;
7618
+ if (typeof args.verified === "boolean") opts.verified = args.verified;
7619
+ if (typeof args.sort === "string") opts.sort = args.sort;
7620
+ if (typeof args.limit === "number") opts.limit = args.limit;
7621
+ const found = await client.discover(opts);
7622
+ return {
7623
+ count: found.length,
7624
+ resources: found.map((r) => ({
7625
+ resource: r.resource,
7626
+ name: r.name,
7627
+ description: r.description,
7628
+ source: r.source,
7629
+ category: r.category,
7630
+ priceUsd: r.priceUsd,
7631
+ reliabilityScore: r.reliabilityScore,
7632
+ health: r.health,
7633
+ verified: r.verified,
7634
+ networks: [...new Set(r.rails.map((rail) => rail.network))]
7635
+ }))
7636
+ };
7637
+ } catch (err) {
7638
+ return toToolError(err);
7639
+ }
6398
7640
  }
6399
- await settleTx(ref, false);
6400
- await settleTx(verified, true);
6401
- } else {
6402
- await settleTx(ref, true);
6403
- }
6404
- await deliverOnPaid(spec, result.receipt);
6405
- return await buildPaidResult(spec, result.receipt, sig.payload.nonce);
6406
- }
6407
- async function verifyExact(exact) {
6408
- const specs = await ready();
6409
- const exactSpecs = specs.filter((s) => s.exact);
6410
- if (exactSpecs.length === 0) {
6411
- return rejection("transfer_not_found", "This resource offers no standard `exact` rail.");
6412
- }
6413
- const isCaip = exact.network.includes(":");
6414
- const wantExactNet = normalizeNetwork(exact.network);
6415
- let candidates = isCaip ? exactSpecs.filter((s) => normalizeNetwork(s.net.network) === wantExactNet) : exactSpecs;
6416
- if (exact.asset) {
6417
- candidates = candidates.filter((s) => s.asset.toLowerCase() === exact.asset.toLowerCase());
6418
- }
6419
- let spec = candidates[0];
6420
- if (!isCaip && !exact.asset && exactSpecs.length > 1) spec = void 0;
6421
- if (!spec && !isCaip && !exact.asset && exactSpecs.length === 1) spec = exactSpecs[0];
6422
- if (!spec || !spec.exact) {
6423
- return rejection(
6424
- "transfer_not_found",
6425
- `No \`exact\` rail offered for ${exact.network}${exact.asset ? `/${exact.asset}` : ""} (offered: ${exactSpecs.map((s) => `${s.asset}@${s.net.network}`).join(", ")}).`
6426
- );
6427
- }
6428
- let nonce;
6429
- let evmAuth = null;
6430
- if ("senderAuth" in exact.payload && "transaction" in exact.payload) {
6431
- nonce = [exact.payload.transaction, exact.payload.senderAuth].map((t) => {
7641
+ },
7642
+ {
7643
+ name: "piprail_quote_payment",
7644
+ description: "Get the price of an x402 payment-gated URL WITHOUT paying. Returns the amount, token, chain, recipient, and whether it is within the spend policy. Returns { gated: false } when the URL needs no payment. Call this first to decide whether a resource is worth buying.",
7645
+ annotations: {
7646
+ title: "Quote an x402 price",
7647
+ readOnlyHint: true,
7648
+ // reads the 402 challenge; never pays
7649
+ openWorldHint: true
7650
+ // fetches an arbitrary URL
7651
+ },
7652
+ parameters: {
7653
+ type: "object",
7654
+ properties: {
7655
+ url: { type: "string", description: "Full URL of the gated resource." }
7656
+ },
7657
+ required: ["url"],
7658
+ additionalProperties: false
7659
+ },
7660
+ outputSchema: OPEN_OBJECT,
7661
+ invoke: async (args) => {
6432
7662
  try {
6433
- return Buffer.from(t, "base64").toString("base64");
6434
- } catch {
6435
- return t;
7663
+ const quote = await client.quote(String(args.url));
7664
+ return quote ? { gated: true, ...quote } : { gated: false, url: String(args.url) };
7665
+ } catch (err) {
7666
+ return toToolError(err);
6436
7667
  }
6437
- }).join("|");
6438
- } else if ("transaction" in exact.payload) {
6439
- try {
6440
- nonce = Buffer.from(exact.payload.transaction, "base64").toString("base64");
6441
- } catch {
6442
- nonce = exact.payload.transaction;
6443
7668
  }
6444
- } else if ("paymentGroup" in exact.payload) {
6445
- nonce = exact.payload.paymentGroup.map((t) => {
7669
+ },
7670
+ {
7671
+ name: "piprail_plan_payment",
7672
+ description: "Check whether you CAN pay an x402-gated URL before paying. Reads your wallet balance, native gas, and whether the recipient can receive \u2014 across every rail the URL offers on your chain \u2014 and returns { gated, payable, best, options, fundingHint }. payable:false means do NOT attempt the payment; fundingHint says exactly what to top up. Call this before piprail_pay_request so you never commit to a payment you cannot finish. Returns { gated: false } when no payment is needed.",
7673
+ annotations: {
7674
+ title: "Plan an x402 payment",
7675
+ readOnlyHint: true,
7676
+ // reads balances + the challenge; never pays
7677
+ openWorldHint: true
7678
+ // fetches a URL and reads chain state
7679
+ },
7680
+ parameters: {
7681
+ type: "object",
7682
+ properties: {
7683
+ url: { type: "string", description: "Full URL of the gated resource." }
7684
+ },
7685
+ required: ["url"],
7686
+ additionalProperties: false
7687
+ },
7688
+ outputSchema: OPEN_OBJECT,
7689
+ invoke: async (args) => {
6446
7690
  try {
6447
- return Buffer.from(t, "base64").toString("base64");
6448
- } catch {
6449
- return t;
7691
+ const plan = await client.planPayment(String(args.url));
7692
+ if (plan == null) return { gated: false, url: String(args.url) };
7693
+ return {
7694
+ gated: true,
7695
+ payable: plan.payable,
7696
+ status: plan.status,
7697
+ fundingHint: plan.fundingHint,
7698
+ // One model-readable line distilling the whole plan.
7699
+ summary: summarizePlan(plan),
7700
+ best: plan.best ? {
7701
+ network: plan.best.accept.network,
7702
+ symbol: plan.best.quote.symbol,
7703
+ amount: plan.best.quote.amountFormatted,
7704
+ gasCoin: plan.best.cost.feeSymbol,
7705
+ gas: plan.best.cost.feeFormatted
7706
+ } : null,
7707
+ options: plan.options.map((o) => ({
7708
+ network: o.accept.network,
7709
+ symbol: o.quote.symbol,
7710
+ amount: o.quote.amountFormatted,
7711
+ state: o.state,
7712
+ blockers: o.blockers,
7713
+ warnings: o.warnings,
7714
+ recipientReady: o.recipient.ready
7715
+ })),
7716
+ // The session's time leash, present only when a time policy is configured.
7717
+ ...plan.session ? { session: plan.session } : {}
7718
+ };
7719
+ } catch (err) {
7720
+ return toToolError(err);
7721
+ }
7722
+ }
7723
+ },
7724
+ {
7725
+ name: "piprail_pay_request",
7726
+ description: "Fetch an x402 payment-gated URL, automatically making the required payment if needed (subject to the spend policy + approval hook). Pays whichever rail the client is configured for \u2014 PipRail's backendless on-chain rail, or, when enabled, the standard `exact` rail (where the buyer signs and the server settles, so no buyer gas). Returns the HTTP status, the response body, and a payment receipt if one settled. If the payment is refused by policy or the approval hook, returns { declined: true, reason } \u2014 no funds moved.",
7727
+ annotations: {
7728
+ title: "Pay an x402 request",
7729
+ readOnlyHint: false,
7730
+ // this is the one tool that MOVES FUNDS
7731
+ destructiveHint: true,
7732
+ // a payment is value-moving and not reversible
7733
+ idempotentHint: false,
7734
+ // paying twice = two payments
7735
+ openWorldHint: true
7736
+ // fetches a URL and settles a payment
7737
+ },
7738
+ parameters: {
7739
+ type: "object",
7740
+ properties: {
7741
+ url: { type: "string", description: "Full URL to fetch." },
7742
+ method: { type: "string", description: "HTTP method, default 'GET'." },
7743
+ body: {
7744
+ type: ["object", "string"],
7745
+ description: "Optional request body for POST/PUT (a JSON object or a string)."
7746
+ }
7747
+ },
7748
+ required: ["url"],
7749
+ additionalProperties: false
7750
+ },
7751
+ invoke: async (args) => {
7752
+ const url = String(args.url);
7753
+ const method = (args.method ? String(args.method) : "GET").toUpperCase();
7754
+ try {
7755
+ let res;
7756
+ if (method === "GET") {
7757
+ res = await client.get(url);
7758
+ } else {
7759
+ const headers = {};
7760
+ let body;
7761
+ if (args.body !== void 0 && args.body !== null) {
7762
+ if (typeof args.body === "string") {
7763
+ body = args.body;
7764
+ } else {
7765
+ body = JSON.stringify(args.body);
7766
+ headers["content-type"] = "application/json";
7767
+ }
7768
+ }
7769
+ res = await client.fetch(url, { method, headers, body });
7770
+ }
7771
+ const verifiable = parseReceiptExtension(res);
7772
+ return {
7773
+ status: res.status,
7774
+ ok: res.ok,
7775
+ body: await readBody(res),
7776
+ receipt: parseReceipt(res),
7777
+ ...verifiable ? { verifiableReceipt: { ...verifiable, resource: { url } } } : {}
7778
+ };
7779
+ } catch (err) {
7780
+ if (err instanceof PipRailError) {
7781
+ const out = {
7782
+ ok: false,
7783
+ code: err.code,
7784
+ reason: err.message,
7785
+ explain: explainDecline(err)
7786
+ };
7787
+ if (err instanceof PaymentDeclinedError) {
7788
+ out.declined = true;
7789
+ if (err.reasonCode) out.reasonCode = err.reasonCode;
7790
+ }
7791
+ const ref = err.ref;
7792
+ if (typeof ref === "string") out.ref = ref;
7793
+ return out;
7794
+ }
7795
+ throw err;
7796
+ }
7797
+ }
7798
+ },
7799
+ {
7800
+ name: "piprail_register",
7801
+ description: "List an x402 payment-gated resource YOU run on the open indexes so other agents can discover it. Default target is 402 Index \u2014 no auth, no signature, no payment; a self-registered listing is pending review (verify your domain on 402index.io for instant approval). Returns one outcome per index ({ source, ok, detail, visibility, note }); a step the chain can't satisfy comes back ok:false with the reason. Moves no funds; nothing is PipRail-hosted. NOTE: index/agent payers are overwhelmingly standard `exact` clients \u2014 a default onchain-proof-only gate gets listed but they cannot pay it, so add an `exact` rail (and set the gate's `discovery` option, required for x402scan) to be usefully discoverable AND payable.",
7802
+ annotations: {
7803
+ title: "Register an x402 endpoint",
7804
+ readOnlyHint: false,
7805
+ // writes a listing to an external index
7806
+ destructiveHint: false,
7807
+ // adds a listing; nothing is destroyed and no funds move
7808
+ openWorldHint: true
7809
+ // posts to external indexes (402 Index)
7810
+ // idempotentHint intentionally omitted — index dedup behaviour isn't guaranteed.
7811
+ },
7812
+ parameters: {
7813
+ type: "object",
7814
+ properties: {
7815
+ url: { type: "string", description: "Full URL of the resource to list." },
7816
+ name: { type: "string", description: "Display name (defaults to the host)." },
7817
+ description: {
7818
+ type: "string",
7819
+ description: "What the resource offers. Pack the words agents will search for INTO this text \u2014 index search is literal, so a keyword that isn't in the name/description won't be found."
7820
+ },
7821
+ category: { type: "string", description: "A category, e.g. 'ai', 'finance', 'data' \u2014 the top findability field (most listings have none)." },
7822
+ tags: { type: "array", items: { type: "string" }, description: "Keywords; folded into the description so they're searchable." },
7823
+ priceUsd: { type: "number", description: "Advertised price in USD (metadata)." },
7824
+ network: {
7825
+ type: "string",
7826
+ description: "Network slug to advertise, e.g. 'base' (defaults to the paying chain). Set it when registering from a multi-chain wallet so the listing names the right chain."
7827
+ },
7828
+ asset: { type: "string", description: "Payment asset symbol, e.g. 'USDC' (metadata)." },
7829
+ provider: { type: "string", description: "Who runs the resource (provider/org name)." },
7830
+ contactEmail: { type: "string", description: "Contact email for the listing." }
7831
+ },
7832
+ required: ["url"],
7833
+ additionalProperties: false
7834
+ },
7835
+ invoke: async (args) => {
7836
+ try {
7837
+ const opts = {};
7838
+ if (typeof args.name === "string") opts.name = args.name;
7839
+ if (typeof args.description === "string") opts.description = args.description;
7840
+ if (typeof args.category === "string") opts.category = args.category;
7841
+ if (Array.isArray(args.tags)) opts.tags = args.tags.filter((t) => typeof t === "string");
7842
+ if (typeof args.priceUsd === "number") opts.priceUsd = args.priceUsd;
7843
+ if (typeof args.network === "string") opts.network = args.network;
7844
+ if (typeof args.asset === "string") opts.asset = args.asset;
7845
+ if (typeof args.provider === "string") opts.provider = args.provider;
7846
+ if (typeof args.contactEmail === "string") opts.contactEmail = args.contactEmail;
7847
+ const outcomes = await client.register(String(args.url), opts);
7848
+ return { outcomes };
7849
+ } catch (err) {
7850
+ return toToolError(err);
6450
7851
  }
6451
- }).join("|");
6452
- } else if ("signedDelegateAction" in exact.payload) {
6453
- try {
6454
- nonce = Buffer.from(exact.payload.signedDelegateAction, "base64").toString("base64");
6455
- } catch {
6456
- nonce = exact.payload.signedDelegateAction;
6457
7852
  }
6458
- } else if ("signedTxBlob" in exact.payload) {
6459
- nonce = exact.payload.signedTxBlob.trim().toUpperCase();
6460
- } else if ("permit2Authorization" in exact.payload) {
6461
- evmAuth = exact.payload.permit2Authorization;
6462
- nonce = evmAuth.nonce;
6463
- } else {
6464
- evmAuth = exact.payload.authorization;
6465
- nonce = evmAuth.nonce;
6466
- }
6467
- if (await claimTx(nonce)) {
6468
- return rejection("tx_already_used", `Authorization ${evmAuth ? `nonce ${nonce}` : "transaction"} was already redeemed.`);
6469
- }
6470
- const accept = buildExactAccept(spec);
6471
- const mode = spec.exact.mode;
6472
- let result;
6473
- try {
6474
- if (mode.kind === "self") {
6475
- result = await spec.net.settleExactSelf({ relayer: mode.relayer, payload: exact.payload, accept });
6476
- } else {
6477
- const ftMethod = accept.extra?.assetTransferMethod;
6478
- const needsFeePayer = ftMethod === "svm" || ftMethod === "algorand" || ftMethod === "aptos" || ftMethod === "near";
6479
- if (needsFeePayer && !accept.extra?.feePayer) {
6480
- throw new SettlementError(
6481
- `exact settle: the ${ftMethod} facilitator rail is missing extra.feePayer (the gas sponsor) \u2014 cannot settle.`
6482
- );
7853
+ },
7854
+ {
7855
+ name: "piprail_budget",
7856
+ description: "Read how much of your spend budget and time leash is left \u2014 per (network, asset) remaining, the cross-token GRAND TOTAL per denomination (e.g. how much USD you can still spend across every stablecoin and chain), the payment-count leash, the session time envelope, and your spend so far. Use it in Mode A (headless) to self-check BEFORE paying, so you never discover the leash by hitting a decline. Read-only; moves no funds. NOTE: the time envelope is in-memory for THIS process; the money/count totals persist only if a spend store is configured.",
7857
+ annotations: {
7858
+ title: "Check remaining budget",
7859
+ readOnlyHint: true,
7860
+ // reads the ledger + policy; never pays
7861
+ idempotentHint: true
7862
+ // a pure read
7863
+ },
7864
+ parameters: { type: "object", properties: {}, additionalProperties: false },
7865
+ outputSchema: OPEN_OBJECT,
7866
+ invoke: async () => {
7867
+ try {
7868
+ const spent = client.spent();
7869
+ const budget = client.budget();
7870
+ return {
7871
+ spent,
7872
+ remaining: budget.byAsset,
7873
+ grandTotal: budget.byDenom,
7874
+ // cross-token spend cap per denomination (USD/EUR/…)
7875
+ counts: budget.counts,
7876
+ // payment-count leash (settled + lifetime/window caps)
7877
+ session: budget.session,
7878
+ policy: client.policy() ?? null,
7879
+ // the configured leash, read back
7880
+ report: formatSpendReport(spent)
7881
+ };
7882
+ } catch (err) {
7883
+ return toToolError(err);
6483
7884
  }
6484
- result = await settleViaFacilitator({
6485
- url: mode.url,
6486
- ...mode.authHeaders ? { authHeaders: mode.authHeaders } : {},
6487
- // PipRail always builds a v2-shaped paymentRequirements (CAIP-2 network + `amount`),
6488
- // so force x402Version:2 — echoing a v1 client's version here would hand the facilitator
6489
- // a self-inconsistent request (v1 envelope, v2 requirements). The inner payload is
6490
- // byte-identical across versions, so forwarding it verbatim is fine.
6491
- x402Version: 2,
6492
- paymentPayload: exact.raw,
6493
- paymentRequirements: {
6494
- scheme: "exact",
6495
- network: accept.network,
6496
- asset: accept.asset,
6497
- amount: accept.amount,
6498
- payTo: accept.payTo,
6499
- maxTimeoutSeconds: accept.maxTimeoutSeconds,
6500
- extra: needsFeePayer ? { feePayer: accept.extra.feePayer } : { name: accept.extra?.name ?? "", version: accept.extra?.version ?? "" }
6501
- },
6502
- receipt: { network: accept.network, asset: accept.asset, payTo: accept.payTo, amount: accept.amount },
6503
- // The buyer address, for the receipt's `payer` fallback. EVM carries it in the
6504
- // authorization; SVM doesn't (the facilitator returns the settled payer) → omit it.
6505
- ...evmAuth ? { payerHint: evmAuth.from } : {}
6506
- });
6507
- }
6508
- } catch (err) {
6509
- await settleTx(nonce, false);
6510
- throw err;
6511
- }
6512
- if (!result.ok) {
6513
- await settleTx(nonce, false);
6514
- return rejection(result.error, result.detail);
6515
- }
6516
- await settleTx(nonce, true);
6517
- await deliverOnPaid(spec, result.receipt);
6518
- return await buildPaidResult(spec, result.receipt);
6519
- }
6520
- function resolveSettleAmount(raw, maxAmount, decimals) {
6521
- if (typeof raw === "bigint") return raw < 0n ? null : raw;
6522
- const s = raw.trim();
6523
- if (s.length === 0) return null;
6524
- try {
6525
- if (s.endsWith("%")) {
6526
- const pct = s.slice(0, -1).trim();
6527
- if (!/^\d+(\.\d+)?$/.test(pct)) return null;
6528
- const pctScaled = floorUnits(pct, 4);
6529
- return maxAmount * pctScaled / (100n * 10n ** 4n);
6530
- }
6531
- if (s.startsWith("$")) {
6532
- const amt = s.slice(1).trim();
6533
- return floorUnits(amt, decimals);
6534
- }
6535
- if (/^\d+$/.test(s)) return BigInt(s);
6536
- return floorUnits(s, decimals);
6537
- } catch {
6538
- return null;
6539
- }
6540
- }
6541
- async function verifyUpto(upto) {
6542
- const specs = await ready();
6543
- const uptoSpecs = specs.filter((s) => s.upto);
6544
- if (uptoSpecs.length === 0) {
6545
- return rejection("transfer_not_found", "This resource offers no standard `upto` (metered) rail.");
6546
- }
6547
- const isCaip = upto.network.includes(":");
6548
- const wantUptoNet = normalizeNetwork(upto.network);
6549
- let candidates = isCaip ? uptoSpecs.filter((s) => normalizeNetwork(s.net.network) === wantUptoNet) : uptoSpecs;
6550
- if (upto.asset) {
6551
- candidates = candidates.filter((s) => s.asset.toLowerCase() === upto.asset.toLowerCase());
6552
- }
6553
- let spec = candidates[0];
6554
- if (!isCaip && !upto.asset && uptoSpecs.length > 1) spec = void 0;
6555
- if (!spec && !isCaip && !upto.asset && uptoSpecs.length === 1) spec = uptoSpecs[0];
6556
- if (!spec || !spec.upto) {
6557
- return rejection(
6558
- "transfer_not_found",
6559
- `No \`upto\` rail offered for ${upto.network}${upto.asset ? `/${upto.asset}` : ""} (offered: ${uptoSpecs.map((s) => `${s.asset}@${s.net.network}`).join(", ")}).`
6560
- );
6561
- }
6562
- const nonce = upto.payload.permit2Authorization.nonce;
6563
- if (await claimTx(nonce)) {
6564
- return rejection("tx_already_used", `Authorization nonce ${nonce} was already redeemed.`);
6565
- }
6566
- const accept = buildUptoAccept(spec);
6567
- const relayer = spec.upto.relayer;
6568
- let result;
6569
- try {
6570
- const rawAmount = await options.upto.settleAmount({
6571
- maxAmount: spec.amountBase,
6572
- asset: spec.asset,
6573
- network: spec.net.network,
6574
- decimals: spec.decimals,
6575
- ...upto.raw ? { request: upto.raw } : {}
6576
- });
6577
- const settleAmount = resolveSettleAmount(rawAmount, spec.amountBase, spec.decimals);
6578
- if (settleAmount === null) {
6579
- await settleTx(nonce, false);
6580
- return rejection(
6581
- "upto_settle_exceeds_max",
6582
- `The settleAmount callback returned an unparseable amount (${String(rawAmount)}). Return a bigint, a raw/"NN%"/"$X" string, or 0.`
6583
- );
6584
7885
  }
6585
- result = await spec.net.settleUptoSelf({ relayer, payload: upto.payload, accept, settleAmount });
6586
- } catch (err) {
6587
- await settleTx(nonce, false);
6588
- if (err instanceof SettlementError) throw err;
6589
- return rejection(
6590
- "tx_reverted",
6591
- `upto: metering/settle failed (${err instanceof Error ? err.message : String(err)}).`
6592
- );
6593
- }
6594
- if (!result.ok) {
6595
- await settleTx(nonce, false);
6596
- return rejection(result.error, result.detail);
6597
- }
6598
- await settleTx(nonce, true);
6599
- await deliverOnPaid(spec, result.receipt);
6600
- return await buildPaidResult(spec, result.receipt);
6601
- }
6602
- async function verify(paymentSignature) {
6603
- const raw = normaliseHeader(paymentSignature);
6604
- const result = await resolveVerdictObject(raw === void 0 ? void 0 : decodeBase64Json(raw));
6605
- if (result.kind === "invalid") await deliverOnFailed(result);
6606
- return result;
6607
- }
6608
- async function verifyObject(payload) {
6609
- const result = await resolveVerdictObject(payload);
6610
- if (result.kind === "invalid") await deliverOnFailed(result);
6611
- return result;
6612
- }
6613
- async function routeVerdictObject(obj) {
6614
- if (obj === void 0 || obj === null) return asChallenge();
6615
- const sig = parseSignatureObject(obj);
6616
- if (sig && sig.accepted && typeof sig.accepted.network === "string" && typeof sig.accepted.asset === "string") {
6617
- return verifyOnchainProof(sig);
6618
- }
6619
- const upto = parseUptoObject(obj);
6620
- if (upto) return verifyUpto(upto);
6621
- const exact = parseExactObject(obj);
6622
- if (exact) return verifyExact(exact);
6623
- return asChallenge();
6624
- }
6625
- function echoPaymentIdentifier(result, id) {
6626
- try {
6627
- const decoded = decodeBase64Json(result.receiptHeader);
6628
- if (!decoded) return result;
6629
- const { extensions: existing, ...receiptOnly } = decoded;
6630
- const merged = {
6631
- ...existing ?? {},
6632
- [EXT_PAYMENT_IDENTIFIER]: { info: { required: false, id } }
6633
- };
6634
- return { ...result, receiptHeader: buildReceiptHeader(receiptOnly, merged) };
6635
- } catch {
6636
- return result;
6637
- }
6638
- }
6639
- async function resolveVerdictObject(obj) {
6640
- if (!options.paymentIdentifier) return routeVerdictObject(obj);
6641
- const id = readPaymentIdentifier(obj);
6642
- if (id !== null && typeof id === "object") {
6643
- return rejection("signature_invalid", `payment-identifier: ${id.invalid}.`);
6644
- }
6645
- if (id === null) return routeVerdictObject(obj);
6646
- const idKey = "pid:" + id;
6647
- if (await claimTx(idKey)) {
6648
- return rejection(
6649
- "tx_already_used",
6650
- `Idempotency id "${id}" is already bound to a settled payment; use a fresh id for a new payment.`
6651
- );
6652
- }
6653
- let result;
6654
- try {
6655
- result = await routeVerdictObject(obj);
6656
- } catch (err) {
6657
- await settleTx(idKey, false);
6658
- throw err;
7886
+ },
7887
+ {
7888
+ name: "piprail_guide",
7889
+ description: "Read the PipRail agent contract \u2014 the quote \u2192 plan \u2192 pay loop, how to read a refusal (and which declines are TERMINAL), the never-re-pay rule for broadcast-but-unconfirmed payments, and Mode A (headless) vs Mode B (supervised). Read-only; call it once if unsure how to use these tools.",
7890
+ annotations: {
7891
+ title: "How to use PipRail",
7892
+ readOnlyHint: true,
7893
+ idempotentHint: true
7894
+ },
7895
+ parameters: { type: "object", properties: {}, additionalProperties: false },
7896
+ invoke: async () => ({ guide: PIPRAIL_AGENT_GUIDE })
7897
+ },
7898
+ {
7899
+ name: "piprail_verify_receipt",
7900
+ description: "Re-verify a PipRail VERIFIABLE RECEIPT against the chain \u2014 confirm a payment REALLY settled (the funds provably moved to payTo for AT LEAST the stated amount) WITHOUT trusting whoever handed you the receipt. Read-only and WALLET-FREE: pass the PipRailReceipt JSON (from a prior piprail_pay_request `verifiableReceipt`, or any third party). Returns { ok, onChain:{payTo,asset,amount,payer}, matchesClaims, ageSeconds, error? }: `ok` = the chain confirms the settlement; `onChain.payer` is RE-DERIVED from the tx and `matchesClaims:false` means the receipt forged the payer; `amount` is a verified lower bound. Pass `rpcUrl` for a chain outside the common presets.",
7901
+ annotations: {
7902
+ title: "Verify a payment receipt",
7903
+ readOnlyHint: true,
7904
+ // re-reads the chain; moves nothing, needs no wallet
7905
+ idempotentHint: true,
7906
+ openWorldHint: true
7907
+ // reads an on-chain tx via RPC
7908
+ },
7909
+ parameters: {
7910
+ type: "object",
7911
+ properties: {
7912
+ receipt: {
7913
+ type: "object",
7914
+ description: "The PipRailReceipt JSON ({ piprail, receipt, resource, decimals? }) to re-verify."
7915
+ },
7916
+ rpcUrl: {
7917
+ type: "string",
7918
+ description: "Optional RPC URL for the receipt's chain (required for chains outside the common presets)."
7919
+ }
7920
+ },
7921
+ required: ["receipt"],
7922
+ additionalProperties: false
7923
+ },
7924
+ invoke: async (args) => {
7925
+ const receipt = args.receipt;
7926
+ const opts = args.rpcUrl ? { rpcUrl: String(args.rpcUrl) } : void 0;
7927
+ return await PipRailClient.verifyReceipt(receipt, opts);
7928
+ }
6659
7929
  }
6660
- await settleTx(idKey, result.kind === "paid");
6661
- return result.kind === "paid" ? echoPaymentIdentifier(result, id) : result;
7930
+ ];
7931
+ const tools = [...base2];
7932
+ if (canSwap(client)) {
7933
+ tools.push(
7934
+ {
7935
+ name: "piprail_quote_swap",
7936
+ description: 'Price a SAME-CHAIN swap of one token into another WITHOUT swapping. Read-only: signs nothing and moves nothing. Use it when a payment is blocked because you hold the wrong token on the RIGHT chain. Returns { ok, from, to, maxSpend, source, slippageBps } where `maxSpend` is the ceiling enforced ON-CHAIN (not an estimate) and `source` NAMES who priced it, because PipRail runs no price oracle. `ok:false` with no quote means no route or no liquidity, never "no funds". This CANNOT cross chains: to pay on a chain you hold nothing on, use a wallet on that chain.',
7937
+ annotations: {
7938
+ title: "Price a token swap",
7939
+ readOnlyHint: true,
7940
+ idempotentHint: true,
7941
+ openWorldHint: true
7942
+ },
7943
+ parameters: {
7944
+ type: "object",
7945
+ properties: {
7946
+ from: { type: "string", description: "Token you HOLD and will spend (symbol, address, or 'native')." },
7947
+ to: { type: "string", description: "Token you NEED (symbol, address, or 'native')." },
7948
+ wantAmount: { type: "string", description: 'How much of `to` you need, in human units (e.g. "0.50").' },
7949
+ slippageBps: {
7950
+ type: "number",
7951
+ description: "Optional tolerance in basis points (default 50 = 0.5%). Refused if above your swapPolicy cap."
7952
+ }
7953
+ },
7954
+ required: ["from", "to", "wantAmount"],
7955
+ additionalProperties: false
7956
+ },
7957
+ invoke: async (args) => {
7958
+ const a = args;
7959
+ try {
7960
+ const quote = await client.quoteSwap?.({
7961
+ from: a.from,
7962
+ to: a.to,
7963
+ wantAmount: a.wantAmount,
7964
+ slippageBps: a.slippageBps
7965
+ });
7966
+ if (!quote) {
7967
+ return {
7968
+ ok: false,
7969
+ reason: "no_route",
7970
+ explain: "No route or no liquidity for that pair on this chain. This is not a funding problem. Try a different pair, or pay from a chain where you already hold the token."
7971
+ };
7972
+ }
7973
+ return {
7974
+ ok: true,
7975
+ from: { symbol: quote.from.symbol, amount: quote.from.amountFormatted },
7976
+ to: { symbol: quote.to.symbol, amount: quote.to.amountFormatted },
7977
+ maxSpend: quote.maxSpendFormatted,
7978
+ slippageBps: quote.slippageBps,
7979
+ source: { kind: quote.source.kind, name: quote.source.name, note: quote.source.note },
7980
+ summary: summarizeSwap(quote),
7981
+ quote
7982
+ };
7983
+ } catch (err) {
7984
+ return toToolError(err);
7985
+ }
7986
+ }
7987
+ },
7988
+ {
7989
+ name: "piprail_swap",
7990
+ description: "\u{1F534} MOVES FUNDS. Execute a swap you already priced with piprail_quote_swap. Pass that tool's `quote` back UNMODIFIED: it carries the route and the on-chain spend ceiling. Re-quote rather than reusing an old one, because a stale route gets you a worse price than you were shown. Your PAYMENT budget does not govern this call; your swapPolicy does. Nothing is swapped if the market moves past the ceiling, though chains that charge for a reverted transaction still take gas.",
7991
+ annotations: {
7992
+ title: "Swap tokens",
7993
+ readOnlyHint: false,
7994
+ destructiveHint: true,
7995
+ idempotentHint: false,
7996
+ openWorldHint: true
7997
+ },
7998
+ parameters: {
7999
+ type: "object",
8000
+ properties: {
8001
+ quote: { type: "object", description: "The `quote` object returned by piprail_quote_swap, unmodified." }
8002
+ },
8003
+ required: ["quote"],
8004
+ additionalProperties: false
8005
+ },
8006
+ invoke: async (args) => {
8007
+ const a = args;
8008
+ try {
8009
+ const receipt = await client.swap?.(a.quote);
8010
+ if (!receipt) return { ok: false, reason: "unsupported", explain: "This client cannot swap." };
8011
+ return {
8012
+ ok: true,
8013
+ transaction: receipt.transaction,
8014
+ network: receipt.network,
8015
+ from: { symbol: receipt.from.symbol, amount: receipt.from.amountFormatted },
8016
+ to: { symbol: receipt.to.symbol, amount: receipt.to.amountFormatted },
8017
+ source: receipt.source.name
8018
+ };
8019
+ } catch (err) {
8020
+ return toToolError(err);
8021
+ }
8022
+ }
8023
+ }
8024
+ );
6662
8025
  }
6663
- async function selfTest() {
6664
- try {
6665
- const specs = await ready();
6666
- const warnings = [];
6667
- const rails = specs.map((s) => {
6668
- if (!s.symbol) {
6669
- warnings.push(
6670
- `${s.asset} on ${s.net.network}: custom token (no built-in symbol) \u2014 double-check the address + decimals.`
6671
- );
8026
+ if (!canSell(client)) return tools;
8027
+ const offers = /* @__PURE__ */ new Map();
8028
+ const spentProofs = /* @__PURE__ */ new Set();
8029
+ const offerOr = (id) => {
8030
+ const found = typeof id === "string" ? offers.get(id) : void 0;
8031
+ return found ?? {
8032
+ ok: false,
8033
+ reason: `no offer ${JSON.stringify(id)}. Offers live in memory for this session only, so a restart clears them \u2014 call piprail_sell again to re-price it.`,
8034
+ offers: [...offers.keys()]
8035
+ };
8036
+ };
8037
+ tools.push(
8038
+ {
8039
+ name: "piprail_sell",
8040
+ description: "Put something up for sale and get the payment challenge to hand a buyer. YOU set the price. Returns an offerId plus a `challenge` object: give that challenge to the buyer over ANY channel (a reply, an A2A task, an MCP result) \u2014 it needs no web server and no open port. When they send back a payment proof, call piprail_collect BEFORE you deliver. Money goes straight to your own address; receiving needs no key, so nothing here can spend. Offers are in-memory for this session. By default the offer carries the standard `exact` rail as well as onchain-proof, so ordinary x402 agent-buyers can pay it \u2014 without `exact` most of them cannot, and a listing they cannot pay earns nothing.",
8041
+ annotations: {
8042
+ title: "Sell something",
8043
+ readOnlyHint: false,
8044
+ // creates an offer in this session and advertises an address to be paid at
8045
+ destructiveHint: false,
8046
+ // moves no funds; receiving cannot spend
8047
+ idempotentHint: false,
8048
+ // each call mints a NEW offer with its own nonce
8049
+ openWorldHint: false
8050
+ // publishes nowhere — piprail_register is what lists an offer publicly
8051
+ },
8052
+ parameters: {
8053
+ type: "object",
8054
+ properties: {
8055
+ description: {
8056
+ type: "string",
8057
+ description: "What the buyer is paying for. They see this in the challenge, and it is what you are promising to deliver \u2014 be specific."
8058
+ },
8059
+ price: { type: "string", description: "The price you are charging, human-readable, e.g. '2.50'." },
8060
+ token: { type: "string", description: "What to be paid in. Defaults to 'USDC'. Use 'native' for the chain's own coin." },
8061
+ chain: { type: "string", description: "Which chain to be paid on. Defaults to the chain this wallet is on." },
8062
+ payTo: {
8063
+ type: "string",
8064
+ description: "Where the money goes. Defaults to YOUR OWN address, which is almost always what you want \u2014 set it only to be paid somewhere else, and never to an address a buyer gave you."
8065
+ },
8066
+ resource: { type: "string", description: "Optional identifier or URL for the thing being sold; a label is minted if omitted." }
8067
+ },
8068
+ required: ["description", "price"],
8069
+ additionalProperties: false
8070
+ },
8071
+ invoke: async (args) => {
8072
+ try {
8073
+ const description = typeof args.description === "string" ? args.description.trim() : "";
8074
+ const price = typeof args.price === "string" ? args.price.trim() : "";
8075
+ if (!description) return { ok: false, reason: "sell needs a `description` \u2014 it is what the buyer sees and what you owe them." };
8076
+ if (!price) return { ok: false, reason: "sell needs a `price`, human-readable, e.g. '2.50'." };
8077
+ const token = typeof args.token === "string" && args.token.trim() ? args.token.trim() : "USDC";
8078
+ const chain = typeof args.chain === "string" && args.chain.trim() ? args.chain.trim() : client.chain?.();
8079
+ if (chain === void 0) {
8080
+ return { ok: false, reason: "sell needs a `chain` \u2014 this wallet cannot report one of its own." };
8081
+ }
8082
+ let payTo;
8083
+ if (typeof args.payTo === "string" && args.payTo.trim()) {
8084
+ payTo = args.payTo.trim();
8085
+ } else if (typeof client.address === "function") {
8086
+ payTo = await client.address();
8087
+ } else {
8088
+ return { ok: false, reason: "sell needs a `payTo` \u2014 this wallet cannot report its own address." };
8089
+ }
8090
+ const id = `offer_${crypto.randomUUID().replace(/-/g, "").slice(0, 10)}`;
8091
+ const resource = typeof args.resource === "string" && args.resource.trim() ? args.resource.trim() : `piprail:${id}`;
8092
+ const base3 = { chain, token, amount: price, payTo, description, discovery: true };
8093
+ const railSchemes = (t) => [
8094
+ ...new Set((t.rails ?? []).flatMap((r) => [...r.schemes ?? []]))
8095
+ ];
8096
+ let gate = createPaymentGate({ ...base3, exact: true });
8097
+ let check = await gate.selfTest();
8098
+ const warnings = [];
8099
+ if (!check.ok || !railSchemes(check).includes("exact")) {
8100
+ const why = check.error ?? "it did not resolve on this RPC";
8101
+ gate = createPaymentGate({ ...base3 });
8102
+ check = await gate.selfTest();
8103
+ warnings.push(
8104
+ `This offer carries onchain-proof ONLY, so a standard x402 agent-buyer cannot pay it. A PipRail buyer (piprail_pay_request) and a human still can. The gasless exact rail was dropped because ${why} Two things cause this: the token or family has no exact scheme (a native coin never does, so price in USDC), or the token domain read failed on a busy public RPC, which is transient. Retrying on a dedicated rpcUrl is worth one attempt before you settle for this.`
8105
+ );
8106
+ }
8107
+ if (!check.ok) {
8108
+ return { ok: false, reason: check.error ?? "this offer could not be priced on that chain.", chain, token };
8109
+ }
8110
+ for (const w of check.warnings ?? []) warnings.push(String(w));
8111
+ const { challenge, requiredHeader } = await gate.challenge(resource);
8112
+ const schemes = railSchemes(check);
8113
+ const nonce = challenge.accepts.find((a) => a.scheme === "onchain-proof")?.extra?.nonce;
8114
+ const offer = {
8115
+ id,
8116
+ nonce: typeof nonce === "string" ? nonce : void 0,
8117
+ gate,
8118
+ description,
8119
+ price,
8120
+ token,
8121
+ chain,
8122
+ payTo,
8123
+ resource,
8124
+ schemes,
8125
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
8126
+ earnings: []
8127
+ };
8128
+ offers.set(id, offer);
8129
+ return {
8130
+ ok: true,
8131
+ offerId: id,
8132
+ description,
8133
+ price,
8134
+ token,
8135
+ chain,
8136
+ payTo,
8137
+ paidToYou: payTo === (typeof client.address === "function" ? await client.address().catch(() => void 0) : void 0),
8138
+ schemes,
8139
+ challenge,
8140
+ /*
8141
+ * The header an x402 402 must carry alongside the body. An agent serving its own
8142
+ * offer over HTTP needs BOTH halves, and omitting this quietly left it emitting a
8143
+ * non-conformant 402 that only a lenient buyer would pay.
8144
+ */
8145
+ requiredHeader,
8146
+ next: `Give \`challenge\` to the buyer over any channel you like. Serving it over HTTP? Send it as a 402 body with \`requiredHeader\` as the PAYMENT-REQUIRED header. When they hand back a proof, call piprail_collect with offerId ${id} and DO NOT deliver until it comes back paid:true. To let strangers find this offer, call piprail_register with its URL.`,
8147
+ ...warnings.length ? { warnings } : {}
8148
+ };
8149
+ } catch (err) {
8150
+ return toToolError(err);
8151
+ }
8152
+ }
8153
+ },
8154
+ {
8155
+ name: "piprail_collect",
8156
+ description: "Verify a payment a buyer says they made for one of YOUR offers, against the chain. This is the only thing that proves you were paid \u2014 a buyer claiming to have paid, a plausible-looking tx hash, and a real settlement are three different things, and only this call tells them apart. Returns paid:true with a receipt, or paid:false with the reason and a fresh challenge the buyer can retry against. DELIVER NOTHING UNTIL IT RETURNS paid:true. Each proof can be redeemed once: a replay of an already-collected payment comes back paid:false, never a second sale.",
8157
+ annotations: {
8158
+ title: "Collect a payment",
8159
+ readOnlyHint: false,
8160
+ // burns the proof into the offer's used set, so it cannot be redeemed twice
8161
+ destructiveHint: false,
8162
+ // moves no funds; it only reads the chain and records what it found
8163
+ idempotentHint: false,
8164
+ // deliberately NOT: a second call on the same proof is a replay and is refused
8165
+ openWorldHint: true
8166
+ // reads the settlement via RPC
8167
+ },
8168
+ parameters: {
8169
+ type: "object",
8170
+ properties: {
8171
+ offerId: { type: "string", description: "The offer being paid for, from piprail_sell." },
8172
+ payment: {
8173
+ type: "string",
8174
+ description: "What the buyer sent back: the payment-signature header value, or the payment payload as a JSON string. Pass it through exactly as received."
8175
+ }
8176
+ },
8177
+ required: ["offerId", "payment"],
8178
+ additionalProperties: false
8179
+ },
8180
+ invoke: async (args) => {
8181
+ try {
8182
+ const offer = offerOr(args.offerId);
8183
+ if ("ok" in offer) return offer;
8184
+ const raw = typeof args.payment === "string" ? args.payment.trim() : "";
8185
+ if (!raw) return { ok: false, reason: "collect needs the `payment` the buyer sent back." };
8186
+ const asObject = raw.startsWith("{") ? JSON.parse(raw) : void 0;
8187
+ const presented = nonceIn(asObject ?? decodeBase64Json(raw));
8188
+ if (offer.nonce && presented && presented !== offer.nonce) {
8189
+ return {
8190
+ ok: true,
8191
+ paid: false,
8192
+ offerId: offer.id,
8193
+ reason: "this proof was minted for a DIFFERENT offer (its challenge nonce is not this one). A payment for one offer is not payment for another, even at the same price.",
8194
+ code: "wrong_offer",
8195
+ next: "Do NOT deliver. Ask the buyer to pay THIS offer's challenge."
8196
+ };
8197
+ }
8198
+ const proofRef = refIn(asObject ?? decodeBase64Json(raw));
8199
+ let reservedHere;
8200
+ if (proofRef) {
8201
+ if (spentProofs.has(proofRef)) {
8202
+ return {
8203
+ ok: true,
8204
+ paid: false,
8205
+ offerId: offer.id,
8206
+ reason: "this settlement was already collected \u2014 one payment settles exactly one offer.",
8207
+ code: "tx_already_used",
8208
+ next: "Do NOT deliver. Ask the buyer to pay THIS offer's challenge."
8209
+ };
8210
+ }
8211
+ spentProofs.add(proofRef);
8212
+ reservedHere = proofRef;
8213
+ }
8214
+ let result;
8215
+ try {
8216
+ result = asObject !== void 0 ? await offer.gate.verifyObject(asObject) : await offer.gate.verify(raw);
8217
+ } catch (err) {
8218
+ if (reservedHere) spentProofs.delete(reservedHere);
8219
+ throw err;
8220
+ }
8221
+ if (result.kind !== "paid" && reservedHere) spentProofs.delete(reservedHere);
8222
+ if (result.kind === "paid") {
8223
+ const r = result.receipt;
8224
+ const entry = {
8225
+ at: (/* @__PURE__ */ new Date()).toISOString(),
8226
+ amount: String(r.amountFormatted ?? offer.price),
8227
+ symbol: String(r.symbol ?? offer.token),
8228
+ ...typeof r.payer === "string" ? { payer: r.payer } : {},
8229
+ ref: String(r.transaction ?? r.reference ?? "")
8230
+ };
8231
+ offer.earnings.push(entry);
8232
+ return {
8233
+ ok: true,
8234
+ paid: true,
8235
+ offerId: offer.id,
8236
+ earned: `${entry.amount} ${entry.symbol}`,
8237
+ receipt: result.receipt,
8238
+ next: `Payment is settled and on-chain. Deliver "${offer.description}" now. This proof is spent and cannot be collected again.`
8239
+ };
8240
+ }
8241
+ if (result.kind === "invalid") {
8242
+ return {
8243
+ ok: true,
8244
+ paid: false,
8245
+ offerId: offer.id,
8246
+ reason: result.detail || result.error,
8247
+ code: result.error,
8248
+ retryChallenge: result.challenge,
8249
+ next: "Do NOT deliver. Give the buyer `retryChallenge` if they want to try again."
8250
+ };
8251
+ }
8252
+ return {
8253
+ ok: true,
8254
+ paid: false,
8255
+ offerId: offer.id,
8256
+ reason: "no payment was presented \u2014 this is a fresh challenge, not a settlement.",
8257
+ retryChallenge: result.challenge,
8258
+ next: "Do NOT deliver. The buyer has not paid yet."
8259
+ };
8260
+ } catch (err) {
8261
+ if (err instanceof SyntaxError) {
8262
+ return { ok: false, reason: "the `payment` looked like JSON but would not parse. Pass the buyer's proof through unchanged." };
8263
+ }
8264
+ return toToolError(err);
8265
+ }
8266
+ }
8267
+ },
8268
+ {
8269
+ name: "piprail_earnings",
8270
+ description: "What you have SOLD and what you have actually been paid this session \u2014 the earning-side mirror of piprail_budget. Counts only payments proven by piprail_collect, never what a buyer claimed. In-memory for this session, so it resets on restart and is a record of this run, not a ledger.",
8271
+ annotations: {
8272
+ title: "Your earnings",
8273
+ readOnlyHint: true,
8274
+ // pure read of this session's collected payments
8275
+ idempotentHint: true,
8276
+ openWorldHint: false
8277
+ },
8278
+ parameters: { type: "object", properties: {}, additionalProperties: false },
8279
+ invoke: async () => {
8280
+ const list = [...offers.values()];
8281
+ const byAsset = {};
8282
+ for (const o of list) {
8283
+ for (const e of o.earnings) byAsset[e.symbol] = (byAsset[e.symbol] ?? 0) + Number(e.amount || 0);
6672
8284
  }
8285
+ const collected = list.reduce((n, o) => n + o.earnings.length, 0);
6673
8286
  return {
6674
- network: s.net.network,
6675
- asset: s.asset,
6676
- ...s.symbol ? { symbol: s.symbol } : {},
6677
- decimals: s.decimals,
6678
- amount: s.amountFormatted,
6679
- payTo: s.payTo,
6680
- schemes: [
6681
- ...s.exact ? ["exact"] : [],
6682
- ...s.upto ? ["upto"] : [],
6683
- "onchain-proof"
6684
- ]
8287
+ ok: true,
8288
+ offers: list.map((o) => ({
8289
+ offerId: o.id,
8290
+ description: o.description,
8291
+ price: `${o.price} ${o.token}`,
8292
+ chain: o.chain,
8293
+ payTo: o.payTo,
8294
+ schemes: o.schemes,
8295
+ timesPaid: o.earnings.length,
8296
+ earnings: o.earnings
8297
+ })),
8298
+ totals: Object.fromEntries(Object.entries(byAsset).map(([s, n]) => [s, String(n)])),
8299
+ collected,
8300
+ report: list.length === 0 ? "Nothing on sale yet. piprail_sell prices something." : `${list.length} offer(s), ${collected} payment(s) collected: ` + (collected === 0 ? "nothing paid yet." : Object.entries(byAsset).map(([s, n]) => `${n} ${s}`).join(", "))
6685
8301
  };
6686
- });
6687
- return { ok: true, rails, warnings };
6688
- } catch (err) {
6689
- return { ok: false, rails: [], warnings: [], error: err instanceof Error ? err.message : String(err) };
8302
+ }
8303
+ },
8304
+ {
8305
+ name: "piprail_wallet",
8306
+ description: "What YOU hold, and where you get paid \u2014 your balance sheet, which is a different question from piprail_budget (that is how much of your allowance is left). Returns your own address per chain plus the amount of each asset you actually hold. Use it before deciding to sell, swap or ask to be topped up, and give the address to anyone who needs to send you funds. A null amount means the read was UNAVAILABLE, not zero: do NOT treat it as being broke. Read-only; moves nothing and needs no approval.",
8307
+ annotations: {
8308
+ title: "Your wallet",
8309
+ readOnlyHint: true,
8310
+ // reads addresses and balances; changes nothing
8311
+ idempotentHint: true,
8312
+ openWorldHint: true
8313
+ // balances come from RPC
8314
+ },
8315
+ parameters: {
8316
+ type: "object",
8317
+ properties: {
8318
+ assets: {
8319
+ type: "array",
8320
+ items: { type: "string" },
8321
+ description: "Symbols to report, e.g. ['native','USDC','USDT']. Defaults to your chain's coin and USDC. A symbol your chain does not ship comes back known:false rather than as a zero."
8322
+ }
8323
+ },
8324
+ additionalProperties: false
8325
+ },
8326
+ invoke: async (args) => {
8327
+ try {
8328
+ const assets = Array.isArray(args.assets) && args.assets.length ? args.assets.filter((a) => typeof a === "string") : ["native", "USDC"];
8329
+ const address = typeof client.address === "function" ? await client.address().catch(() => null) : null;
8330
+ const holdings = typeof client.balanceOf === "function" ? await client.balanceOf(assets).catch(() => []) : [];
8331
+ const held = holdings.filter((h) => h.known && h.amount !== null && h.amount !== "0");
8332
+ const unknown = holdings.filter((h) => h.known && h.amount === null);
8333
+ return {
8334
+ ok: true,
8335
+ address,
8336
+ chain: typeof client.chain === "function" ? client.chain() : void 0,
8337
+ holdings,
8338
+ report: (held.length ? `You hold ${held.map((h) => `${h.amountFormatted} ${h.symbol}`).join(", ")}.` : "No positive balance in the assets you asked about.") + (unknown.length ? ` Could NOT read ${unknown.map((h) => h.symbol).join(", ")} \u2014 unknown, not zero; retry before concluding you are broke.` : ""),
8339
+ next: "To be paid here, give out `address`. piprail_budget is your spend leash; this is what you own."
8340
+ };
8341
+ } catch (err) {
8342
+ return toToolError(err);
8343
+ }
8344
+ }
6690
8345
  }
6691
- }
6692
- return { challenge, verify, verifyObject, describe, landingPage, selfTest };
8346
+ );
8347
+ return tools;
6693
8348
  }
6694
- function requirePayment(options) {
6695
- if (options.upto) {
6696
- throw new class extends PipRailError {
6697
- code = "UNSUPPORTED_SCHEME";
6698
- }(
6699
- "requirePayment: the 'upto' (metered) rail is unsupported through the Express middleware \u2014 it settles before the route handler serves, so metered usage is unknown at settle time. Call gate.verify() directly (createPaymentGate) and meter inside settleAmount; see docs/accepting-payments/upto-rail-seller.md."
8349
+
8350
+ // src/classify.ts
8351
+ function classifyChallenge(challenge, opts) {
8352
+ try {
8353
+ const accepts = Array.isArray(challenge?.accepts) ? challenge.accepts : [];
8354
+ const network = opts?.network;
8355
+ const schemes = opts?.schemes ?? [];
8356
+ const offeredSchemes = [
8357
+ ...new Set(accepts.map((a) => a?.scheme).filter((s) => s != null))
8358
+ ];
8359
+ const offeredNetworks = [...new Set(accepts.map((a) => a?.network).filter((n) => n != null))];
8360
+ const onClientChain = accepts.some((a) => a?.network === network);
8361
+ const payableScheme = accepts.some(
8362
+ (a) => a?.network === network && schemes.includes(a?.scheme)
6700
8363
  );
8364
+ const verdict = accepts.length === 0 ? "NO_RAIL" : payableScheme ? "PAYABLE_RAIL" : onClientChain ? "UNPAYABLE_SCHEME" : "WRONG_CHAIN";
8365
+ return { onClientChain, payableScheme, offeredSchemes, offeredNetworks, verdict };
8366
+ } catch {
8367
+ return {
8368
+ onClientChain: false,
8369
+ payableScheme: false,
8370
+ offeredSchemes: [],
8371
+ offeredNetworks: [],
8372
+ verdict: "NO_RAIL"
8373
+ };
6701
8374
  }
6702
- const gate = createPaymentGate(options);
6703
- return async (req, res, next) => {
6704
- let result;
6705
- try {
6706
- result = await gate.verify(req.headers[HEADER_SIGNATURE] ?? req.headers[HEADER_SIGNATURE_V1]);
6707
- } catch (err) {
6708
- if (err instanceof SettlementError) {
6709
- res.status(502);
6710
- res.json({
6711
- x402Version: 2,
6712
- error: "settlement_failed",
6713
- detail: err.message,
6714
- fallback: "The gasless `exact` settlement failed. This resource also accepts the `onchain-proof` scheme \u2014 retry by paying that rail yourself (you broadcast the transfer and pay the gas). It is the fallback when no facilitator can sponsor the gas."
6715
- });
6716
- return;
6717
- }
6718
- next(err);
6719
- return;
6720
- }
6721
- switch (result.kind) {
6722
- case "paid":
6723
- res.setHeader(HEADER_RESPONSE, result.receiptHeader);
6724
- res.setHeader(HEADER_RESPONSE_V1, result.receiptHeader);
6725
- return next();
6726
- case "challenge":
6727
- res.setHeader(HEADER_REQUIRED, result.requiredHeader);
6728
- res.status(result.statusCode);
6729
- res.json(result.challenge);
6730
- return;
6731
- case "invalid":
6732
- res.setHeader(HEADER_REQUIRED, result.requiredHeader);
6733
- res.status(result.statusCode);
6734
- res.json(result.challenge);
6735
- return;
6736
- }
6737
- };
6738
- }
6739
- function normaliseHeader(value) {
6740
- if (Array.isArray(value)) return value[0];
6741
- return value;
6742
8375
  }
6743
8376
 
6744
8377
  // src/merchant.ts
@@ -7287,11 +8920,14 @@ export {
7287
8920
  A2A_STATUS_KEY,
7288
8921
  A2A_X402_EXTENSION_URI_V01,
7289
8922
  A2A_X402_EXTENSION_URI_V02,
8923
+ AGENT_MODES,
7290
8924
  BRAND,
7291
8925
  BUILTIN_DENOMS,
7292
8926
  CHAINS,
7293
8927
  ConfirmationTimeoutError,
8928
+ DEFAULT_AGENT_MODE,
7294
8929
  DEFAULT_EXACT_TRANSFER_METHOD,
8930
+ DEFAULT_SLIPPAGE_BPS,
7295
8931
  DENOM_PRECISION,
7296
8932
  DIRECTORY_INFO,
7297
8933
  EIP3009_TYPES,
@@ -7309,6 +8945,7 @@ export {
7309
8945
  InvalidEnvelopeError,
7310
8946
  KNOWN_EXACT_TRANSFER_METHODS,
7311
8947
  KNOWN_FACILITATORS,
8948
+ MAX_SLIPPAGE_BPS,
7312
8949
  MCP_PAYMENT_META_KEY,
7313
8950
  MCP_PAYMENT_RESPONSE_META_KEY,
7314
8951
  MaxRetriesExceededError,
@@ -7328,6 +8965,7 @@ export {
7328
8965
  PipRailError,
7329
8966
  REGISTER_ATTRIBUTION,
7330
8967
  RecipientNotReadyError,
8968
+ SWAP_PROVIDERS,
7331
8969
  SettlementError,
7332
8970
  SpendLedger,
7333
8971
  UPTO_PROXY_CHAIN_IDS,
@@ -7343,6 +8981,7 @@ export {
7343
8981
  agentGuide,
7344
8982
  appendAttribution,
7345
8983
  appendKeywords,
8984
+ applySlippage,
7346
8985
  buildBazaarExtension,
7347
8986
  buildChallengeHeader,
7348
8987
  buildEndpointInfo,
@@ -7360,6 +8999,7 @@ export {
7360
8999
  buildWellKnownX402,
7361
9000
  buildWellKnownX402Manifest,
7362
9001
  buildX402DnsTxt,
9002
+ canSwapOn,
7363
9003
  chainIdForExactNetwork,
7364
9004
  claim402IndexDomain,
7365
9005
  classifyChallenge,
@@ -7422,10 +9062,14 @@ export {
7422
9062
  renderLandingPage,
7423
9063
  requirePayment,
7424
9064
  resolveChain,
9065
+ resolveSlippageBps,
7425
9066
  scoreResource,
7426
9067
  searchOpenIndexes,
7427
9068
  settleViaFacilitator,
7428
9069
  summarizePlan,
9070
+ summarizeSwap,
9071
+ swapProvidersFor,
9072
+ swappableNetworks,
7429
9073
  toA2AErrorCode,
7430
9074
  toA2APaymentFailed,
7431
9075
  toA2APaymentReceipts,