@piprail/sdk 2.16.2 → 3.0.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 (33) hide show
  1. package/CHANGELOG.md +343 -0
  2. package/README.md +1 -0
  3. package/dist/{algorand-25FMBCT3.js → algorand-HL57PQHE.js} +157 -2
  4. package/dist/{algorand-Q3TUQLPK.cjs → algorand-W776EEM2.cjs} +205 -50
  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 +2929 -1444
  15. package/dist/index.d.cts +625 -13
  16. package/dist/index.d.ts +625 -13
  17. package/dist/index.js +2506 -1021
  18. package/dist/{near-OLKCMTBI.js → near-5X2CQGML.js} +197 -1
  19. package/dist/{near-5LTTDU6G.cjs → near-XSQXEIHT.cjs} +242 -46
  20. package/dist/{solana-AI2G7V33.cjs → solana-EBV6PUCU.cjs} +211 -52
  21. package/dist/{solana-HTKDRTD3.js → solana-O6Q6QILH.js} +167 -8
  22. package/dist/{stellar-E2KWEV2E.cjs → stellar-5C7FQLPS.cjs} +191 -29
  23. package/dist/{stellar-EUFZLX6J.js → stellar-YF5LOJEM.js} +163 -1
  24. package/dist/{sui-KCIITCYH.js → sui-JFQSDNSZ.js} +165 -3
  25. package/dist/{sui-Q3NJOJZS.cjs → sui-WIZEAO7E.cjs} +185 -23
  26. package/dist/{ton-J7TQWRN4.cjs → ton-AGTKLQA5.cjs} +264 -22
  27. package/dist/{ton-WH2JVQOO.js → ton-OZKFNRLT.js} +244 -2
  28. package/dist/{tron-FXBXDNEY.js → tron-HG3IOOCG.js} +283 -1
  29. package/dist/{tron-HIPMOX7S.cjs → tron-S4WX7OHY.cjs} +323 -41
  30. package/dist/{xrpl-RUOB37QH.js → xrpl-GOVHMYYK.js} +222 -5
  31. package/dist/{xrpl-SVVS445B.cjs → xrpl-YS3IXPLV.cjs} +273 -56
  32. package/package.json +12 -2
  33. 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) {
@@ -383,7 +419,7 @@ import {
383
419
  createWalletClient,
384
420
  http
385
421
  } from "viem";
386
- import { privateKeyToAccount } from "viem/accounts";
422
+ import { privateKeyToAccount, nonceManager } from "viem/accounts";
387
423
  function createWalletAdapter(config, resolved) {
388
424
  assertNoLegacyWalletKey(config, "EVM");
389
425
  if ("key" in config) {
@@ -396,7 +432,7 @@ function createWalletAdapter(config, resolved) {
396
432
  }
397
433
  let account;
398
434
  try {
399
- account = privateKeyToAccount(key);
435
+ account = privateKeyToAccount(key, { nonceManager });
400
436
  } catch (err) {
401
437
  throw new WrongFamilyError(
402
438
  `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 +482,242 @@ async function payEvm(input) {
446
482
  });
447
483
  }
448
484
 
485
+ // src/drivers/evm/swap.ts
486
+ var KYBER_API = "https://aggregator-api.kyberswap.com";
487
+ var KYBER_CHAIN = {
488
+ 1: "ethereum",
489
+ 10: "optimism",
490
+ 56: "bsc",
491
+ 137: "polygon",
492
+ 8453: "base",
493
+ 42161: "arbitrum",
494
+ 43114: "avalanche",
495
+ 59144: "linea",
496
+ 4663: "robinhood"
497
+ };
498
+ var NATIVE_SENTINEL = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";
499
+ var ERC20_ALLOWANCE_ABI = [
500
+ {
501
+ type: "function",
502
+ name: "allowance",
503
+ stateMutability: "view",
504
+ inputs: [
505
+ { name: "owner", type: "address" },
506
+ { name: "spender", type: "address" }
507
+ ],
508
+ outputs: [{ type: "uint256" }]
509
+ },
510
+ {
511
+ type: "function",
512
+ name: "approve",
513
+ stateMutability: "nonpayable",
514
+ inputs: [
515
+ { name: "spender", type: "address" },
516
+ { name: "amount", type: "uint256" }
517
+ ],
518
+ outputs: [{ type: "bool" }]
519
+ }
520
+ ];
521
+ function sourceFor(needsApproval, extraFee) {
522
+ const charged = !!(extraFee?.feeAmount && extraFee.feeAmount !== "0" && extraFee.feeReceiver);
523
+ return {
524
+ kind: "provider",
525
+ name: "KyberSwap",
526
+ 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.")
527
+ };
528
+ }
529
+ function addrFor(t) {
530
+ return t.asset === "native" ? NATIVE_SENTINEL : t.asset;
531
+ }
532
+ function side(t, amount, nativeSymbol) {
533
+ return {
534
+ asset: t.asset,
535
+ symbol: t.symbol ?? (t.asset === "native" ? nativeSymbol : t.asset),
536
+ decimals: t.decimals,
537
+ amount: amount.toString(),
538
+ amountFormatted: formatUnits(amount, t.decimals)
539
+ };
540
+ }
541
+ async function getJson(url, init) {
542
+ const ctrl = new AbortController();
543
+ const timer = setTimeout(() => ctrl.abort(), 12e3);
544
+ try {
545
+ const res = await fetch(url, { ...init ?? {}, signal: ctrl.signal });
546
+ if (!res.ok) return null;
547
+ return await res.json();
548
+ } catch {
549
+ return null;
550
+ } finally {
551
+ clearTimeout(timer);
552
+ }
553
+ }
554
+ async function quoteEvmSwap(p) {
555
+ const chainPath = KYBER_CHAIN[p.chainId];
556
+ if (!chainPath) return null;
557
+ if (p.wantAmount <= 0n) return null;
558
+ const tokenIn = addrFor(p.from);
559
+ const tokenOut = addrFor(p.to);
560
+ if (tokenIn.toLowerCase() === tokenOut.toLowerCase()) return null;
561
+ const route = async (amountIn) => getJson(
562
+ `${KYBER_API}/${chainPath}/api/v1/routes?tokenIn=${tokenIn}&tokenOut=${tokenOut}&amountIn=${amountIn.toString()}`
563
+ );
564
+ const probeIn = 10n ** BigInt(Math.max(p.from.decimals - 2, 1));
565
+ const probe = await route(probeIn);
566
+ const probeOut = probe?.data?.routeSummary?.amountOut;
567
+ if (!probeOut) return null;
568
+ let needIn;
569
+ try {
570
+ const out = BigInt(probeOut);
571
+ if (out <= 0n) return null;
572
+ needIn = (probeIn * p.wantAmount + out - 1n) / out;
573
+ needIn = applySlippage(needIn, p.slippageBps);
574
+ } catch {
575
+ return null;
576
+ }
577
+ const real = await route(needIn);
578
+ const summary = real?.data?.routeSummary;
579
+ if (!summary?.amountOut || !real?.data?.routerAddress) return null;
580
+ try {
581
+ if (BigInt(summary.amountOut) < p.wantAmount) return null;
582
+ } catch {
583
+ return null;
584
+ }
585
+ const needsApproval = p.from.asset !== "native";
586
+ const spend = needIn;
587
+ return {
588
+ source: sourceFor(needsApproval, summary.extraFee),
589
+ network: p.network,
590
+ from: side(p.from, spend, p.nativeSymbol),
591
+ // What we will actually receive: at least the invoice amount, usually a little more.
592
+ to: side(p.to, BigInt(summary.amountOut), p.nativeSymbol),
593
+ // Exact-in means the input IS the cap: it cannot spend more than this.
594
+ maxSpend: spend.toString(),
595
+ maxSpendFormatted: formatUnits(spend, p.from.decimals),
596
+ slippageBps: p.slippageBps,
597
+ route: {
598
+ chainPath,
599
+ routeSummary: summary,
600
+ routerAddress: real.data.routerAddress,
601
+ needsApproval,
602
+ tokenIn
603
+ }
604
+ };
605
+ }
606
+ async function swapEvm(p) {
607
+ const { publicClient, walletClient, account, chain, quote } = p;
608
+ const route = quote.route;
609
+ if (!route?.routerAddress || !route.routeSummary) {
610
+ throw new Error("EVM: swap quote is missing its routing data \u2014 re-quote before swapping.");
611
+ }
612
+ const router = route.routerAddress;
613
+ const spend = BigInt(quote.maxSpend);
614
+ if (route.needsApproval) {
615
+ const token = route.tokenIn;
616
+ let allowance = 0n;
617
+ try {
618
+ allowance = await publicClient.readContract({
619
+ address: token,
620
+ abi: ERC20_ALLOWANCE_ABI,
621
+ functionName: "allowance",
622
+ args: [account.address, router]
623
+ });
624
+ } catch {
625
+ allowance = 0n;
626
+ }
627
+ if (allowance < spend) {
628
+ if (allowance > 0n) {
629
+ const resetHash = await walletClient.writeContract({
630
+ address: token,
631
+ abi: ERC20_ALLOWANCE_ABI,
632
+ functionName: "approve",
633
+ args: [router, 0n],
634
+ account,
635
+ chain
636
+ });
637
+ await publicClient.waitForTransactionReceipt({ hash: resetHash });
638
+ }
639
+ const approveHash = await walletClient.writeContract({
640
+ address: token,
641
+ abi: ERC20_ALLOWANCE_ABI,
642
+ functionName: "approve",
643
+ args: [router, spend],
644
+ account,
645
+ chain
646
+ });
647
+ await publicClient.waitForTransactionReceipt({ hash: approveHash });
648
+ }
649
+ }
650
+ const built = await getJson(
651
+ `${KYBER_API}/${route.chainPath}/api/v1/route/build`,
652
+ {
653
+ method: "POST",
654
+ headers: { "content-type": "application/json" },
655
+ body: JSON.stringify({
656
+ routeSummary: route.routeSummary,
657
+ sender: account.address,
658
+ recipient: account.address,
659
+ // ← ourselves: the funds never leave the wallet
660
+ slippageTolerance: quote.slippageBps
661
+ })
662
+ }
663
+ );
664
+ if (!built?.data?.data) {
665
+ throw new InsufficientFundsError(
666
+ "KyberSwap could not build this swap (the route went stale, or the market moved). Nothing was spent \u2014 re-quote and retry."
667
+ );
668
+ }
669
+ const execRouter = built.data.routerAddress ?? router;
670
+ if (route.needsApproval && execRouter.toLowerCase() !== router.toLowerCase()) {
671
+ const token = route.tokenIn;
672
+ const approveHash = await walletClient.writeContract({
673
+ address: token,
674
+ abi: ERC20_ALLOWANCE_ABI,
675
+ functionName: "approve",
676
+ args: [execRouter, spend],
677
+ account,
678
+ chain
679
+ });
680
+ await publicClient.waitForTransactionReceipt({ hash: approveHash });
681
+ }
682
+ try {
683
+ const hash = await walletClient.sendTransaction({
684
+ account,
685
+ chain,
686
+ to: execRouter,
687
+ data: built.data.data,
688
+ value: route.tokenIn === NATIVE_SENTINEL ? spend : 0n
689
+ });
690
+ const receipt = await publicClient.waitForTransactionReceipt({ hash });
691
+ if (receipt.status === "reverted") {
692
+ throw new InsufficientFundsError(
693
+ `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.`
694
+ );
695
+ }
696
+ return {
697
+ transaction: hash,
698
+ network: quote.network,
699
+ source: quote.source,
700
+ from: quote.from,
701
+ to: quote.to
702
+ };
703
+ } catch (err) {
704
+ const msg = String(err?.message ?? err);
705
+ if (/TRANSFER_FROM_FAILED/i.test(msg)) {
706
+ throw new InsufficientFundsError(
707
+ "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.",
708
+ { cause: err }
709
+ );
710
+ }
711
+ if (/insufficient funds|exceeds balance/i.test(msg)) {
712
+ throw new InsufficientFundsError(
713
+ `EVM swap failed: the wallet can't cover it (token balance or native gas). (${msg.slice(0, 160)})`,
714
+ { cause: err }
715
+ );
716
+ }
717
+ throw err;
718
+ }
719
+ }
720
+
449
721
  // src/drivers/evm/verify.ts
450
722
  import {
451
723
  decodeEventLog,
@@ -1947,6 +2219,11 @@ function makeEvmNetwork(resolved) {
1947
2219
  });
1948
2220
  }
1949
2221
  },
2222
+ /** The bound wallet's own address — where THIS wallet gets paid. Derived from the key
2223
+ * material only: no RPC, nothing moved. See {@link ResolvedNetwork.addressOf}. */
2224
+ async addressOf(wallet) {
2225
+ return wallet._native.account.address;
2226
+ },
1950
2227
  async balanceOf(wallet, asset) {
1951
2228
  const owner = wallet._native.account.address;
1952
2229
  const native = await publicClient.getBalance({ address: owner }).catch(() => null);
@@ -1985,6 +2262,31 @@ function makeEvmNetwork(resolved) {
1985
2262
  signReceipt(wallet, input) {
1986
2263
  return signReceiptEvm(wallet, input);
1987
2264
  },
2265
+ /* ---- swap (OPTIONAL, opt-in): via KyberSwap, keyless + no integrator fee. See ./swap.ts ---- */
2266
+ async quoteSwap({ from, to, wantAmount, slippageBps, wallet }) {
2267
+ const a = wallet._native;
2268
+ return quoteEvmSwap({
2269
+ publicClient,
2270
+ chainId: resolved.chainId,
2271
+ network,
2272
+ nativeSymbol: resolved.chain.nativeCurrency?.symbol ?? "ETH",
2273
+ owner: a.account.address,
2274
+ from,
2275
+ to,
2276
+ wantAmount,
2277
+ slippageBps
2278
+ });
2279
+ },
2280
+ async swap(wallet, quote) {
2281
+ const a = wallet._native;
2282
+ return swapEvm({
2283
+ publicClient,
2284
+ walletClient: a.walletClient,
2285
+ account: a.account,
2286
+ chain: resolved.chain,
2287
+ quote
2288
+ });
2289
+ },
1988
2290
  async verify(ref, accept) {
1989
2291
  return verifyEvm({
1990
2292
  publicClient,
@@ -2138,7 +2440,7 @@ var loaders = {
2138
2440
  solana: async () => {
2139
2441
  let mod;
2140
2442
  try {
2141
- mod = await import("./solana-HTKDRTD3.js");
2443
+ mod = await import("./solana-O6Q6QILH.js");
2142
2444
  } catch (cause) {
2143
2445
  throw new MissingDriverError(
2144
2446
  `Solana selected, but its packages aren't installed. Run: npm install @solana/web3.js @solana/spl-token bs58`,
@@ -2150,7 +2452,7 @@ var loaders = {
2150
2452
  ton: async () => {
2151
2453
  let mod;
2152
2454
  try {
2153
- mod = await import("./ton-WH2JVQOO.js");
2455
+ mod = await import("./ton-OZKFNRLT.js");
2154
2456
  } catch (cause) {
2155
2457
  throw new MissingDriverError(
2156
2458
  `TON selected, but its packages aren't installed. Run: npm install @ton/ton @ton/core @ton/crypto`,
@@ -2162,7 +2464,7 @@ var loaders = {
2162
2464
  stellar: async () => {
2163
2465
  let mod;
2164
2466
  try {
2165
- mod = await import("./stellar-EUFZLX6J.js");
2467
+ mod = await import("./stellar-YF5LOJEM.js");
2166
2468
  } catch (cause) {
2167
2469
  throw new MissingDriverError(
2168
2470
  `Stellar selected, but its package isn't installed. Run: npm install @stellar/stellar-sdk`,
@@ -2174,7 +2476,7 @@ var loaders = {
2174
2476
  xrpl: async () => {
2175
2477
  let mod;
2176
2478
  try {
2177
- mod = await import("./xrpl-RUOB37QH.js");
2479
+ mod = await import("./xrpl-GOVHMYYK.js");
2178
2480
  } catch (cause) {
2179
2481
  throw new MissingDriverError(
2180
2482
  `XRPL selected, but its package isn't installed. Run: npm install xrpl`,
@@ -2186,7 +2488,7 @@ var loaders = {
2186
2488
  tron: async () => {
2187
2489
  let mod;
2188
2490
  try {
2189
- mod = await import("./tron-FXBXDNEY.js");
2491
+ mod = await import("./tron-HG3IOOCG.js");
2190
2492
  } catch (cause) {
2191
2493
  throw new MissingDriverError(
2192
2494
  `Tron selected, but its package isn't installed. Run: npm install tronweb`,
@@ -2198,7 +2500,7 @@ var loaders = {
2198
2500
  sui: async () => {
2199
2501
  let mod;
2200
2502
  try {
2201
- mod = await import("./sui-KCIITCYH.js");
2503
+ mod = await import("./sui-JFQSDNSZ.js");
2202
2504
  } catch (cause) {
2203
2505
  throw new MissingDriverError(
2204
2506
  `Sui selected, but its package isn't installed. Run: npm install @mysten/sui`,
@@ -2210,7 +2512,7 @@ var loaders = {
2210
2512
  near: async () => {
2211
2513
  let mod;
2212
2514
  try {
2213
- mod = await import("./near-OLKCMTBI.js");
2515
+ mod = await import("./near-5X2CQGML.js");
2214
2516
  } catch (cause) {
2215
2517
  throw new MissingDriverError(
2216
2518
  `NEAR selected, but its package isn't installed. Run: npm install near-api-js`,
@@ -2222,7 +2524,7 @@ var loaders = {
2222
2524
  aptos: async () => {
2223
2525
  let mod;
2224
2526
  try {
2225
- mod = await import("./aptos-SBV6SGDP.js");
2527
+ mod = await import("./aptos-H5VLH2QL.js");
2226
2528
  } catch (cause) {
2227
2529
  throw new MissingDriverError(
2228
2530
  `Aptos selected, but its package isn't installed. Run: npm install @aptos-labs/ts-sdk`,
@@ -2234,7 +2536,7 @@ var loaders = {
2234
2536
  algorand: async () => {
2235
2537
  let mod;
2236
2538
  try {
2237
- mod = await import("./algorand-25FMBCT3.js");
2539
+ mod = await import("./algorand-HL57PQHE.js");
2238
2540
  } catch (cause) {
2239
2541
  throw new MissingDriverError(
2240
2542
  `Algorand selected, but its package isn't installed. Run: npm install algosdk`,
@@ -2273,6 +2575,7 @@ var BUILTIN_DENOMS = {
2273
2575
  USDT: "USD",
2274
2576
  USD1: "USD",
2275
2577
  FDUSD: "USD",
2578
+ USDG: "USD",
2276
2579
  U: "USD",
2277
2580
  // United Stables — BNB EIP-3009 stablecoin (1:1 USDC/USDT/USD1-backed)
2278
2581
  RLUSD: "USD",
@@ -2645,6 +2948,299 @@ var SpendLedger = class {
2645
2948
  }
2646
2949
  };
2647
2950
 
2951
+ // src/swapProviders.ts
2952
+ var SWAP_PROVIDERS = [
2953
+ {
2954
+ id: "stellar-sdex",
2955
+ name: "Stellar SDEX",
2956
+ kind: "protocol",
2957
+ url: "https://developers.stellar.org/docs/build/guides/transactions/path-payments",
2958
+ networks: ["stellar:pubnet"],
2959
+ keyless: true,
2960
+ 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.",
2961
+ mechanism: "A PathPaymentStrictReceive addressed to your own account: one atomic operation, routed across the order books and liquidity pools.",
2962
+ proofs: [
2963
+ {
2964
+ network: "stellar:pubnet",
2965
+ tx: "f7b784d4758ea83a022921c7202051b4643fefb4fa61530a49010a4a9348a0aa",
2966
+ date: "2026-09-08",
2967
+ summary: "0.2652 XLM \u2192 0.05 USDC",
2968
+ covers: "native in, and the ledger records source and destination as the same account"
2969
+ },
2970
+ {
2971
+ network: "stellar:pubnet",
2972
+ tx: "3aa2ca9e35ff7d7c3d0aaff3b24d53b4d2ea009067debf058c30ed8cad445f03",
2973
+ date: "2026-09-08",
2974
+ summary: "USDC \u2192 0.2 XLM",
2975
+ covers: "the reverse direction, token in"
2976
+ },
2977
+ {
2978
+ network: "stellar:pubnet",
2979
+ tx: "27f50b2c2c57a64a8b50c1b14f310ec99b4ccbbb4bc72c479a5f6f9f1180e385",
2980
+ date: "2026-09-08",
2981
+ summary: "0.1064 XLM \u2192 0.02 USDC",
2982
+ covers: "run straight from the committed example, so the documented flow is proven too"
2983
+ }
2984
+ ]
2985
+ },
2986
+ {
2987
+ id: "xrpl-dex",
2988
+ name: "XRPL DEX + AMM",
2989
+ kind: "protocol",
2990
+ url: "https://xrpl.org/docs/concepts/tokens/decentralized-exchange/automated-market-makers",
2991
+ networks: ["xrpl:0"],
2992
+ keyless: true,
2993
+ 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.",
2994
+ 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.",
2995
+ proofs: [
2996
+ {
2997
+ network: "xrpl:0",
2998
+ tx: "F703B271E35BD3C2408DB64F693461513DD5166C15EC61DBAB0B2A9F726ED97E",
2999
+ date: "2026-09-08",
3000
+ summary: "0.072 XRP \u2192 0.1 RLUSD",
3001
+ covers: "native in; delivered_amount matched the requested amount exactly"
3002
+ },
3003
+ {
3004
+ network: "xrpl:0",
3005
+ tx: "24EC9DDD2B5D00FD7CD56882999FCF59ABC274AA32E35119EADE397B335EEB63",
3006
+ date: "2026-09-08",
3007
+ summary: "RLUSD \u2192 0.02 XRP",
3008
+ covers: "the reverse direction, token in"
3009
+ }
3010
+ ]
3011
+ },
3012
+ {
3013
+ id: "jupiter",
3014
+ name: "Jupiter",
3015
+ kind: "provider",
3016
+ url: "https://jup.ag",
3017
+ networks: ["solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp"],
3018
+ keyless: true,
3019
+ 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.",
3020
+ 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.",
3021
+ proofs: [
3022
+ {
3023
+ network: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
3024
+ tx: "2rXpQYVerRzrwUfmXMYcm6ijkm8x7LHU91onSZjrng34kr1nfcxZj1A8Ek7inSw7ZBzk3phAaYnvHwdSJTKxJ4rq",
3025
+ date: "2026-09-08",
3026
+ summary: "0.1031 USDC \u2192 0.001 SOL",
3027
+ covers: "token in, exact output"
3028
+ },
3029
+ {
3030
+ network: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
3031
+ tx: "UEeXBp3sPZgEHmahMasAbFhbTootXGCcpoNh7bN1sWsjoPDc1QE7uGaHjivT3iTroxLVJeWGVp2jZT63d2BVTk5",
3032
+ date: "2026-09-08",
3033
+ summary: "0.000485 SOL \u2192 0.05 USDC",
3034
+ covers: "the reverse direction, native in"
3035
+ }
3036
+ ]
3037
+ },
3038
+ {
3039
+ id: "kyberswap",
3040
+ name: "KyberSwap",
3041
+ kind: "provider",
3042
+ url: "https://kyberswap.com",
3043
+ // Every one live-probed with a real stablecoin route before being listed. Celo and
3044
+ // Scroll are deliberately ABSENT: the API answers but returns no route even for the
3045
+ // most liquid pair, so listing them would advertise a swap that cannot execute.
3046
+ networks: [
3047
+ "eip155:1",
3048
+ "eip155:10",
3049
+ "eip155:56",
3050
+ "eip155:137",
3051
+ "eip155:8453",
3052
+ "eip155:42161",
3053
+ "eip155:43114",
3054
+ "eip155:59144",
3055
+ "eip155:4663"
3056
+ ],
3057
+ keyless: true,
3058
+ 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.",
3059
+ 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.",
3060
+ proofs: [
3061
+ {
3062
+ network: "eip155:56",
3063
+ tx: "0x1748b6b27b1920aafd1d2730d250378b08cd4c0851a29e5e785dea960e0c8d4d",
3064
+ date: "2026-09-08",
3065
+ summary: "0.0101 USDT \u2192 USDC on BNB Chain",
3066
+ covers: "the ERC-20 path, which needs an approval first"
3067
+ },
3068
+ {
3069
+ network: "eip155:4663",
3070
+ tx: "0x424299e67be623313c679e659fbfe9fb8707800fe145cddf7fac5a9bdbb78f10",
3071
+ date: "2026-09-08",
3072
+ summary: "native ETH \u2192 0.360518 USDG on Robinhood Chain",
3073
+ covers: "a 100ms-block chain, where a route goes stale fast: the default 50 bps slippage reverted first, 300 bps cleared"
3074
+ },
3075
+ {
3076
+ network: "eip155:56",
3077
+ tx: "0xe27f957b40285acf0b846aea23808d49cc6a44fccd755aa88656abf31204fb00",
3078
+ date: "2026-09-08",
3079
+ summary: "native BNB \u2192 0.02 USDC",
3080
+ covers: "the native-in path, which needs no approval"
3081
+ },
3082
+ {
3083
+ network: "eip155:8453",
3084
+ tx: "0xb28b802f530945eba1ea1662890792bfa1dfba64f5094caa58297f9693604032",
3085
+ date: "2026-09-08",
3086
+ summary: "0.00117 USDC \u2192 EURC on Base",
3087
+ covers: "a second EVM chain on the same implementation"
3088
+ },
3089
+ {
3090
+ network: "eip155:8453",
3091
+ tx: "0x99a0842cd6c3fb33ddc03ddf776c2981c629debcd8ac47678a2e5e29e4496242",
3092
+ date: "2026-09-08",
3093
+ summary: "EURC \u2192 USDC on Base",
3094
+ covers: "the reverse direction"
3095
+ }
3096
+ ]
3097
+ },
3098
+ {
3099
+ id: "aftermath",
3100
+ name: "Aftermath",
3101
+ kind: "provider",
3102
+ url: "https://aftermath.finance",
3103
+ networks: ["sui:mainnet"],
3104
+ keyless: true,
3105
+ 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.",
3106
+ 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.",
3107
+ proofs: [
3108
+ {
3109
+ network: "sui:mainnet",
3110
+ tx: "GtykrnLxejyrjmTL8oKVoEtTAiP9eY69CGne7Djw3Cha",
3111
+ date: "2026-09-08",
3112
+ summary: "0.0657 SUI to 0.0505 USDC",
3113
+ covers: "native in; the balance changes on chain match the quote exactly"
3114
+ }
3115
+ ]
3116
+ },
3117
+ {
3118
+ id: "ref-finance",
3119
+ name: "Ref Finance",
3120
+ kind: "provider",
3121
+ url: "https://app.ref.finance",
3122
+ networks: ["near:mainnet"],
3123
+ keyless: true,
3124
+ 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.",
3125
+ 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.",
3126
+ proofs: [
3127
+ {
3128
+ network: "near:mainnet",
3129
+ tx: "FAb28z1tCcecd2teFTxJGkgaLMr1mpctuiFhUeCKjszT",
3130
+ date: "2026-09-08",
3131
+ summary: "0.0505 USDT to 0.05 USDC",
3132
+ covers: "token to token across a stable pool, 14 receipts, all successful"
3133
+ }
3134
+ ]
3135
+ },
3136
+ {
3137
+ id: "vestige",
3138
+ name: "Vestige",
3139
+ kind: "provider",
3140
+ url: "https://vestige.fi",
3141
+ networks: ["algorand:wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qzkkit8="],
3142
+ keyless: true,
3143
+ fee: "The pool's own fee (0.30% on the probed route) plus the group's network fee. PipRail sends no fee parameter.",
3144
+ 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.",
3145
+ proofs: [
3146
+ {
3147
+ network: "algorand:wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qzkkit8=",
3148
+ tx: "5UV7SWSJODHJDGMXODTOMBNXMK4YGZIZKR4VOM6XHML7JS7A32UA",
3149
+ date: "2026-09-08",
3150
+ summary: "0.5094 ALGO to 0.05 USDC",
3151
+ covers: "a four-transaction atomic group, every signer the sender"
3152
+ }
3153
+ ]
3154
+ },
3155
+ {
3156
+ id: "hyperion",
3157
+ name: "Hyperion",
3158
+ kind: "provider",
3159
+ url: "https://hyperion.xyz",
3160
+ networks: ["aptos:1"],
3161
+ keyless: true,
3162
+ 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.",
3163
+ 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.",
3164
+ proofs: [
3165
+ {
3166
+ network: "aptos:1",
3167
+ tx: "0x927293cf8584320f1e265c564ab3096262b675a61996e0da955a2b3c27af1adc",
3168
+ date: "2026-09-08",
3169
+ summary: "0.049997 USDC to exactly 0.05 USDT",
3170
+ covers: "the stable pair, and the first swap in this SDK to deliver an EXACT output rather than at-least"
3171
+ },
3172
+ {
3173
+ network: "aptos:1",
3174
+ tx: "0xae849e031cb50a660f8c9f286c1f3a9caeff08b427efd9428154d884f1e3059e",
3175
+ date: "2026-09-08",
3176
+ summary: "0.040012 USDT to exactly 0.04 USDC",
3177
+ covers: "the reverse direction, which takes the opposite on-chain price bound"
3178
+ },
3179
+ {
3180
+ network: "aptos:1",
3181
+ tx: "0x7921118d64ce1e4ebe7911ed22fef2222597e1b9e20caf0fd4f89bce07c07981",
3182
+ date: "2026-09-08",
3183
+ summary: "0.0459 APT to exactly 0.03 USDC",
3184
+ covers: "native APT in, on a different fee tier"
3185
+ },
3186
+ {
3187
+ network: "aptos:1",
3188
+ tx: "0x640bce4b1e5f688d4494d0c343247b2951d22bc26d371b81f7d2c04655a7a707",
3189
+ date: "2026-09-08",
3190
+ summary: "0.012842 USDC to exactly 0.02 APT",
3191
+ covers: "native APT out, where the balance delta nets off gas and only the deposit event proves the exact amount"
3192
+ }
3193
+ ]
3194
+ },
3195
+ {
3196
+ id: "stonfi",
3197
+ name: "STON.fi",
3198
+ kind: "provider",
3199
+ url: "https://ston.fi",
3200
+ networks: ["tvm:-239"],
3201
+ keyless: true,
3202
+ 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.",
3203
+ 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.",
3204
+ proofs: [
3205
+ {
3206
+ network: "tvm:-239",
3207
+ tx: "1f6d58d073bd988b47272658297ebabc4748725ca919a4a9c863f28cef8eeb3f",
3208
+ date: "2026-09-08",
3209
+ summary: "0.035528 TON for 0.05 USDT",
3210
+ 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"
3211
+ },
3212
+ {
3213
+ network: "tvm:-239",
3214
+ tx: "f76ae97e107b7852ea105d3b3b0caf095ec77bd2541f40417303c08566a87947",
3215
+ date: "2026-09-08",
3216
+ summary: "0.028171 USDT for 0.02 TON",
3217
+ 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"
3218
+ }
3219
+ ]
3220
+ },
3221
+ {
3222
+ id: "sunswap",
3223
+ name: "SunSwap V2",
3224
+ kind: "provider",
3225
+ url: "https://sunswap.com",
3226
+ networks: ["tron:mainnet"],
3227
+ keyless: true,
3228
+ 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.",
3229
+ 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.",
3230
+ 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.",
3231
+ proofs: []
3232
+ }
3233
+ ];
3234
+ function swapProvidersFor(network) {
3235
+ return SWAP_PROVIDERS.filter((p) => p.networks.includes(network));
3236
+ }
3237
+ function canSwapOn(network) {
3238
+ return swapProvidersFor(network).length > 0;
3239
+ }
3240
+ function swappableNetworks() {
3241
+ return [...new Set(SWAP_PROVIDERS.flatMap((p) => p.networks))].sort();
3242
+ }
3243
+
2648
3244
  // src/util/exactRecovery.ts
2649
3245
  function exactSettleCheckHint(family, payerFrom, nonce) {
2650
3246
  switch (family) {
@@ -2706,6 +3302,7 @@ var PipRailClient = class {
2706
3302
  this.assertPolicyAmountCaps(opts.policy);
2707
3303
  this.assertPolicyTimeOptions(opts.policy);
2708
3304
  this.assertPolicySpendControls(opts.policy);
3305
+ this.assertModeIsHonest(opts);
2709
3306
  }
2710
3307
  /**
2711
3308
  * Fail LOUDLY at construction on a malformed amount cap — a security boundary
@@ -3055,12 +3652,141 @@ var PipRailClient = class {
3055
3652
  spent() {
3056
3653
  return this.ledger.summary();
3057
3654
  }
3655
+ /**
3656
+ * Each mode must be able to KEEP its promise, checked once at construction.
3657
+ *
3658
+ * `'supervised'` means a human approves each payment, and the only thing that can pause a
3659
+ * payment for a human is `onBeforePay`. Without it the mode was a label: the client happily
3660
+ * paid without asking anyone, while the name, the docs and the operator's mental model all
3661
+ * said otherwise. A safety control that silently does nothing is worse than an absent one,
3662
+ * because the operator has already stopped worrying about it.
3663
+ *
3664
+ * The mirror of the sovereign rule, which refuses to unlock swapping without a ceiling. Both
3665
+ * fail at CONSTRUCTION rather than at the first payment, so the mistake surfaces while
3666
+ * somebody is reading their own config instead of mid-spend.
3667
+ */
3668
+ assertModeIsHonest(opts) {
3669
+ const mode = opts.mode ?? DEFAULT_AGENT_MODE;
3670
+ if (mode === "supervised" && typeof opts.onBeforePay !== "function") {
3671
+ throw new TypeError(
3672
+ "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.)"
3673
+ );
3674
+ }
3675
+ if (mode === "sovereign" && opts.swapPolicy?.maxPerSwap === void 0) {
3676
+ throw new TypeError(
3677
+ "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."
3678
+ );
3679
+ }
3680
+ }
3058
3681
  /** The CONFIGURED spend policy, read back unchanged — so an agent can self-check
3059
3682
  * its WHOLE leash (caps, allowlists, time, denom + count limits) without hitting a
3060
3683
  * decline. `undefined` when no policy is set. Pure; never throws. */
3061
3684
  policy() {
3062
3685
  return this.opts.policy;
3063
3686
  }
3687
+ /**
3688
+ * Who is answerable for this wallet: `'supervised'`, `'budgeted'` (default) or
3689
+ * `'sovereign'`. READ-ONLY on purpose. A model asks what authority it has; it can
3690
+ * never grant itself more, exactly as an agent cannot widen its own permissions in
3691
+ * its host. Set once at construction by whoever provisioned the key.
3692
+ */
3693
+ mode() {
3694
+ return this.opts.mode ?? DEFAULT_AGENT_MODE;
3695
+ }
3696
+ /** The configured swap guardrails, read back unchanged. `undefined` when none is set. */
3697
+ swapPolicy() {
3698
+ return this.opts.swapPolicy;
3699
+ }
3700
+ /**
3701
+ * This wallet's OWN address — where it gets paid.
3702
+ *
3703
+ * The receiving half of a wallet, and the one thing an agent handed a key cannot work
3704
+ * out for itself: the key is set by whoever provisioned it, so without this the agent
3705
+ * can spend but can never tell a buyer where to send anything. `sell` defaults its
3706
+ * `payTo` to exactly this.
3707
+ *
3708
+ * Derived from the key, not the network — no RPC read, nothing moved. Throws
3709
+ * {@link WalletRequiredError} on a read-only client (no wallet, no address).
3710
+ */
3711
+ /** The chain this client is configured for, exactly as it was given. A sold offer
3712
+ * defaults to it, so an agent prices on the chain it actually holds funds on. */
3713
+ chain() {
3714
+ return this.opts.chain;
3715
+ }
3716
+ /**
3717
+ * What this wallet actually HOLDS, per asset — the balance sheet, not the leash.
3718
+ *
3719
+ * `budget()` answers "how much of my allowance is left", a different question and the only
3720
+ * one an agent could previously ask. An agent that OWNS a wallet has to answer "what do I
3721
+ * have?" before it can decide anything: whether to sell, whether to swap, whether it needs
3722
+ * topping up and in which denomination.
3723
+ *
3724
+ * RPC-read-only and never throws for a read problem: an asset whose read was unavailable
3725
+ * comes back `null` (unknown), never `0`, because a rate-limited read that reads as "broke"
3726
+ * would make an agent behave as though it had been drained. A symbol this chain does not ship
3727
+ * is reported as unknown rather than guessed at.
3728
+ */
3729
+ async balanceOf(assets = ["native"]) {
3730
+ const { net, wallet } = await this.ensure();
3731
+ if (!wallet) {
3732
+ throw new WalletRequiredError("balanceOf needs a wallet \u2014 a read-only client holds nothing.");
3733
+ }
3734
+ const out = [];
3735
+ for (const symbol of assets) {
3736
+ let asset;
3737
+ let decimals;
3738
+ let resolvedSymbol;
3739
+ try {
3740
+ const t = net.resolveToken(symbol);
3741
+ asset = t.asset;
3742
+ decimals = t.decimals;
3743
+ resolvedSymbol = t.symbol;
3744
+ } catch {
3745
+ out.push({ symbol, asset: null, decimals: null, known: false, amount: null, amountFormatted: null });
3746
+ continue;
3747
+ }
3748
+ const bal = await net.balanceOf(wallet, asset).catch(() => ({ token: null, native: null }));
3749
+ out.push({
3750
+ symbol: resolvedSymbol ?? symbol,
3751
+ asset,
3752
+ decimals,
3753
+ known: true,
3754
+ amount: bal.token === null ? null : bal.token.toString(),
3755
+ amountFormatted: bal.token === null ? null : formatUnits(bal.token, decimals)
3756
+ });
3757
+ }
3758
+ return out;
3759
+ }
3760
+ async address() {
3761
+ const { net, wallet } = await this.ensure();
3762
+ if (!wallet) {
3763
+ throw new WalletRequiredError(
3764
+ "address() needs a wallet \u2014 a read-only client has no address to be paid at."
3765
+ );
3766
+ }
3767
+ return net.addressOf(wallet);
3768
+ }
3769
+ /**
3770
+ * May a model SELL on this wallet — price its own offers and collect payment for them?
3771
+ * True only in `'sovereign'` mode, the same authority test as {@link canAgentSwap}:
3772
+ * earning is the other half of owning a wallet, and an agent that answers for its own
3773
+ * balance answers for how that balance is filled.
3774
+ *
3775
+ * Taking money is not the risk that gates this. Committing to DELIVER something is, and
3776
+ * so is publishing an address as an open invitation to pay it — neither is a supervised
3777
+ * agent's call to make alone.
3778
+ */
3779
+ canAgentSell() {
3780
+ return this.mode() === "sovereign";
3781
+ }
3782
+ /**
3783
+ * May a model move this wallet's own funds between denominations? True only in
3784
+ * `'sovereign'` mode. The payment tools consult this, so the answer lives in ONE place
3785
+ * rather than being re-derived by every surface that asks.
3786
+ */
3787
+ canAgentSwap() {
3788
+ return this.mode() === "sovereign";
3789
+ }
3064
3790
  /**
3065
3791
  * Read-only budget + time leash for a Mode-A (headless) agent — the policy IS
3066
3792
  * the consent, and this is how the agent SEES what's left of it before paying.
@@ -3210,22 +3936,142 @@ var PipRailClient = class {
3210
3936
  const plan = await this.planPayment(url, init);
3211
3937
  return plan == null ? true : plan.payable;
3212
3938
  }
3213
- /* ------------------------- discovery (find + list) ------------------------- */
3939
+ /* --------------------------- swap (OPTIONAL helper) --------------------------- */
3214
3940
  /**
3215
- * Find payable resources on the OPEN x402 indexes WITHOUT paying. Reads the
3216
- * free indexes (CDP Bazaar + 402 Index by default), merges + dedupes them, and
3217
- * by default returns only resources payable on THIS client's chain
3218
- * (`network: 'self'`). Each result carries its advertised `rails[]`; feed a
3219
- * chosen `resource` straight into `quote()` → `planPayment()` → `fetch()`.
3941
+ * Price a same-chain swap "I hold the wrong token". READ-ONLY: no funds move,
3942
+ * nothing is signed, nothing is committed to.
3220
3943
  *
3221
- * Nothing PipRail-hosted: these are third-party open directories. Never throws
3222
- * for a read problem an index that's down or changed simply contributes
3223
- * nothing. Honest caveats (see {@link DIRECTORY_INFO}):
3224
- * - Reads **`bazaar` + `402index`** only **NOT `x402scan`** (its reads are paid). A
3225
- * resource you registered on x402scan is live there but will NOT appear here; don't
3226
- * read that absence as failure. (Passing `sources:['x402scan']` explicitly yields `[]`.)
3227
- * - A resource just listed via {@link register} may not appear yet — 402 Index reviews
3228
- * before publishing, so retry with a brief backoff if a fresh listing is missing.
3944
+ * ⚠️ **This is a convenience, not part of paying.** Nothing calls it for you.
3945
+ * `fetch()` never swaps, `planPayment()` never swaps, and there is deliberately no
3946
+ * `autoSwap` option: converting one asset into another is a priced, irreversible
3947
+ * act, and a payment library should not do that on your behalf because it noticed
3948
+ * you were short. If you would rather bridge or swap somewhere else entirely, or
3949
+ * just top the wallet up by hand, that is a perfectly good answer and this method
3950
+ * costs you nothing by existing.
3951
+ *
3952
+ * 🔴 **The rate is not PipRail's.** Read `quote.source` — it names who produced the
3953
+ * number. Today both implementations are `kind: 'protocol'`, meaning the chain's
3954
+ * own order books priced it and no company is involved.
3955
+ *
3956
+ * Returns `null` — never throws — when this chain has no swap support, the pair
3957
+ * can't be routed, there's no liquidity, or a read failed. `null` means "no quote",
3958
+ * never "no funds".
3959
+ */
3960
+ async quoteSwap(req) {
3961
+ const slippageBps = resolveSlippageBps(req.slippageBps);
3962
+ const sp = this.opts.swapPolicy;
3963
+ if (sp?.maxSlippageBps !== void 0 && slippageBps > sp.maxSlippageBps) {
3964
+ throw new PaymentDeclinedError(
3965
+ `slippageBps ${slippageBps} exceeds this agent's swapPolicy.maxSlippageBps of ${sp.maxSlippageBps}.`,
3966
+ { reasonCode: "POLICY" }
3967
+ );
3968
+ }
3969
+ const { net, wallet } = await this.ensure();
3970
+ if (!net.quoteSwap || !wallet) return null;
3971
+ let from;
3972
+ let to;
3973
+ let wantAmount;
3974
+ try {
3975
+ from = net.resolveToken(req.from);
3976
+ to = net.resolveToken(req.to);
3977
+ wantAmount = parseUnits(req.wantAmount, to.decimals);
3978
+ } catch {
3979
+ return null;
3980
+ }
3981
+ try {
3982
+ return await net.quoteSwap({ from, to, wantAmount, slippageBps, wallet });
3983
+ } catch {
3984
+ return null;
3985
+ }
3986
+ }
3987
+ /**
3988
+ * Execute a swap you have already quoted and chosen to accept. Signs from your own
3989
+ * wallet (one transaction, or two where a token must be approved first); on every route
3990
+ * the funds never leave your account, they just change denomination.
3991
+ *
3992
+ * Pass the {@link SwapQuote} from {@link quoteSwap} unmodified — it carries the
3993
+ * route and the on-chain slippage cap. Re-quote rather than reusing an old one: a
3994
+ * stale route is how you get a worse price than you were shown.
3995
+ *
3996
+ * Throws {@link WalletRequiredError} without a wallet, {@link UnsupportedNetworkError}
3997
+ * when the chain has no swap support, and {@link InsufficientFundsError} when the
3998
+ * wallet can't cover it (which includes the market moving past your slippage cap —
3999
+ * nothing is swapped in that case, though chains that charge for a reverted
4000
+ * transaction, such as EVM, Aptos and Tron, still take the gas).
4001
+ */
4002
+ async swap(quote) {
4003
+ const { net, wallet } = await this.ensure();
4004
+ if (!wallet) {
4005
+ throw new WalletRequiredError(
4006
+ "swap needs a wallet \u2014 it signs a transaction. This client is read-only."
4007
+ );
4008
+ }
4009
+ if (!net.swap) {
4010
+ const venues = SWAP_PROVIDERS.map((p) => p.name).join(", ");
4011
+ throw new UnsupportedNetworkError(
4012
+ `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.`
4013
+ );
4014
+ }
4015
+ if (quote.network !== net.network) {
4016
+ throw new UnsupportedNetworkError(
4017
+ `This swap quote is for ${quote.network} but the client is bound to ${net.network}.`
4018
+ );
4019
+ }
4020
+ const sp = this.opts.swapPolicy;
4021
+ if (sp?.maxPerSwap !== void 0) {
4022
+ const cap = parseUnits(sp.maxPerSwap, quote.from.decimals);
4023
+ if (BigInt(quote.maxSpend) > cap) {
4024
+ throw new PaymentDeclinedError(
4025
+ `This swap could spend up to ${quote.maxSpendFormatted} ${quote.from.symbol}, over this agent's swapPolicy.maxPerSwap of ${sp.maxPerSwap}. Nothing was signed.`,
4026
+ { reasonCode: "POLICY" }
4027
+ );
4028
+ }
4029
+ }
4030
+ if (sp?.allowTo?.length) {
4031
+ const want = quote.to.symbol;
4032
+ if (!sp.allowTo.some((t) => t.toUpperCase() === want.toUpperCase())) {
4033
+ throw new PaymentDeclinedError(
4034
+ `This agent may only swap into ${sp.allowTo.join(", ")}, not ${want}. Nothing was signed.`,
4035
+ { reasonCode: "POLICY" }
4036
+ );
4037
+ }
4038
+ }
4039
+ const approve = this.opts.onBeforeSwap;
4040
+ if (approve) {
4041
+ let okToSwap;
4042
+ try {
4043
+ okToSwap = await approve(quote);
4044
+ } catch (err) {
4045
+ throw new PaymentDeclinedError(
4046
+ "onBeforeSwap threw \u2014 refusing to swap. Nothing was signed.",
4047
+ { reasonCode: "APPROVAL", cause: err }
4048
+ );
4049
+ }
4050
+ if (okToSwap !== true) {
4051
+ throw new PaymentDeclinedError(
4052
+ `onBeforeSwap declined this swap of up to ${quote.maxSpendFormatted} ${quote.from.symbol} for ${quote.to.symbol}. Nothing was signed.`,
4053
+ { reasonCode: "APPROVAL" }
4054
+ );
4055
+ }
4056
+ }
4057
+ return net.swap(wallet, quote);
4058
+ }
4059
+ /* ------------------------- discovery (find + list) ------------------------- */
4060
+ /**
4061
+ * Find payable resources on the OPEN x402 indexes — WITHOUT paying. Reads the
4062
+ * free indexes (CDP Bazaar + 402 Index by default), merges + dedupes them, and
4063
+ * by default returns only resources payable on THIS client's chain
4064
+ * (`network: 'self'`). Each result carries its advertised `rails[]`; feed a
4065
+ * chosen `resource` straight into `quote()` → `planPayment()` → `fetch()`.
4066
+ *
4067
+ * Nothing PipRail-hosted: these are third-party open directories. Never throws
4068
+ * for a read problem — an index that's down or changed simply contributes
4069
+ * nothing. Honest caveats (see {@link DIRECTORY_INFO}):
4070
+ * - Reads **`bazaar` + `402index`** only — **NOT `x402scan`** (its reads are paid). A
4071
+ * resource you registered on x402scan is live there but will NOT appear here; don't
4072
+ * read that absence as failure. (Passing `sources:['x402scan']` explicitly yields `[]`.)
4073
+ * - A resource just listed via {@link register} may not appear yet — 402 Index reviews
4074
+ * before publishing, so retry with a brief backoff if a fresh listing is missing.
3229
4075
  * - Results are cross-scheme (mostly the mainstream `exact` scheme); `fetch()` pays
3230
4076
  * `onchain-proof` rails by default, and standard `exact` rails too once you opt in
3231
4077
  * with `schemes: ['onchain-proof', 'exact']` (EVM EIP-3009/Permit2 + Solana SVM + Algorand
@@ -4601,6 +5447,71 @@ var MultiChainPayer = class _MultiChainPayer {
4601
5447
  policy() {
4602
5448
  return this._clients[0].policy();
4603
5449
  }
5450
+ /*
5451
+ * ── SWAP, ACROSS A MULTI-CHAIN PAYER ────────────────────────────────────────────
5452
+ *
5453
+ * A swap is SAME-CHAIN by definition, so there is nothing to route: it delegates to the
5454
+ * PRIMARY client, which is the first one given, and the constructor already documents
5455
+ * that order as your chain preference. Without this the MCP could never expose swapping
5456
+ * at all, because it always wraps its accounts in a MultiChainPayer even for one chain.
5457
+ */
5458
+ mode() {
5459
+ return this._clients[0].mode();
5460
+ }
5461
+ canAgentSwap() {
5462
+ return this._clients[0].canAgentSwap();
5463
+ }
5464
+ /*
5465
+ * ── SELLING, ACROSS A MULTI-CHAIN PAYER ─────────────────────────────────────────
5466
+ *
5467
+ * Same reasoning as the swap block above, and the same necessity: the MCP always wraps
5468
+ * its accounts in a MultiChainPayer, so without these the seller tools would be
5469
+ * unreachable through the MCP no matter what mode the operator set.
5470
+ *
5471
+ * The address is the PRIMARY chain's — a wallet has one address per family, not one
5472
+ * overall, so "where do I get paid?" is only answerable per chain. An offer priced on
5473
+ * another chain names its own payTo; this is the default, not the only option.
5474
+ */
5475
+ canAgentSell() {
5476
+ return this._clients[0].canAgentSell();
5477
+ }
5478
+ async address() {
5479
+ return this._clients[0].address();
5480
+ }
5481
+ chain() {
5482
+ return this._clients[0].chain();
5483
+ }
5484
+ /**
5485
+ * Holdings across EVERY chain this payer owns, not just the primary. A multi-chain agent's
5486
+ * balance sheet is the union: reporting only the first chain would tell it it was broke while
5487
+ * it held funds one client along, which is exactly the wrong answer to "what do I have?".
5488
+ */
5489
+ async balanceOf(assets = ["native"]) {
5490
+ const out = [];
5491
+ for (const c of this._clients) {
5492
+ const rows = await c.balanceOf(assets).catch(() => []);
5493
+ for (const r of rows) out.push({ ...r, chain: String(c.chain()) });
5494
+ }
5495
+ return out;
5496
+ }
5497
+ async quoteSwap(req) {
5498
+ return this._clients[0].quoteSwap(req);
5499
+ }
5500
+ async swap(quote) {
5501
+ let firstRefusal;
5502
+ for (const c of this._clients) {
5503
+ try {
5504
+ return await c.swap(quote);
5505
+ } catch (err) {
5506
+ if (err instanceof WrongChainError || err?.name === "UnsupportedNetworkError") {
5507
+ firstRefusal ??= err;
5508
+ continue;
5509
+ }
5510
+ throw err;
5511
+ }
5512
+ }
5513
+ throw firstRefusal ?? new Error("MultiChainPayer: no client could execute this swap quote.");
5514
+ }
4604
5515
  };
4605
5516
  function isBodyInit(value) {
4606
5517
  if (typeof value === "string") return true;
@@ -4741,6 +5652,22 @@ var PIPRAIL_AGENT_GUIDE = `# Paying with PipRail \u2014 the agent contract
4741
5652
  You can pay for x402 "402 Payment Required" resources autonomously. Money moves
4742
5653
  straight from your wallet to the server; PipRail custodies nothing. Follow this.
4743
5654
 
5655
+ ## Your tools, and which ones act
5656
+ You always have these eight tools. Six are READ-ONLY and safe to call freely, two change
5657
+ something, and only ONE spends. In 'sovereign' mode you also hold the swap and seller tools
5658
+ described later, so count your OWN list rather than trusting this number:
5659
+ - piprail_guide \u2014 this contract. Re-read it if you lose the thread. Read-only.
5660
+ - piprail_discover \u2014 find payable resources on the OPEN indexes (a phone book of x402 APIs),
5661
+ without paying. Read-only.
5662
+ - piprail_quote_payment \u2014 the price of one URL. Read-only.
5663
+ - piprail_plan_payment \u2014 can I afford it NOW, on this wallet. Read-only.
5664
+ - piprail_budget \u2014 how much budget and time is left. Read-only.
5665
+ - piprail_verify_receipt \u2014 re-check a receipt against the chain. Read-only.
5666
+ - \u{1F536} piprail_register \u2014 PUBLISHES a resource you run to a public index. It writes to the
5667
+ outside world and cannot be un-published by you, so only call it when asked to list
5668
+ something. It moves no money.
5669
+ - \u{1F534} piprail_pay_request \u2014 the ONLY tool that spends. Everything else is safe to explore with.
5670
+
4744
5671
  ## Landing cold \u2014 read the self-description
4745
5672
  Every PipRail 402 self-describes. Read challenge.extensions.piprail for { name, what, pay[]
4746
5673
  (each rail's how-to-pay), sdk.install, mcp, docs } \u2014 never guess what an endpoint is. If your
@@ -4805,6 +5732,87 @@ Branch on \`code\` (always reliable). Key cases:
4805
5732
  If it's a standard x402 server offering an exact rail, that's a config fix the operator makes
4806
5733
  once (enable the exact scheme); report it, don't retry the same call blindly.
4807
5734
 
5735
+ ## Finding work to buy \u2014 piprail_discover
5736
+ You do not have to be handed a URL. piprail_discover reads the open x402 indexes and returns
5737
+ resources with their advertised rails, which you can feed straight into quote \u2192 plan \u2192 pay.
5738
+ It never throws: an index that is down simply contributes nothing, so an empty list means
5739
+ "nothing found", never "something broke". These are third-party directories, not a PipRail
5740
+ registry \u2014 treat a listing as a claim, and let quote/plan tell you what is really true.
5741
+
5742
+ ## Proving a payment settled \u2014 piprail_verify_receipt
5743
+ A receipt is a claim until it is checked. piprail_verify_receipt re-reads the transaction from
5744
+ the chain and tells you whether the funds provably moved, to the right recipient, for the right
5745
+ amount. Use it when you must be SURE (before delivering something costly, or when reconciling),
5746
+ and after any timeout where the payment may already be on-chain. Read-only, and it moves nothing.
5747
+
5748
+ ## Who else is in the path \u2014 nobody who holds your money
5749
+ On the gasless exact rail a FACILITATOR may broadcast your signed authorization (PayAI, Ultravioleta
5750
+ DAO and others). You never choose one and never need an account with one: the operator configures it,
5751
+ and it is a courier, not a custodian. It cannot change the amount or the recipient, because those are
5752
+ inside what you signed. If one is down the payment fails cleanly; it cannot take your funds.
5753
+
5754
+ ## Getting PAID \u2014 only if you hold piprail_sell
5755
+ If piprail_sell is in your tool list you are in 'sovereign' mode and the wallet is yours to
5756
+ EARN with, not only to spend from. Three tools, mirroring the buying loop:
5757
+ - piprail_sell \u2014 price something and get a \`challenge\` to hand a buyer. Mirrors quote.
5758
+ - piprail_collect \u2014 verify a proof a buyer sent you. Mirrors pay.
5759
+ - piprail_earnings \u2014 what you have actually been paid. Mirrors piprail_budget.
5760
+ - piprail_wallet \u2014 what you HOLD, and the address you get paid at. Read-only.
5761
+
5762
+ Know the difference between your two numbers: piprail_budget is your spend LEASH (how much of
5763
+ your allowance is left), piprail_wallet is what you actually OWN. Check piprail_wallet before
5764
+ deciding to sell, to swap, or to ask to be topped up, and hand out its \`address\` to anyone who
5765
+ needs to send you funds. A null amount there means the read FAILED, not that you are broke:
5766
+ retry before you act on it.
5767
+
5768
+ The loop: piprail_sell \u2192 give the buyer the \`challenge\` \u2192 they send back a proof \u2192
5769
+ piprail_collect \u2192 and ONLY on paid:true, deliver.
5770
+
5771
+ A proof belongs to ONE offer. If a buyer hands you a settlement that was really payment for a
5772
+ different offer, piprail_collect returns paid:false with code 'wrong_offer', even when the two
5773
+ cost the same and pay the same address. Do not "helpfully" retry it against another offerId: that
5774
+ is the exact move the check exists to stop.
5775
+
5776
+ \u{1F534} COLLECT IS THE ONLY PROOF YOU WERE PAID. A buyer saying they paid, a real-looking tx hash,
5777
+ and a settled payment are three different things, and only piprail_collect tells them apart.
5778
+ Deliver nothing before it returns paid:true. It reads the chain; a claim is not evidence, and
5779
+ a confident buyer is not evidence. One proof is one sale: collecting the same proof twice
5780
+ returns paid:false, which is a REPLAY and not a second payment, so never treat it as one.
5781
+
5782
+ You need no web server and no open port: a challenge is just data, so carry it to the buyer
5783
+ over whatever you already speak. Receiving needs NO KEY at all, only your address, so the
5784
+ earning side cannot spend and cannot be drained even if the machine you run on is taken.
5785
+ piprail_sell defaults \`payTo\` to your OWN address; set it to someone else's only if you
5786
+ genuinely mean to be paid elsewhere, and NEVER to an address a buyer supplies.
5787
+
5788
+ Two things to check on an offer. If its schemes are onchain-proof only, most agent-buyers
5789
+ CANNOT pay it and your listing will simply stay silent, so read any warnings piprail_sell
5790
+ returns. And offers live in memory for this session: a restart clears them, so re-price
5791
+ anything you still intend to sell. To let strangers find an offer, call piprail_register.
5792
+
5793
+ ## Wrong token? Check your MODE before you answer
5794
+ Wrong token on the RIGHT chain? What you may do depends on the mode your operator set, and
5795
+ YOUR TOOL LIST IS THE TRUTH \u2014 LOOK AT IT, never claim a capability it does not show:
5796
+ - no piprail_swap \u21D2 'budgeted' or 'supervised'. You CANNOT swap. Top up, pay from another
5797
+ chain you hold funds on, tell the human, or skip it. fundingHint names what is missing.
5798
+ Never offer "I could swap for you".
5799
+ - piprail_swap present \u21D2 'sovereign': this wallet is yours and you answer for it. Price with
5800
+ piprail_quote_swap, read who priced it, then piprail_swap.
5801
+
5802
+ \u{1F534} You cannot change your own mode \u2014 whoever provisioned this wallet set it, exactly as you
5803
+ cannot widen your own permissions in your host. Never ask for it.
5804
+
5805
+ WHY the default withholds it: YOUR SPEND POLICY DOES NOT GOVERN SWAPS. Every cap you have
5806
+ limits PAYING A MERCHANT; a swap moves your own funds between denominations and passes all
5807
+ of them untouched, so an agent looping between tokens can bleed a wallet through fees and
5808
+ slippage without tripping one budget check. Sovereign mode bounds it with a SEPARATE
5809
+ instrument (a ceiling per swap and on slippage), so a refusal there is your swapPolicy, not
5810
+ your budget.
5811
+
5812
+ True in EVERY mode: a swap is SAME-CHAIN only and can never move funds between chains, and
5813
+ PipRail prices nothing itself, so every quote NAMES the venue that priced it \u2014 read
5814
+ \`source\` before you trust a number. docs.piprail.com/making-payments/swapping/
5815
+
4808
5816
  ## Knowing your leash \u2014 call piprail_budget
4809
5817
  piprail_budget tells you how much budget and time you have left: per (network,
4810
5818
  asset) remaining, the cross-token GRAND TOTAL per denomination (e.g. how much
@@ -4812,13 +5820,15 @@ USD you can still spend across every stablecoin and chain), the payment-count
4812
5820
  leash, the session time envelope, your spend so far, and the configured policy
4813
5821
  read back. Read-only; moves no funds. Use it in Mode A to self-check before paying.
4814
5822
 
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.
5823
+ ## Two modes of CONSENT \u2014 a different axis from the mode above
5824
+ Your 'supervised'/'budgeted'/'sovereign' mode says what you may DO. This says how each
5825
+ payment is agreed. Both are in force at once, so read them together:
5826
+ - Mode A (headless \u2014 how 'budgeted' and 'sovereign' behave): you run FREE inside a pre-set
5827
+ budget + time envelope. The policy IS the consent, and there is no per-payment prompt.
5828
+ Stay inside it; piprail_budget shows what's left.
5829
+ - Mode B (supervised \u2014 how the 'supervised' mode behaves): the host may ask a human to
5830
+ approve each payment. A decline/cancel/timeout comes back as declined:true
5831
+ (reasonCode:'APPROVAL') \u2014 do NOT retry it as if it were a transient error.
4822
5832
 
4823
5833
  ## Hard facts
4824
5834
  - Per-payment + per-(network, asset) caps always apply. A cross-token GRAND TOTAL per
@@ -4835,525 +5845,110 @@ function agentGuide() {
4835
5845
  return PIPRAIL_AGENT_GUIDE;
4836
5846
  }
4837
5847
 
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;
5848
+ // src/discovery.ts
5849
+ var GENERATOR = "@piprail/sdk \xB7 https://piprail.com";
5850
+ var POWERED_BY = "PipRail x402 | https://piprail.com";
5851
+ function discoveryHeaders(opts = {}) {
5852
+ return {
5853
+ link: '</openapi.json>; rel="service-desc", </.well-known/x402>; rel="x402-discovery"',
5854
+ ...opts.attribution === false ? {} : { "x-powered-by": POWERED_BY }
5855
+ };
5856
+ }
5857
+ function buildBazaarExtension(descriptor = {}) {
5858
+ const method = (descriptor.method ?? "GET").toUpperCase();
5859
+ const queryParams = descriptor.queryParams ?? {};
5860
+ return {
5861
+ info: {
5862
+ input: { type: "http", method, queryParams },
5863
+ output: descriptor.output ?? { type: "json" }
5864
+ },
5865
+ schema: {
5866
+ $schema: "https://json-schema.org/draft/2020-12/schema",
5867
+ type: "object",
5868
+ properties: {
5869
+ input: {
5870
+ type: "object",
5871
+ properties: {
5872
+ type: { type: "string", const: "http" },
5873
+ method: { type: "string", enum: ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"] },
5874
+ queryParams: { type: "object", properties: queryParams, additionalProperties: false }
5875
+ },
5876
+ required: ["type", "method"],
5877
+ additionalProperties: false
5878
+ }
5879
+ },
5880
+ required: ["input"]
5881
+ }
5882
+ };
5883
+ }
5884
+ function pathOf(url) {
4843
5885
  try {
4844
- return JSON.parse(text);
5886
+ return new URL(url).pathname || "/";
4845
5887
  } catch {
4846
- return text;
5888
+ return url.startsWith("/") ? url : `/${url}`;
4847
5889
  }
4848
5890
  }
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)
5891
+ function buildOpenApi(input) {
5892
+ assertResourceList(input?.resources, "buildOpenApi");
5893
+ const paths = {};
5894
+ for (const r of input.resources) {
5895
+ const path = pathOf(r.url);
5896
+ const method = (r.method ?? "GET").toLowerCase();
5897
+ const op = {
5898
+ ...r.description ? { summary: r.description } : {},
5899
+ responses: {
5900
+ "200": { description: "Paid \u2014 the resource." },
5901
+ "402": { description: "Payment required (x402)." }
5902
+ },
5903
+ "x-payment-info": {
5904
+ x402Version: 2,
5905
+ accepts: r.accepts
5906
+ }
5907
+ };
5908
+ paths[path] = { ...paths[path] ?? {}, [method]: op };
5909
+ }
5910
+ return {
5911
+ openapi: "3.1.0",
5912
+ info: { title: input.title ?? "PipRail x402 resources", version: input.version ?? "1.0.0" },
5913
+ servers: [{ url: input.origin }],
5914
+ paths,
5915
+ // "Built with @piprail/sdk" — default on (opt out with attribution:false). At the
5916
+ // document ROOT so `info` stays exactly { title, version }.
5917
+ ...input.attribution === false ? {} : { "x-generator": GENERATOR },
5918
+ ...input.ownershipProofs && input.ownershipProofs.length > 0 ? { "x-agentcash-provenance": { ownershipProofs: input.ownershipProofs } } : {}
4856
5919
  };
4857
- if (err instanceof PaymentDeclinedError) {
4858
- out.declined = true;
4859
- if (err.reasonCode) out.reasonCode = err.reasonCode;
5920
+ }
5921
+ function buildWellKnownX402(input) {
5922
+ assertResourceList(input?.resources, "buildWellKnownX402");
5923
+ return {
5924
+ version: 1,
5925
+ resources: input.resources.map((r) => r.url),
5926
+ ...input.ownershipProofs && input.ownershipProofs.length > 0 ? { ownershipProofs: input.ownershipProofs } : {}
5927
+ };
5928
+ }
5929
+ function assertResourceList(resources, fn) {
5930
+ if (!Array.isArray(resources)) {
5931
+ throw new InvalidConfigError(`${fn}: \`resources\` must be an array of ResourceDescription.`);
4860
5932
  }
4861
- const ref = err.ref;
4862
- if (typeof ref === "string") out.ref = ref;
4863
- return out;
5933
+ resources.forEach((r, i) => {
5934
+ if (r === null || typeof r !== "object" || typeof r.url !== "string") {
5935
+ throw new InvalidConfigError(
5936
+ `${fn}: resources[${i}] must be a ResourceDescription with a string \`url\`.`
5937
+ );
5938
+ }
5939
+ });
4864
5940
  }
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
- };
5325
- }
5326
- function buildWellKnownX402(input) {
5327
- assertResourceList(input?.resources, "buildWellKnownX402");
5328
- return {
5329
- version: 1,
5330
- resources: input.resources.map((r) => r.url),
5331
- ...input.ownershipProofs && input.ownershipProofs.length > 0 ? { ownershipProofs: input.ownershipProofs } : {}
5332
- };
5333
- }
5334
- function assertResourceList(resources, fn) {
5335
- if (!Array.isArray(resources)) {
5336
- throw new InvalidConfigError(`${fn}: \`resources\` must be an array of ResourceDescription.`);
5337
- }
5338
- resources.forEach((r, i) => {
5339
- if (r === null || typeof r !== "object" || typeof r.url !== "string") {
5340
- throw new InvalidConfigError(
5341
- `${fn}: resources[${i}] must be a ResourceDescription with a string \`url\`.`
5342
- );
5343
- }
5344
- });
5345
- }
5346
- function buildWellKnownX402Manifest(input) {
5347
- assertResourceList(input?.resources, "buildWellKnownX402Manifest");
5348
- const lastUpdated = typeof input.lastUpdated === "number" && Number.isFinite(input.lastUpdated) ? input.lastUpdated : Math.floor(Date.now() / 1e3);
5349
- return {
5350
- x402Version: 2,
5351
- lastUpdated,
5352
- items: input.resources.map((r) => ({
5353
- resource: {
5354
- url: r.url,
5355
- ...r.description ? { description: r.description } : {},
5356
- ...r.mimeType ? { mimeType: r.mimeType } : {}
5941
+ function buildWellKnownX402Manifest(input) {
5942
+ assertResourceList(input?.resources, "buildWellKnownX402Manifest");
5943
+ const lastUpdated = typeof input.lastUpdated === "number" && Number.isFinite(input.lastUpdated) ? input.lastUpdated : Math.floor(Date.now() / 1e3);
5944
+ return {
5945
+ x402Version: 2,
5946
+ lastUpdated,
5947
+ items: input.resources.map((r) => ({
5948
+ resource: {
5949
+ url: r.url,
5950
+ ...r.description ? { description: r.description } : {},
5951
+ ...r.mimeType ? { mimeType: r.mimeType } : {}
5357
5952
  },
5358
5953
  type: "http",
5359
5954
  accepts: r.accepts,
@@ -5506,10 +6101,19 @@ async function post(url, body, headers) {
5506
6101
  }
5507
6102
  async function settleViaFacilitator(input) {
5508
6103
  const base2 = input.url.replace(/\/+$/, "");
6104
+ const v2 = input.x402Version >= 2;
5509
6105
  const body = {
5510
6106
  x402Version: input.x402Version,
5511
6107
  paymentPayload: input.paymentPayload,
5512
- paymentRequirements: input.paymentRequirements
6108
+ paymentRequirements: input.paymentRequirements,
6109
+ ...v2 ? {
6110
+ accepted: input.paymentRequirements,
6111
+ resource: {
6112
+ url: input.resource?.url || "https://piprail.com/x402/resource",
6113
+ description: input.resource?.description || "Paid resource",
6114
+ mimeType: input.resource?.mimeType || "application/json"
6115
+ }
6116
+ } : {}
5513
6117
  };
5514
6118
  const auth = input.authHeaders ? await input.authHeaders() : {};
5515
6119
  let verify;
@@ -5775,6 +6379,13 @@ var KNOWN_FACILITATORS = {
5775
6379
  schemes: ["exact"],
5776
6380
  settles: ["eip3009"],
5777
6381
  note: "Dexter: keyless, sponsors gas (no floor hit at $0.001 here). LIVE-settled on Avalanche 2026-06-18 (tx 0xb2263e9a4ea3917eee6acabcb454d42a50264fdd69a0781ed8fcaec5590e264b)."
6382
+ },
6383
+ {
6384
+ url: "https://facilitator.ultravioletadao.xyz",
6385
+ keyless: true,
6386
+ schemes: ["exact"],
6387
+ settles: ["eip3009"],
6388
+ 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
6389
  }
5779
6390
  ],
5780
6391
  // Sei (eip155:1329) — PayAI (the only keyless facilitator that lists Sei).
@@ -5813,6 +6424,27 @@ var KNOWN_FACILITATORS = {
5813
6424
  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
6425
  }
5815
6426
  ],
6427
+ // Robinhood Chain (eip155:4663) — the tokenized-equity Arbitrum Orbit L2, mainnet since
6428
+ // 2026-07-01. Both facilitators settle the `exact` rail in USDG (Paxos Global Dollar), the
6429
+ // asset the chain's own markets quote against; the address below matched our on-chain read
6430
+ // and Paxos's published list exactly. USDG is EIP-3009 AND EIP-2612 (verified on-chain
6431
+ // 2026-09-08), so the gasless rail is real rather than inferred from the chain being an L2.
6432
+ "eip155:4663": [
6433
+ {
6434
+ url: "https://facilitator.ultravioletadao.xyz",
6435
+ keyless: true,
6436
+ schemes: ["exact"],
6437
+ settles: ["eip3009"],
6438
+ 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."
6439
+ },
6440
+ {
6441
+ url: "https://x402.dexter.cash",
6442
+ keyless: true,
6443
+ schemes: ["exact"],
6444
+ settles: ["permit2"],
6445
+ 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).'
6446
+ }
6447
+ ],
5816
6448
  // Solana (mainnet-beta). Keyless fee-payer sponsors for the SVM exact rail, each LIVE-settled
5817
6449
  // by us (a real SPL TransferChecked, buyer paid zero SOL) on the dated day — beyond a /supported
5818
6450
  // read. Daydreams + Questflow are intentionally ABSENT: their /supported is public but /verify
@@ -5839,18 +6471,26 @@ var KNOWN_FACILITATORS = {
5839
6471
  settles: ["svm"],
5840
6472
  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
6473
  }
6474
+ ],
6475
+ // NEAR (near:mainnet) — the first keyless facilitator settlement on NEAR.
6476
+ //
6477
+ // 🔴 A CORRECTION, kept here because the wrong conclusion was recorded for months. This entry
6478
+ // read "DELIBERATELY UNSEEDED — UVD's /verify 400s on near:mainnet, so the listing is aspirational,
6479
+ // not settle-capable (2026-06-18)". That was OUR bug, not theirs. The 400 was
6480
+ // `data did not match any variant of untagged enum VerifyRequestEnvelope`: PipRail sent an x402 v2
6481
+ // body carrying only the v1 `paymentRequirements` key, and a strict v2 facilitator needs `accepted`
6482
+ // and `resource` at the root. We read a deserialization error as a missing chain implementation.
6483
+ // Fixed in facilitator.ts (a v2 body now carries both spellings); NEAR settled first time after.
6484
+ // The lesson: a 400 from a facilitator is a claim about the REQUEST until you have read its body.
6485
+ "near:mainnet": [
6486
+ {
6487
+ url: "https://facilitator.ultravioletadao.xyz",
6488
+ keyless: true,
6489
+ schemes: ["exact"],
6490
+ settles: ["near"],
6491
+ 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."
6492
+ }
5842
6493
  ]
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
6494
  };
5855
6495
  function knownFacilitatorsFor(network) {
5856
6496
  return KNOWN_FACILITATORS[network] ?? [];
@@ -6225,520 +6865,1354 @@ function createPaymentGate(options) {
6225
6865
  amountFormatted = formatUnits(BigInt(receipt.amount), spec.decimals);
6226
6866
  } catch {
6227
6867
  }
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 {
6868
+ return {
6869
+ ...receipt,
6870
+ decimals: spec.decimals,
6871
+ ...spec.symbol ? { symbol: spec.symbol } : {},
6872
+ amountFormatted,
6873
+ idempotencyKey: receipt.transaction
6874
+ };
6875
+ }
6876
+ function reportOnPaidError(error, receipt) {
6877
+ if (!options.onPaidError) return;
6878
+ try {
6879
+ options.onPaidError(error, receipt);
6880
+ } catch {
6881
+ }
6882
+ }
6883
+ function fireOnPaid(receipt) {
6884
+ if (!options.onPaid) return;
6885
+ let outcome;
6886
+ try {
6887
+ outcome = options.onPaid(receipt);
6888
+ } catch (err) {
6889
+ reportOnPaidError(err, receipt);
6890
+ return;
6891
+ }
6892
+ if (outcome != null && typeof outcome.then === "function") {
6893
+ return Promise.resolve(outcome).catch((err) => reportOnPaidError(err, receipt));
6894
+ }
6895
+ }
6896
+ async function deliverOnPaid(spec, receipt) {
6897
+ const paid = enrichReceipt(spec, receipt);
6898
+ if (options.awaitOnPaid) await fireOnPaid(paid);
6899
+ else void fireOnPaid(paid);
6900
+ }
6901
+ async function buildPaidResult(spec, receipt, nonce) {
6902
+ if (!receiptsOn) {
6903
+ return { kind: "paid", receipt, receiptHeader: buildReceiptHeader(receipt) };
6904
+ }
6905
+ try {
6906
+ const stamped = {
6907
+ ...receipt,
6908
+ ...nonce ? { nonce } : {},
6909
+ // §5.3: a suppressed tx is the empty string on the wire, never a missing key.
6910
+ ...receiptIncludeTxHash ? {} : { transaction: "" }
6911
+ };
6912
+ const attestation = await maybeSignAttestation(spec, stamped);
6913
+ const extensions = buildReceiptExtension({
6914
+ receipt: stamped,
6915
+ resource: { url: receiptResourceUrl },
6916
+ decimals: spec.decimals,
6917
+ ...attestation ? { attestation } : {}
6918
+ });
6919
+ return { kind: "paid", receipt: stamped, receiptHeader: buildReceiptHeader(stamped, extensions) };
6920
+ } catch {
6921
+ return { kind: "paid", receipt, receiptHeader: buildReceiptHeader(receipt) };
6922
+ }
6923
+ }
6924
+ async function maybeSignAttestation(spec, stamped) {
6925
+ if (attestJws) {
6926
+ warnAttestDegrade("JWS attestation is not yet implemented (R3)");
6927
+ return void 0;
6928
+ }
6929
+ if (attestWallet === void 0) return void 0;
6930
+ if (typeof spec.net.signReceipt !== "function") {
6931
+ warnAttestDegrade(`is EVM-only; the ${spec.net.family} rail can't sign an EIP-712 attestation`);
6932
+ return void 0;
6933
+ }
6934
+ try {
6935
+ const wallet = spec.net.bindWallet(attestWallet);
6936
+ return await spec.net.signReceipt(wallet, {
6937
+ payTo: spec.payTo,
6938
+ network: spec.net.network,
6939
+ resourceUrl: receiptResourceUrl,
6940
+ payer: stamped.payer,
6941
+ issuedAt: nowUnixSeconds(stamped.verifiedAt),
6942
+ // §5.3: the signed message carries the empty string for a suppressed tx, never omitted.
6943
+ transaction: receiptIncludeTxHash ? stamped.transaction : ""
6944
+ });
6945
+ } catch {
6946
+ warnAttestDegrade("signing failed");
6947
+ return void 0;
6948
+ }
6949
+ }
6950
+ function reportOnFailedError(error, failure) {
6951
+ if (!options.onFailedError) return;
6952
+ try {
6953
+ options.onFailedError(error, failure);
6954
+ } catch {
6955
+ }
6956
+ }
6957
+ function fireOnFailed(failure) {
6958
+ if (!options.onFailed) return;
6959
+ let outcome;
6960
+ try {
6961
+ outcome = options.onFailed(failure);
6962
+ } catch (err) {
6963
+ reportOnFailedError(err, failure);
6964
+ return;
6965
+ }
6966
+ if (outcome != null && typeof outcome.then === "function") {
6967
+ return Promise.resolve(outcome).catch((err) => reportOnFailedError(err, failure));
6968
+ }
6969
+ }
6970
+ async function deliverOnFailed(result) {
6971
+ const code = result.error;
6972
+ const failure = { code, detail: result.detail, transient: TRANSIENT_VERIFY_CODES.has(code) };
6973
+ if (options.awaitOnFailed) await fireOnFailed(failure);
6974
+ else void fireOnFailed(failure);
6975
+ }
6976
+ function railExtra(full) {
6977
+ const out = {};
6978
+ for (const k of ["assetTransferMethod", "facilitatorAddress", "name", "version"]) {
6979
+ const v = full[k];
6980
+ if (v !== void 0 && v !== "") out[k] = v;
6981
+ }
6982
+ return Object.keys(out).length > 0 ? { extra: out } : {};
6983
+ }
6984
+ async function describe(resourceUrl = "") {
6985
+ const specs = await ready();
6986
+ const accepts = [];
6987
+ for (const s of specs) {
6988
+ const base2 = {
6989
+ network: s.net.network,
6990
+ asset: s.asset,
6991
+ payTo: s.payTo,
6992
+ amount: s.amountBase.toString(),
6993
+ amountFormatted: s.amountFormatted,
6994
+ decimals: s.decimals,
6995
+ maxTimeoutSeconds,
6996
+ ...s.symbol ? { symbol: s.symbol } : {}
6997
+ };
6998
+ if (s.exact) accepts.push({ scheme: "exact", ...base2, ...railExtra(buildExactAccept(s).extra ?? {}) });
6999
+ if (s.upto) accepts.push({ scheme: "upto", ...base2, ...railExtra(buildUptoAccept(s).extra ?? {}) });
7000
+ accepts.push({ scheme: "onchain-proof", ...base2 });
7001
+ }
7002
+ return {
7003
+ url: resourceUrl,
7004
+ ...options.description ? { description: options.description } : {},
7005
+ ...options.mimeType ? { mimeType: options.mimeType } : {},
7006
+ accepts
7007
+ };
7008
+ }
7009
+ async function verifyOnchainProof(sig) {
7010
+ const specs = await ready();
7011
+ const spec = specs.find(
7012
+ (s) => normalizeNetwork(s.net.network) === normalizeNetwork(sig.accepted.network) && s.asset === sig.accepted.asset
7013
+ );
7014
+ if (!spec) {
7015
+ return rejection(
7016
+ "transfer_not_found",
7017
+ `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(", ")}).`
7018
+ );
7019
+ }
7020
+ const ref = typeof sig.payload.txHash === "string" ? sig.payload.txHash.trim() : sig.payload.txHash;
7021
+ if (await claimTx(ref)) return rejection("tx_already_used", `Proof ${ref} was already redeemed.`);
7022
+ let result;
7023
+ try {
7024
+ result = await spec.net.verify(ref, buildAccept(spec, sig.payload.nonce));
7025
+ } catch (err) {
7026
+ await settleTx(ref, false);
7027
+ throw err;
7028
+ }
7029
+ if (!result.ok) {
7030
+ await settleTx(ref, false);
7031
+ return rejection(result.error, result.detail);
7032
+ }
7033
+ const verified = result.receipt.transaction;
7034
+ if (verified && verified !== ref) {
7035
+ if (await claimTx(verified)) {
7036
+ await settleTx(ref, false);
7037
+ return rejection("tx_already_used", `Payment ${verified} was already redeemed.`);
7038
+ }
7039
+ await settleTx(ref, false);
7040
+ await settleTx(verified, true);
7041
+ } else {
7042
+ await settleTx(ref, true);
7043
+ }
7044
+ await deliverOnPaid(spec, result.receipt);
7045
+ return await buildPaidResult(spec, result.receipt, sig.payload.nonce);
7046
+ }
7047
+ async function verifyExact(exact) {
7048
+ const specs = await ready();
7049
+ const exactSpecs = specs.filter((s) => s.exact);
7050
+ if (exactSpecs.length === 0) {
7051
+ return rejection("transfer_not_found", "This resource offers no standard `exact` rail.");
7052
+ }
7053
+ const isCaip = exact.network.includes(":");
7054
+ const wantExactNet = normalizeNetwork(exact.network);
7055
+ let candidates = isCaip ? exactSpecs.filter((s) => normalizeNetwork(s.net.network) === wantExactNet) : exactSpecs;
7056
+ if (exact.asset) {
7057
+ candidates = candidates.filter((s) => s.asset.toLowerCase() === exact.asset.toLowerCase());
7058
+ }
7059
+ let spec = candidates[0];
7060
+ if (!isCaip && !exact.asset && exactSpecs.length > 1) spec = void 0;
7061
+ if (!spec && !isCaip && !exact.asset && exactSpecs.length === 1) spec = exactSpecs[0];
7062
+ if (!spec || !spec.exact) {
7063
+ return rejection(
7064
+ "transfer_not_found",
7065
+ `No \`exact\` rail offered for ${exact.network}${exact.asset ? `/${exact.asset}` : ""} (offered: ${exactSpecs.map((s) => `${s.asset}@${s.net.network}`).join(", ")}).`
7066
+ );
7067
+ }
7068
+ let nonce;
7069
+ let evmAuth = null;
7070
+ if ("senderAuth" in exact.payload && "transaction" in exact.payload) {
7071
+ nonce = [exact.payload.transaction, exact.payload.senderAuth].map((t) => {
7072
+ try {
7073
+ return Buffer.from(t, "base64").toString("base64");
7074
+ } catch {
7075
+ return t;
7076
+ }
7077
+ }).join("|");
7078
+ } else if ("transaction" in exact.payload) {
7079
+ try {
7080
+ nonce = Buffer.from(exact.payload.transaction, "base64").toString("base64");
7081
+ } catch {
7082
+ nonce = exact.payload.transaction;
7083
+ }
7084
+ } else if ("paymentGroup" in exact.payload) {
7085
+ nonce = exact.payload.paymentGroup.map((t) => {
7086
+ try {
7087
+ return Buffer.from(t, "base64").toString("base64");
7088
+ } catch {
7089
+ return t;
7090
+ }
7091
+ }).join("|");
7092
+ } else if ("signedDelegateAction" in exact.payload) {
7093
+ try {
7094
+ nonce = Buffer.from(exact.payload.signedDelegateAction, "base64").toString("base64");
7095
+ } catch {
7096
+ nonce = exact.payload.signedDelegateAction;
7097
+ }
7098
+ } else if ("signedTxBlob" in exact.payload) {
7099
+ nonce = exact.payload.signedTxBlob.trim().toUpperCase();
7100
+ } else if ("permit2Authorization" in exact.payload) {
7101
+ evmAuth = exact.payload.permit2Authorization;
7102
+ nonce = evmAuth.nonce;
7103
+ } else {
7104
+ evmAuth = exact.payload.authorization;
7105
+ nonce = evmAuth.nonce;
6241
7106
  }
6242
- }
6243
- function fireOnPaid(receipt) {
6244
- if (!options.onPaid) return;
6245
- let outcome;
7107
+ if (await claimTx(nonce)) {
7108
+ return rejection("tx_already_used", `Authorization ${evmAuth ? `nonce ${nonce}` : "transaction"} was already redeemed.`);
7109
+ }
7110
+ const accept = buildExactAccept(spec);
7111
+ const mode = spec.exact.mode;
7112
+ let result;
6246
7113
  try {
6247
- outcome = options.onPaid(receipt);
7114
+ if (mode.kind === "self") {
7115
+ result = await spec.net.settleExactSelf({ relayer: mode.relayer, payload: exact.payload, accept });
7116
+ } else {
7117
+ const ftMethod = accept.extra?.assetTransferMethod;
7118
+ const needsFeePayer = ftMethod === "svm" || ftMethod === "algorand" || ftMethod === "aptos" || ftMethod === "near";
7119
+ if (needsFeePayer && !accept.extra?.feePayer) {
7120
+ throw new SettlementError(
7121
+ `exact settle: the ${ftMethod} facilitator rail is missing extra.feePayer (the gas sponsor) \u2014 cannot settle.`
7122
+ );
7123
+ }
7124
+ result = await settleViaFacilitator({
7125
+ url: mode.url,
7126
+ ...mode.authHeaders ? { authHeaders: mode.authHeaders } : {},
7127
+ // PipRail always builds a v2-shaped paymentRequirements (CAIP-2 network + `amount`),
7128
+ // so force x402Version:2 — echoing a v1 client's version here would hand the facilitator
7129
+ // a self-inconsistent request (v1 envelope, v2 requirements). The inner payload is
7130
+ // byte-identical across versions, so forwarding it verbatim is fine.
7131
+ x402Version: 2,
7132
+ paymentPayload: exact.raw,
7133
+ paymentRequirements: {
7134
+ scheme: "exact",
7135
+ network: accept.network,
7136
+ asset: accept.asset,
7137
+ amount: accept.amount,
7138
+ payTo: accept.payTo,
7139
+ maxTimeoutSeconds: accept.maxTimeoutSeconds,
7140
+ extra: needsFeePayer ? { feePayer: accept.extra.feePayer } : { name: accept.extra?.name ?? "", version: accept.extra?.version ?? "" }
7141
+ },
7142
+ receipt: { network: accept.network, asset: accept.asset, payTo: accept.payTo, amount: accept.amount },
7143
+ // From the merchant's own config, never the client's echo — same rule as `accept`.
7144
+ resource: {
7145
+ ...receiptResourceUrl ? { url: receiptResourceUrl } : {},
7146
+ ...options.description ? { description: options.description } : {},
7147
+ ...options.mimeType ? { mimeType: options.mimeType } : {}
7148
+ },
7149
+ // The buyer address, for the receipt's `payer` fallback. EVM carries it in the
7150
+ // authorization; SVM doesn't (the facilitator returns the settled payer) → omit it.
7151
+ ...evmAuth ? { payerHint: evmAuth.from } : {}
7152
+ });
7153
+ }
6248
7154
  } catch (err) {
6249
- reportOnPaidError(err, receipt);
6250
- return;
7155
+ await settleTx(nonce, false);
7156
+ throw err;
6251
7157
  }
6252
- if (outcome != null && typeof outcome.then === "function") {
6253
- return Promise.resolve(outcome).catch((err) => reportOnPaidError(err, receipt));
7158
+ if (!result.ok) {
7159
+ await settleTx(nonce, false);
7160
+ return rejection(result.error, result.detail);
6254
7161
  }
7162
+ await settleTx(nonce, true);
7163
+ await deliverOnPaid(spec, result.receipt);
7164
+ return await buildPaidResult(spec, result.receipt);
6255
7165
  }
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
- }
7166
+ function resolveSettleAmount(raw, maxAmount, decimals) {
7167
+ if (typeof raw === "bigint") return raw < 0n ? null : raw;
7168
+ const s = raw.trim();
7169
+ if (s.length === 0) return null;
6265
7170
  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) };
7171
+ if (s.endsWith("%")) {
7172
+ const pct = s.slice(0, -1).trim();
7173
+ if (!/^\d+(\.\d+)?$/.test(pct)) return null;
7174
+ const pctScaled = floorUnits(pct, 4);
7175
+ return maxAmount * pctScaled / (100n * 10n ** 4n);
7176
+ }
7177
+ if (s.startsWith("$")) {
7178
+ const amt = s.slice(1).trim();
7179
+ return floorUnits(amt, decimals);
7180
+ }
7181
+ if (/^\d+$/.test(s)) return BigInt(s);
7182
+ return floorUnits(s, decimals);
6280
7183
  } catch {
6281
- return { kind: "paid", receipt, receiptHeader: buildReceiptHeader(receipt) };
7184
+ return null;
6282
7185
  }
6283
7186
  }
6284
- async function maybeSignAttestation(spec, stamped) {
6285
- if (attestJws) {
6286
- warnAttestDegrade("JWS attestation is not yet implemented (R3)");
6287
- return void 0;
7187
+ async function verifyUpto(upto) {
7188
+ const specs = await ready();
7189
+ const uptoSpecs = specs.filter((s) => s.upto);
7190
+ if (uptoSpecs.length === 0) {
7191
+ return rejection("transfer_not_found", "This resource offers no standard `upto` (metered) rail.");
6288
7192
  }
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;
7193
+ const isCaip = upto.network.includes(":");
7194
+ const wantUptoNet = normalizeNetwork(upto.network);
7195
+ let candidates = isCaip ? uptoSpecs.filter((s) => normalizeNetwork(s.net.network) === wantUptoNet) : uptoSpecs;
7196
+ if (upto.asset) {
7197
+ candidates = candidates.filter((s) => s.asset.toLowerCase() === upto.asset.toLowerCase());
6293
7198
  }
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;
7199
+ let spec = candidates[0];
7200
+ if (!isCaip && !upto.asset && uptoSpecs.length > 1) spec = void 0;
7201
+ if (!spec && !isCaip && !upto.asset && uptoSpecs.length === 1) spec = uptoSpecs[0];
7202
+ if (!spec || !spec.upto) {
7203
+ return rejection(
7204
+ "transfer_not_found",
7205
+ `No \`upto\` rail offered for ${upto.network}${upto.asset ? `/${upto.asset}` : ""} (offered: ${uptoSpecs.map((s) => `${s.asset}@${s.net.network}`).join(", ")}).`
7206
+ );
6308
7207
  }
6309
- }
6310
- function reportOnFailedError(error, failure) {
6311
- if (!options.onFailedError) return;
6312
- try {
6313
- options.onFailedError(error, failure);
6314
- } catch {
7208
+ const nonce = upto.payload.permit2Authorization.nonce;
7209
+ if (await claimTx(nonce)) {
7210
+ return rejection("tx_already_used", `Authorization nonce ${nonce} was already redeemed.`);
6315
7211
  }
6316
- }
6317
- function fireOnFailed(failure) {
6318
- if (!options.onFailed) return;
6319
- let outcome;
7212
+ const accept = buildUptoAccept(spec);
7213
+ const relayer = spec.upto.relayer;
7214
+ let result;
6320
7215
  try {
6321
- outcome = options.onFailed(failure);
7216
+ const rawAmount = await options.upto.settleAmount({
7217
+ maxAmount: spec.amountBase,
7218
+ asset: spec.asset,
7219
+ network: spec.net.network,
7220
+ decimals: spec.decimals,
7221
+ ...upto.raw ? { request: upto.raw } : {}
7222
+ });
7223
+ const settleAmount = resolveSettleAmount(rawAmount, spec.amountBase, spec.decimals);
7224
+ if (settleAmount === null) {
7225
+ await settleTx(nonce, false);
7226
+ return rejection(
7227
+ "upto_settle_exceeds_max",
7228
+ `The settleAmount callback returned an unparseable amount (${String(rawAmount)}). Return a bigint, a raw/"NN%"/"$X" string, or 0.`
7229
+ );
7230
+ }
7231
+ result = await spec.net.settleUptoSelf({ relayer, payload: upto.payload, accept, settleAmount });
6322
7232
  } catch (err) {
6323
- reportOnFailedError(err, failure);
6324
- return;
7233
+ await settleTx(nonce, false);
7234
+ if (err instanceof SettlementError) throw err;
7235
+ return rejection(
7236
+ "tx_reverted",
7237
+ `upto: metering/settle failed (${err instanceof Error ? err.message : String(err)}).`
7238
+ );
6325
7239
  }
6326
- if (outcome != null && typeof outcome.then === "function") {
6327
- return Promise.resolve(outcome).catch((err) => reportOnFailedError(err, failure));
7240
+ if (!result.ok) {
7241
+ await settleTx(nonce, false);
7242
+ return rejection(result.error, result.detail);
6328
7243
  }
7244
+ await settleTx(nonce, true);
7245
+ await deliverOnPaid(spec, result.receipt);
7246
+ return await buildPaidResult(spec, result.receipt);
6329
7247
  }
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);
7248
+ async function verify(paymentSignature) {
7249
+ const raw = normaliseHeader(paymentSignature);
7250
+ const result = await resolveVerdictObject(raw === void 0 ? void 0 : decodeBase64Json(raw));
7251
+ if (result.kind === "invalid") await deliverOnFailed(result);
7252
+ return result;
7253
+ }
7254
+ async function verifyObject(payload) {
7255
+ const result = await resolveVerdictObject(payload);
7256
+ if (result.kind === "invalid") await deliverOnFailed(result);
7257
+ return result;
6335
7258
  }
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;
7259
+ async function routeVerdictObject(obj) {
7260
+ if (obj === void 0 || obj === null) return asChallenge();
7261
+ const sig = parseSignatureObject(obj);
7262
+ if (sig && sig.accepted && typeof sig.accepted.network === "string" && typeof sig.accepted.asset === "string") {
7263
+ return verifyOnchainProof(sig);
6341
7264
  }
6342
- return Object.keys(out).length > 0 ? { extra: out } : {};
7265
+ const upto = parseUptoObject(obj);
7266
+ if (upto) return verifyUpto(upto);
7267
+ const exact = parseExactObject(obj);
7268
+ if (exact) return verifyExact(exact);
7269
+ return asChallenge();
6343
7270
  }
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 } : {}
7271
+ function echoPaymentIdentifier(result, id) {
7272
+ try {
7273
+ const decoded = decodeBase64Json(result.receiptHeader);
7274
+ if (!decoded) return result;
7275
+ const { extensions: existing, ...receiptOnly } = decoded;
7276
+ const merged = {
7277
+ ...existing ?? {},
7278
+ [EXT_PAYMENT_IDENTIFIER]: { info: { required: false, id } }
6357
7279
  };
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 });
7280
+ return { ...result, receiptHeader: buildReceiptHeader(receiptOnly, merged) };
7281
+ } catch {
7282
+ return result;
6361
7283
  }
6362
- return {
6363
- url: resourceUrl,
6364
- ...options.description ? { description: options.description } : {},
6365
- ...options.mimeType ? { mimeType: options.mimeType } : {},
6366
- accepts
6367
- };
6368
7284
  }
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) {
7285
+ async function resolveVerdictObject(obj) {
7286
+ if (!options.paymentIdentifier) return routeVerdictObject(obj);
7287
+ const id = readPaymentIdentifier(obj);
7288
+ if (id !== null && typeof id === "object") {
7289
+ return rejection("signature_invalid", `payment-identifier: ${id.invalid}.`);
7290
+ }
7291
+ if (id === null) return routeVerdictObject(obj);
7292
+ const idKey = "pid:" + id;
7293
+ if (await claimTx(idKey)) {
6375
7294
  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(", ")}).`
7295
+ "tx_already_used",
7296
+ `Idempotency id "${id}" is already bound to a settled payment; use a fresh id for a new payment.`
6378
7297
  );
6379
7298
  }
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
7299
  let result;
6383
7300
  try {
6384
- result = await spec.net.verify(ref, buildAccept(spec, sig.payload.nonce));
7301
+ result = await routeVerdictObject(obj);
6385
7302
  } catch (err) {
6386
- await settleTx(ref, false);
7303
+ await settleTx(idKey, false);
6387
7304
  throw err;
6388
7305
  }
6389
- if (!result.ok) {
6390
- await settleTx(ref, false);
6391
- return rejection(result.error, result.detail);
7306
+ await settleTx(idKey, result.kind === "paid");
7307
+ return result.kind === "paid" ? echoPaymentIdentifier(result, id) : result;
7308
+ }
7309
+ async function selfTest() {
7310
+ try {
7311
+ const specs = await ready();
7312
+ const warnings = [];
7313
+ const rails = specs.map((s) => {
7314
+ if (!s.symbol) {
7315
+ warnings.push(
7316
+ `${s.asset} on ${s.net.network}: custom token (no built-in symbol) \u2014 double-check the address + decimals.`
7317
+ );
7318
+ }
7319
+ return {
7320
+ network: s.net.network,
7321
+ asset: s.asset,
7322
+ ...s.symbol ? { symbol: s.symbol } : {},
7323
+ decimals: s.decimals,
7324
+ amount: s.amountFormatted,
7325
+ payTo: s.payTo,
7326
+ schemes: [
7327
+ ...s.exact ? ["exact"] : [],
7328
+ ...s.upto ? ["upto"] : [],
7329
+ "onchain-proof"
7330
+ ]
7331
+ };
7332
+ });
7333
+ return { ok: true, rails, warnings };
7334
+ } catch (err) {
7335
+ return { ok: false, rails: [], warnings: [], error: err instanceof Error ? err.message : String(err) };
6392
7336
  }
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.`);
7337
+ }
7338
+ return { challenge, verify, verifyObject, describe, landingPage, selfTest };
7339
+ }
7340
+ function requirePayment(options) {
7341
+ if (options.upto) {
7342
+ throw new class extends PipRailError {
7343
+ code = "UNSUPPORTED_SCHEME";
7344
+ }(
7345
+ "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."
7346
+ );
7347
+ }
7348
+ const gate = createPaymentGate(options);
7349
+ return async (req, res, next) => {
7350
+ let result;
7351
+ try {
7352
+ result = await gate.verify(req.headers[HEADER_SIGNATURE] ?? req.headers[HEADER_SIGNATURE_V1]);
7353
+ } catch (err) {
7354
+ if (err instanceof SettlementError) {
7355
+ res.status(502);
7356
+ res.json({
7357
+ x402Version: 2,
7358
+ error: "settlement_failed",
7359
+ detail: err.message,
7360
+ 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."
7361
+ });
7362
+ return;
7363
+ }
7364
+ next(err);
7365
+ return;
7366
+ }
7367
+ switch (result.kind) {
7368
+ case "paid":
7369
+ res.setHeader(HEADER_RESPONSE, result.receiptHeader);
7370
+ res.setHeader(HEADER_RESPONSE_V1, result.receiptHeader);
7371
+ return next();
7372
+ case "challenge":
7373
+ res.setHeader(HEADER_REQUIRED, result.requiredHeader);
7374
+ res.status(result.statusCode);
7375
+ res.json(result.challenge);
7376
+ return;
7377
+ case "invalid":
7378
+ res.setHeader(HEADER_REQUIRED, result.requiredHeader);
7379
+ res.status(result.statusCode);
7380
+ res.json(result.challenge);
7381
+ return;
7382
+ }
7383
+ };
7384
+ }
7385
+ function normaliseHeader(value) {
7386
+ if (Array.isArray(value)) return value[0];
7387
+ return value;
7388
+ }
7389
+
7390
+ // src/agent.ts
7391
+ var OPEN_OBJECT = { type: "object", additionalProperties: true };
7392
+ async function readBody(res) {
7393
+ const text = await res.text();
7394
+ if (!text) return null;
7395
+ try {
7396
+ return JSON.parse(text);
7397
+ } catch {
7398
+ return text;
7399
+ }
7400
+ }
7401
+ function canSwap(client) {
7402
+ return client.canAgentSwap?.() === true && typeof client.quoteSwap === "function";
7403
+ }
7404
+ function canSell(client) {
7405
+ return client.canAgentSell?.() === true && typeof client.address === "function";
7406
+ }
7407
+ function nonceIn(payload) {
7408
+ if (typeof payload !== "object" || payload === null) return void 0;
7409
+ const p = payload;
7410
+ const inner = p.payload;
7411
+ const fromPayload = inner && typeof inner.nonce === "string" ? inner.nonce : void 0;
7412
+ const accepted = p.accepted;
7413
+ const fromAccept = typeof accepted?.extra?.nonce === "string" ? accepted.extra.nonce : void 0;
7414
+ return fromPayload ?? fromAccept;
7415
+ }
7416
+ function toToolError(err) {
7417
+ if (!(err instanceof PipRailError)) throw err;
7418
+ const out = {
7419
+ ok: false,
7420
+ code: err.code,
7421
+ reason: err.message,
7422
+ explain: explainDecline(err)
7423
+ };
7424
+ if (err instanceof PaymentDeclinedError) {
7425
+ out.declined = true;
7426
+ if (err.reasonCode) out.reasonCode = err.reasonCode;
7427
+ }
7428
+ const ref = err.ref;
7429
+ if (typeof ref === "string") out.ref = ref;
7430
+ return out;
7431
+ }
7432
+ function paymentTools(client) {
7433
+ const base2 = [
7434
+ {
7435
+ name: "piprail_discover",
7436
+ 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.`,
7437
+ annotations: {
7438
+ title: "Discover payable x402 APIs",
7439
+ readOnlyHint: true,
7440
+ // reads the open indexes only; never pays
7441
+ openWorldHint: true
7442
+ // reaches external indexes (402 Index, CDP Bazaar)
7443
+ },
7444
+ parameters: {
7445
+ type: "object",
7446
+ properties: {
7447
+ query: {
7448
+ type: "string",
7449
+ 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.'
7450
+ },
7451
+ network: {
7452
+ type: "string",
7453
+ description: "CAIP-2 id, 'self' (your chain \u2014 default), or 'any' (all chains)."
7454
+ },
7455
+ category: { type: "string", description: "Keep ONLY this category, e.g. 'ai', 'finance', 'data' (strict)." },
7456
+ asset: { type: "string", description: "Keep only resources paying in this token symbol, e.g. 'USDC'." },
7457
+ maxPrice: { type: "number", description: "Drop results advertised above this USD price." },
7458
+ minReliability: { type: "number", description: "Drop results below this health score (0\u2013100); unscored pass." },
7459
+ verified: { type: "boolean", description: "Prefer verified listings (402 Index)." },
7460
+ sort: {
7461
+ type: "string",
7462
+ enum: ["relevance", "reliability", "price", "uptime", "name"],
7463
+ description: "Ordering. Default 'relevance' with a query, else first-seen."
7464
+ },
7465
+ limit: { type: "number", description: "Max results to fetch per index (default 20)." }
7466
+ },
7467
+ additionalProperties: false
7468
+ },
7469
+ invoke: async (args) => {
7470
+ try {
7471
+ const opts = {};
7472
+ if (typeof args.query === "string") opts.query = args.query;
7473
+ if (typeof args.network === "string") opts.network = args.network;
7474
+ if (typeof args.category === "string") opts.category = args.category;
7475
+ if (typeof args.asset === "string") opts.asset = args.asset;
7476
+ if (typeof args.maxPrice === "number") opts.maxPrice = args.maxPrice;
7477
+ if (typeof args.minReliability === "number") opts.minReliability = args.minReliability;
7478
+ if (typeof args.verified === "boolean") opts.verified = args.verified;
7479
+ if (typeof args.sort === "string") opts.sort = args.sort;
7480
+ if (typeof args.limit === "number") opts.limit = args.limit;
7481
+ const found = await client.discover(opts);
7482
+ return {
7483
+ count: found.length,
7484
+ resources: found.map((r) => ({
7485
+ resource: r.resource,
7486
+ name: r.name,
7487
+ description: r.description,
7488
+ source: r.source,
7489
+ category: r.category,
7490
+ priceUsd: r.priceUsd,
7491
+ reliabilityScore: r.reliabilityScore,
7492
+ health: r.health,
7493
+ verified: r.verified,
7494
+ networks: [...new Set(r.rails.map((rail) => rail.network))]
7495
+ }))
7496
+ };
7497
+ } catch (err) {
7498
+ return toToolError(err);
7499
+ }
6398
7500
  }
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) => {
7501
+ },
7502
+ {
7503
+ name: "piprail_quote_payment",
7504
+ 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.",
7505
+ annotations: {
7506
+ title: "Quote an x402 price",
7507
+ readOnlyHint: true,
7508
+ // reads the 402 challenge; never pays
7509
+ openWorldHint: true
7510
+ // fetches an arbitrary URL
7511
+ },
7512
+ parameters: {
7513
+ type: "object",
7514
+ properties: {
7515
+ url: { type: "string", description: "Full URL of the gated resource." }
7516
+ },
7517
+ required: ["url"],
7518
+ additionalProperties: false
7519
+ },
7520
+ outputSchema: OPEN_OBJECT,
7521
+ invoke: async (args) => {
6432
7522
  try {
6433
- return Buffer.from(t, "base64").toString("base64");
6434
- } catch {
6435
- return t;
7523
+ const quote = await client.quote(String(args.url));
7524
+ return quote ? { gated: true, ...quote } : { gated: false, url: String(args.url) };
7525
+ } catch (err) {
7526
+ return toToolError(err);
6436
7527
  }
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
7528
  }
6444
- } else if ("paymentGroup" in exact.payload) {
6445
- nonce = exact.payload.paymentGroup.map((t) => {
7529
+ },
7530
+ {
7531
+ name: "piprail_plan_payment",
7532
+ 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.",
7533
+ annotations: {
7534
+ title: "Plan an x402 payment",
7535
+ readOnlyHint: true,
7536
+ // reads balances + the challenge; never pays
7537
+ openWorldHint: true
7538
+ // fetches a URL and reads chain state
7539
+ },
7540
+ parameters: {
7541
+ type: "object",
7542
+ properties: {
7543
+ url: { type: "string", description: "Full URL of the gated resource." }
7544
+ },
7545
+ required: ["url"],
7546
+ additionalProperties: false
7547
+ },
7548
+ outputSchema: OPEN_OBJECT,
7549
+ invoke: async (args) => {
6446
7550
  try {
6447
- return Buffer.from(t, "base64").toString("base64");
6448
- } catch {
6449
- return t;
7551
+ const plan = await client.planPayment(String(args.url));
7552
+ if (plan == null) return { gated: false, url: String(args.url) };
7553
+ return {
7554
+ gated: true,
7555
+ payable: plan.payable,
7556
+ status: plan.status,
7557
+ fundingHint: plan.fundingHint,
7558
+ // One model-readable line distilling the whole plan.
7559
+ summary: summarizePlan(plan),
7560
+ best: plan.best ? {
7561
+ network: plan.best.accept.network,
7562
+ symbol: plan.best.quote.symbol,
7563
+ amount: plan.best.quote.amountFormatted,
7564
+ gasCoin: plan.best.cost.feeSymbol,
7565
+ gas: plan.best.cost.feeFormatted
7566
+ } : null,
7567
+ options: plan.options.map((o) => ({
7568
+ network: o.accept.network,
7569
+ symbol: o.quote.symbol,
7570
+ amount: o.quote.amountFormatted,
7571
+ state: o.state,
7572
+ blockers: o.blockers,
7573
+ warnings: o.warnings,
7574
+ recipientReady: o.recipient.ready
7575
+ })),
7576
+ // The session's time leash, present only when a time policy is configured.
7577
+ ...plan.session ? { session: plan.session } : {}
7578
+ };
7579
+ } catch (err) {
7580
+ return toToolError(err);
7581
+ }
7582
+ }
7583
+ },
7584
+ {
7585
+ name: "piprail_pay_request",
7586
+ 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.",
7587
+ annotations: {
7588
+ title: "Pay an x402 request",
7589
+ readOnlyHint: false,
7590
+ // this is the one tool that MOVES FUNDS
7591
+ destructiveHint: true,
7592
+ // a payment is value-moving and not reversible
7593
+ idempotentHint: false,
7594
+ // paying twice = two payments
7595
+ openWorldHint: true
7596
+ // fetches a URL and settles a payment
7597
+ },
7598
+ parameters: {
7599
+ type: "object",
7600
+ properties: {
7601
+ url: { type: "string", description: "Full URL to fetch." },
7602
+ method: { type: "string", description: "HTTP method, default 'GET'." },
7603
+ body: {
7604
+ type: ["object", "string"],
7605
+ description: "Optional request body for POST/PUT (a JSON object or a string)."
7606
+ }
7607
+ },
7608
+ required: ["url"],
7609
+ additionalProperties: false
7610
+ },
7611
+ invoke: async (args) => {
7612
+ const url = String(args.url);
7613
+ const method = (args.method ? String(args.method) : "GET").toUpperCase();
7614
+ try {
7615
+ let res;
7616
+ if (method === "GET") {
7617
+ res = await client.get(url);
7618
+ } else {
7619
+ const headers = {};
7620
+ let body;
7621
+ if (args.body !== void 0 && args.body !== null) {
7622
+ if (typeof args.body === "string") {
7623
+ body = args.body;
7624
+ } else {
7625
+ body = JSON.stringify(args.body);
7626
+ headers["content-type"] = "application/json";
7627
+ }
7628
+ }
7629
+ res = await client.fetch(url, { method, headers, body });
7630
+ }
7631
+ const verifiable = parseReceiptExtension(res);
7632
+ return {
7633
+ status: res.status,
7634
+ ok: res.ok,
7635
+ body: await readBody(res),
7636
+ receipt: parseReceipt(res),
7637
+ ...verifiable ? { verifiableReceipt: { ...verifiable, resource: { url } } } : {}
7638
+ };
7639
+ } catch (err) {
7640
+ if (err instanceof PipRailError) {
7641
+ const out = {
7642
+ ok: false,
7643
+ code: err.code,
7644
+ reason: err.message,
7645
+ explain: explainDecline(err)
7646
+ };
7647
+ if (err instanceof PaymentDeclinedError) {
7648
+ out.declined = true;
7649
+ if (err.reasonCode) out.reasonCode = err.reasonCode;
7650
+ }
7651
+ const ref = err.ref;
7652
+ if (typeof ref === "string") out.ref = ref;
7653
+ return out;
7654
+ }
7655
+ throw err;
7656
+ }
7657
+ }
7658
+ },
7659
+ {
7660
+ name: "piprail_register",
7661
+ 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.",
7662
+ annotations: {
7663
+ title: "Register an x402 endpoint",
7664
+ readOnlyHint: false,
7665
+ // writes a listing to an external index
7666
+ destructiveHint: false,
7667
+ // adds a listing; nothing is destroyed and no funds move
7668
+ openWorldHint: true
7669
+ // posts to external indexes (402 Index)
7670
+ // idempotentHint intentionally omitted — index dedup behaviour isn't guaranteed.
7671
+ },
7672
+ parameters: {
7673
+ type: "object",
7674
+ properties: {
7675
+ url: { type: "string", description: "Full URL of the resource to list." },
7676
+ name: { type: "string", description: "Display name (defaults to the host)." },
7677
+ description: {
7678
+ type: "string",
7679
+ 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."
7680
+ },
7681
+ category: { type: "string", description: "A category, e.g. 'ai', 'finance', 'data' \u2014 the top findability field (most listings have none)." },
7682
+ tags: { type: "array", items: { type: "string" }, description: "Keywords; folded into the description so they're searchable." },
7683
+ priceUsd: { type: "number", description: "Advertised price in USD (metadata)." },
7684
+ network: {
7685
+ type: "string",
7686
+ 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."
7687
+ },
7688
+ asset: { type: "string", description: "Payment asset symbol, e.g. 'USDC' (metadata)." },
7689
+ provider: { type: "string", description: "Who runs the resource (provider/org name)." },
7690
+ contactEmail: { type: "string", description: "Contact email for the listing." }
7691
+ },
7692
+ required: ["url"],
7693
+ additionalProperties: false
7694
+ },
7695
+ invoke: async (args) => {
7696
+ try {
7697
+ const opts = {};
7698
+ if (typeof args.name === "string") opts.name = args.name;
7699
+ if (typeof args.description === "string") opts.description = args.description;
7700
+ if (typeof args.category === "string") opts.category = args.category;
7701
+ if (Array.isArray(args.tags)) opts.tags = args.tags.filter((t) => typeof t === "string");
7702
+ if (typeof args.priceUsd === "number") opts.priceUsd = args.priceUsd;
7703
+ if (typeof args.network === "string") opts.network = args.network;
7704
+ if (typeof args.asset === "string") opts.asset = args.asset;
7705
+ if (typeof args.provider === "string") opts.provider = args.provider;
7706
+ if (typeof args.contactEmail === "string") opts.contactEmail = args.contactEmail;
7707
+ const outcomes = await client.register(String(args.url), opts);
7708
+ return { outcomes };
7709
+ } catch (err) {
7710
+ return toToolError(err);
6450
7711
  }
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
7712
  }
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
- );
7713
+ },
7714
+ {
7715
+ name: "piprail_budget",
7716
+ 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.",
7717
+ annotations: {
7718
+ title: "Check remaining budget",
7719
+ readOnlyHint: true,
7720
+ // reads the ledger + policy; never pays
7721
+ idempotentHint: true
7722
+ // a pure read
7723
+ },
7724
+ parameters: { type: "object", properties: {}, additionalProperties: false },
7725
+ outputSchema: OPEN_OBJECT,
7726
+ invoke: async () => {
7727
+ try {
7728
+ const spent = client.spent();
7729
+ const budget = client.budget();
7730
+ return {
7731
+ spent,
7732
+ remaining: budget.byAsset,
7733
+ grandTotal: budget.byDenom,
7734
+ // cross-token spend cap per denomination (USD/EUR/…)
7735
+ counts: budget.counts,
7736
+ // payment-count leash (settled + lifetime/window caps)
7737
+ session: budget.session,
7738
+ policy: client.policy() ?? null,
7739
+ // the configured leash, read back
7740
+ report: formatSpendReport(spent)
7741
+ };
7742
+ } catch (err) {
7743
+ return toToolError(err);
6483
7744
  }
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
7745
  }
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;
7746
+ },
7747
+ {
7748
+ name: "piprail_guide",
7749
+ 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.",
7750
+ annotations: {
7751
+ title: "How to use PipRail",
7752
+ readOnlyHint: true,
7753
+ idempotentHint: true
7754
+ },
7755
+ parameters: { type: "object", properties: {}, additionalProperties: false },
7756
+ invoke: async () => ({ guide: PIPRAIL_AGENT_GUIDE })
7757
+ },
7758
+ {
7759
+ name: "piprail_verify_receipt",
7760
+ 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.",
7761
+ annotations: {
7762
+ title: "Verify a payment receipt",
7763
+ readOnlyHint: true,
7764
+ // re-reads the chain; moves nothing, needs no wallet
7765
+ idempotentHint: true,
7766
+ openWorldHint: true
7767
+ // reads an on-chain tx via RPC
7768
+ },
7769
+ parameters: {
7770
+ type: "object",
7771
+ properties: {
7772
+ receipt: {
7773
+ type: "object",
7774
+ description: "The PipRailReceipt JSON ({ piprail, receipt, resource, decimals? }) to re-verify."
7775
+ },
7776
+ rpcUrl: {
7777
+ type: "string",
7778
+ description: "Optional RPC URL for the receipt's chain (required for chains outside the common presets)."
7779
+ }
7780
+ },
7781
+ required: ["receipt"],
7782
+ additionalProperties: false
7783
+ },
7784
+ invoke: async (args) => {
7785
+ const receipt = args.receipt;
7786
+ const opts = args.rpcUrl ? { rpcUrl: String(args.rpcUrl) } : void 0;
7787
+ return await PipRailClient.verifyReceipt(receipt, opts);
7788
+ }
6659
7789
  }
6660
- await settleTx(idKey, result.kind === "paid");
6661
- return result.kind === "paid" ? echoPaymentIdentifier(result, id) : result;
7790
+ ];
7791
+ const tools = [...base2];
7792
+ if (canSwap(client)) {
7793
+ tools.push(
7794
+ {
7795
+ name: "piprail_quote_swap",
7796
+ 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.',
7797
+ annotations: {
7798
+ title: "Price a token swap",
7799
+ readOnlyHint: true,
7800
+ idempotentHint: true,
7801
+ openWorldHint: true
7802
+ },
7803
+ parameters: {
7804
+ type: "object",
7805
+ properties: {
7806
+ from: { type: "string", description: "Token you HOLD and will spend (symbol, address, or 'native')." },
7807
+ to: { type: "string", description: "Token you NEED (symbol, address, or 'native')." },
7808
+ wantAmount: { type: "string", description: 'How much of `to` you need, in human units (e.g. "0.50").' },
7809
+ slippageBps: {
7810
+ type: "number",
7811
+ description: "Optional tolerance in basis points (default 50 = 0.5%). Refused if above your swapPolicy cap."
7812
+ }
7813
+ },
7814
+ required: ["from", "to", "wantAmount"],
7815
+ additionalProperties: false
7816
+ },
7817
+ invoke: async (args) => {
7818
+ const a = args;
7819
+ try {
7820
+ const quote = await client.quoteSwap?.({
7821
+ from: a.from,
7822
+ to: a.to,
7823
+ wantAmount: a.wantAmount,
7824
+ slippageBps: a.slippageBps
7825
+ });
7826
+ if (!quote) {
7827
+ return {
7828
+ ok: false,
7829
+ reason: "no_route",
7830
+ 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."
7831
+ };
7832
+ }
7833
+ return {
7834
+ ok: true,
7835
+ from: { symbol: quote.from.symbol, amount: quote.from.amountFormatted },
7836
+ to: { symbol: quote.to.symbol, amount: quote.to.amountFormatted },
7837
+ maxSpend: quote.maxSpendFormatted,
7838
+ slippageBps: quote.slippageBps,
7839
+ source: { kind: quote.source.kind, name: quote.source.name, note: quote.source.note },
7840
+ summary: summarizeSwap(quote),
7841
+ quote
7842
+ };
7843
+ } catch (err) {
7844
+ return toToolError(err);
7845
+ }
7846
+ }
7847
+ },
7848
+ {
7849
+ name: "piprail_swap",
7850
+ 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.",
7851
+ annotations: {
7852
+ title: "Swap tokens",
7853
+ readOnlyHint: false,
7854
+ destructiveHint: true,
7855
+ idempotentHint: false,
7856
+ openWorldHint: true
7857
+ },
7858
+ parameters: {
7859
+ type: "object",
7860
+ properties: {
7861
+ quote: { type: "object", description: "The `quote` object returned by piprail_quote_swap, unmodified." }
7862
+ },
7863
+ required: ["quote"],
7864
+ additionalProperties: false
7865
+ },
7866
+ invoke: async (args) => {
7867
+ const a = args;
7868
+ try {
7869
+ const receipt = await client.swap?.(a.quote);
7870
+ if (!receipt) return { ok: false, reason: "unsupported", explain: "This client cannot swap." };
7871
+ return {
7872
+ ok: true,
7873
+ transaction: receipt.transaction,
7874
+ network: receipt.network,
7875
+ from: { symbol: receipt.from.symbol, amount: receipt.from.amountFormatted },
7876
+ to: { symbol: receipt.to.symbol, amount: receipt.to.amountFormatted },
7877
+ source: receipt.source.name
7878
+ };
7879
+ } catch (err) {
7880
+ return toToolError(err);
7881
+ }
7882
+ }
7883
+ }
7884
+ );
6662
7885
  }
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
- );
7886
+ if (!canSell(client)) return tools;
7887
+ const offers = /* @__PURE__ */ new Map();
7888
+ const spentProofs = /* @__PURE__ */ new Set();
7889
+ const offerOr = (id) => {
7890
+ const found = typeof id === "string" ? offers.get(id) : void 0;
7891
+ return found ?? {
7892
+ ok: false,
7893
+ 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.`,
7894
+ offers: [...offers.keys()]
7895
+ };
7896
+ };
7897
+ tools.push(
7898
+ {
7899
+ name: "piprail_sell",
7900
+ 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.",
7901
+ annotations: {
7902
+ title: "Sell something",
7903
+ readOnlyHint: false,
7904
+ // creates an offer in this session and advertises an address to be paid at
7905
+ destructiveHint: false,
7906
+ // moves no funds; receiving cannot spend
7907
+ idempotentHint: false,
7908
+ // each call mints a NEW offer with its own nonce
7909
+ openWorldHint: false
7910
+ // publishes nowhere — piprail_register is what lists an offer publicly
7911
+ },
7912
+ parameters: {
7913
+ type: "object",
7914
+ properties: {
7915
+ description: {
7916
+ type: "string",
7917
+ 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."
7918
+ },
7919
+ price: { type: "string", description: "The price you are charging, human-readable, e.g. '2.50'." },
7920
+ token: { type: "string", description: "What to be paid in. Defaults to 'USDC'. Use 'native' for the chain's own coin." },
7921
+ chain: { type: "string", description: "Which chain to be paid on. Defaults to the chain this wallet is on." },
7922
+ payTo: {
7923
+ type: "string",
7924
+ 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."
7925
+ },
7926
+ resource: { type: "string", description: "Optional identifier or URL for the thing being sold; a label is minted if omitted." }
7927
+ },
7928
+ required: ["description", "price"],
7929
+ additionalProperties: false
7930
+ },
7931
+ invoke: async (args) => {
7932
+ try {
7933
+ const description = typeof args.description === "string" ? args.description.trim() : "";
7934
+ const price = typeof args.price === "string" ? args.price.trim() : "";
7935
+ if (!description) return { ok: false, reason: "sell needs a `description` \u2014 it is what the buyer sees and what you owe them." };
7936
+ if (!price) return { ok: false, reason: "sell needs a `price`, human-readable, e.g. '2.50'." };
7937
+ const token = typeof args.token === "string" && args.token.trim() ? args.token.trim() : "USDC";
7938
+ const chain = typeof args.chain === "string" && args.chain.trim() ? args.chain.trim() : client.chain?.();
7939
+ if (chain === void 0) {
7940
+ return { ok: false, reason: "sell needs a `chain` \u2014 this wallet cannot report one of its own." };
7941
+ }
7942
+ let payTo;
7943
+ if (typeof args.payTo === "string" && args.payTo.trim()) {
7944
+ payTo = args.payTo.trim();
7945
+ } else if (typeof client.address === "function") {
7946
+ payTo = await client.address();
7947
+ } else {
7948
+ return { ok: false, reason: "sell needs a `payTo` \u2014 this wallet cannot report its own address." };
7949
+ }
7950
+ const id = `offer_${crypto.randomUUID().replace(/-/g, "").slice(0, 10)}`;
7951
+ const resource = typeof args.resource === "string" && args.resource.trim() ? args.resource.trim() : `piprail:${id}`;
7952
+ const base3 = { chain, token, amount: price, payTo, description, discovery: true };
7953
+ const railSchemes = (t) => [
7954
+ ...new Set((t.rails ?? []).flatMap((r) => [...r.schemes ?? []]))
7955
+ ];
7956
+ const shared = {
7957
+ isUsed: (ref) => spentProofs.has(ref),
7958
+ markUsed: (ref) => void spentProofs.add(ref)
7959
+ };
7960
+ let gate = createPaymentGate({ ...base3, ...shared, exact: true });
7961
+ let check = await gate.selfTest();
7962
+ const warnings = [];
7963
+ if (!check.ok || !railSchemes(check).includes("exact")) {
7964
+ const why = check.error ?? "it did not resolve on this RPC";
7965
+ gate = createPaymentGate({ ...base3, ...shared });
7966
+ check = await gate.selfTest();
7967
+ warnings.push(
7968
+ `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.`
7969
+ );
7970
+ }
7971
+ if (!check.ok) {
7972
+ return { ok: false, reason: check.error ?? "this offer could not be priced on that chain.", chain, token };
7973
+ }
7974
+ for (const w of check.warnings ?? []) warnings.push(String(w));
7975
+ const { challenge, requiredHeader } = await gate.challenge(resource);
7976
+ const schemes = railSchemes(check);
7977
+ const nonce = challenge.accepts.find((a) => a.scheme === "onchain-proof")?.extra?.nonce;
7978
+ const offer = {
7979
+ id,
7980
+ nonce: typeof nonce === "string" ? nonce : void 0,
7981
+ gate,
7982
+ description,
7983
+ price,
7984
+ token,
7985
+ chain,
7986
+ payTo,
7987
+ resource,
7988
+ schemes,
7989
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
7990
+ earnings: []
7991
+ };
7992
+ offers.set(id, offer);
7993
+ return {
7994
+ ok: true,
7995
+ offerId: id,
7996
+ description,
7997
+ price,
7998
+ token,
7999
+ chain,
8000
+ payTo,
8001
+ paidToYou: payTo === (typeof client.address === "function" ? await client.address().catch(() => void 0) : void 0),
8002
+ schemes,
8003
+ challenge,
8004
+ /*
8005
+ * The header an x402 402 must carry alongside the body. An agent serving its own
8006
+ * offer over HTTP needs BOTH halves, and omitting this quietly left it emitting a
8007
+ * non-conformant 402 that only a lenient buyer would pay.
8008
+ */
8009
+ requiredHeader,
8010
+ 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.`,
8011
+ ...warnings.length ? { warnings } : {}
8012
+ };
8013
+ } catch (err) {
8014
+ return toToolError(err);
8015
+ }
8016
+ }
8017
+ },
8018
+ {
8019
+ name: "piprail_collect",
8020
+ 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.",
8021
+ annotations: {
8022
+ title: "Collect a payment",
8023
+ readOnlyHint: false,
8024
+ // burns the proof into the offer's used set, so it cannot be redeemed twice
8025
+ destructiveHint: false,
8026
+ // moves no funds; it only reads the chain and records what it found
8027
+ idempotentHint: false,
8028
+ // deliberately NOT: a second call on the same proof is a replay and is refused
8029
+ openWorldHint: true
8030
+ // reads the settlement via RPC
8031
+ },
8032
+ parameters: {
8033
+ type: "object",
8034
+ properties: {
8035
+ offerId: { type: "string", description: "The offer being paid for, from piprail_sell." },
8036
+ payment: {
8037
+ type: "string",
8038
+ 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."
8039
+ }
8040
+ },
8041
+ required: ["offerId", "payment"],
8042
+ additionalProperties: false
8043
+ },
8044
+ invoke: async (args) => {
8045
+ try {
8046
+ const offer = offerOr(args.offerId);
8047
+ if ("ok" in offer) return offer;
8048
+ const raw = typeof args.payment === "string" ? args.payment.trim() : "";
8049
+ if (!raw) return { ok: false, reason: "collect needs the `payment` the buyer sent back." };
8050
+ const asObject = raw.startsWith("{") ? JSON.parse(raw) : void 0;
8051
+ const presented = nonceIn(asObject ?? decodeBase64Json(raw));
8052
+ if (offer.nonce && presented && presented !== offer.nonce) {
8053
+ return {
8054
+ ok: true,
8055
+ paid: false,
8056
+ offerId: offer.id,
8057
+ 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.",
8058
+ code: "wrong_offer",
8059
+ next: "Do NOT deliver. Ask the buyer to pay THIS offer's challenge."
8060
+ };
8061
+ }
8062
+ const result = asObject !== void 0 ? await offer.gate.verifyObject(asObject) : await offer.gate.verify(raw);
8063
+ if (result.kind === "paid") {
8064
+ const r = result.receipt;
8065
+ const entry = {
8066
+ at: (/* @__PURE__ */ new Date()).toISOString(),
8067
+ amount: String(r.amountFormatted ?? offer.price),
8068
+ symbol: String(r.symbol ?? offer.token),
8069
+ ...typeof r.payer === "string" ? { payer: r.payer } : {},
8070
+ ref: String(r.transaction ?? r.reference ?? "")
8071
+ };
8072
+ offer.earnings.push(entry);
8073
+ return {
8074
+ ok: true,
8075
+ paid: true,
8076
+ offerId: offer.id,
8077
+ earned: `${entry.amount} ${entry.symbol}`,
8078
+ receipt: result.receipt,
8079
+ next: `Payment is settled and on-chain. Deliver "${offer.description}" now. This proof is spent and cannot be collected again.`
8080
+ };
8081
+ }
8082
+ if (result.kind === "invalid") {
8083
+ return {
8084
+ ok: true,
8085
+ paid: false,
8086
+ offerId: offer.id,
8087
+ reason: result.detail || result.error,
8088
+ code: result.error,
8089
+ retryChallenge: result.challenge,
8090
+ next: "Do NOT deliver. Give the buyer `retryChallenge` if they want to try again."
8091
+ };
8092
+ }
8093
+ return {
8094
+ ok: true,
8095
+ paid: false,
8096
+ offerId: offer.id,
8097
+ reason: "no payment was presented \u2014 this is a fresh challenge, not a settlement.",
8098
+ retryChallenge: result.challenge,
8099
+ next: "Do NOT deliver. The buyer has not paid yet."
8100
+ };
8101
+ } catch (err) {
8102
+ if (err instanceof SyntaxError) {
8103
+ return { ok: false, reason: "the `payment` looked like JSON but would not parse. Pass the buyer's proof through unchanged." };
8104
+ }
8105
+ return toToolError(err);
8106
+ }
8107
+ }
8108
+ },
8109
+ {
8110
+ name: "piprail_earnings",
8111
+ 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.",
8112
+ annotations: {
8113
+ title: "Your earnings",
8114
+ readOnlyHint: true,
8115
+ // pure read of this session's collected payments
8116
+ idempotentHint: true,
8117
+ openWorldHint: false
8118
+ },
8119
+ parameters: { type: "object", properties: {}, additionalProperties: false },
8120
+ invoke: async () => {
8121
+ const list = [...offers.values()];
8122
+ const byAsset = {};
8123
+ for (const o of list) {
8124
+ for (const e of o.earnings) byAsset[e.symbol] = (byAsset[e.symbol] ?? 0) + Number(e.amount || 0);
6672
8125
  }
8126
+ const collected = list.reduce((n, o) => n + o.earnings.length, 0);
6673
8127
  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
- ]
8128
+ ok: true,
8129
+ offers: list.map((o) => ({
8130
+ offerId: o.id,
8131
+ description: o.description,
8132
+ price: `${o.price} ${o.token}`,
8133
+ chain: o.chain,
8134
+ payTo: o.payTo,
8135
+ schemes: o.schemes,
8136
+ timesPaid: o.earnings.length,
8137
+ earnings: o.earnings
8138
+ })),
8139
+ totals: Object.fromEntries(Object.entries(byAsset).map(([s, n]) => [s, String(n)])),
8140
+ collected,
8141
+ 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
8142
  };
6686
- });
6687
- return { ok: true, rails, warnings };
6688
- } catch (err) {
6689
- return { ok: false, rails: [], warnings: [], error: err instanceof Error ? err.message : String(err) };
8143
+ }
8144
+ },
8145
+ {
8146
+ name: "piprail_wallet",
8147
+ 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.",
8148
+ annotations: {
8149
+ title: "Your wallet",
8150
+ readOnlyHint: true,
8151
+ // reads addresses and balances; changes nothing
8152
+ idempotentHint: true,
8153
+ openWorldHint: true
8154
+ // balances come from RPC
8155
+ },
8156
+ parameters: {
8157
+ type: "object",
8158
+ properties: {
8159
+ assets: {
8160
+ type: "array",
8161
+ items: { type: "string" },
8162
+ 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."
8163
+ }
8164
+ },
8165
+ additionalProperties: false
8166
+ },
8167
+ invoke: async (args) => {
8168
+ try {
8169
+ const assets = Array.isArray(args.assets) && args.assets.length ? args.assets.filter((a) => typeof a === "string") : ["native", "USDC"];
8170
+ const address = typeof client.address === "function" ? await client.address().catch(() => null) : null;
8171
+ const holdings = typeof client.balanceOf === "function" ? await client.balanceOf(assets).catch(() => []) : [];
8172
+ const held = holdings.filter((h) => h.known && h.amount !== null && h.amount !== "0");
8173
+ const unknown = holdings.filter((h) => h.known && h.amount === null);
8174
+ return {
8175
+ ok: true,
8176
+ address,
8177
+ chain: typeof client.chain === "function" ? client.chain() : void 0,
8178
+ holdings,
8179
+ 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.` : ""),
8180
+ next: "To be paid here, give out `address`. piprail_budget is your spend leash; this is what you own."
8181
+ };
8182
+ } catch (err) {
8183
+ return toToolError(err);
8184
+ }
8185
+ }
6690
8186
  }
6691
- }
6692
- return { challenge, verify, verifyObject, describe, landingPage, selfTest };
8187
+ );
8188
+ return tools;
6693
8189
  }
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."
8190
+
8191
+ // src/classify.ts
8192
+ function classifyChallenge(challenge, opts) {
8193
+ try {
8194
+ const accepts = Array.isArray(challenge?.accepts) ? challenge.accepts : [];
8195
+ const network = opts?.network;
8196
+ const schemes = opts?.schemes ?? [];
8197
+ const offeredSchemes = [
8198
+ ...new Set(accepts.map((a) => a?.scheme).filter((s) => s != null))
8199
+ ];
8200
+ const offeredNetworks = [...new Set(accepts.map((a) => a?.network).filter((n) => n != null))];
8201
+ const onClientChain = accepts.some((a) => a?.network === network);
8202
+ const payableScheme = accepts.some(
8203
+ (a) => a?.network === network && schemes.includes(a?.scheme)
6700
8204
  );
8205
+ const verdict = accepts.length === 0 ? "NO_RAIL" : payableScheme ? "PAYABLE_RAIL" : onClientChain ? "UNPAYABLE_SCHEME" : "WRONG_CHAIN";
8206
+ return { onClientChain, payableScheme, offeredSchemes, offeredNetworks, verdict };
8207
+ } catch {
8208
+ return {
8209
+ onClientChain: false,
8210
+ payableScheme: false,
8211
+ offeredSchemes: [],
8212
+ offeredNetworks: [],
8213
+ verdict: "NO_RAIL"
8214
+ };
6701
8215
  }
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
8216
  }
6743
8217
 
6744
8218
  // src/merchant.ts
@@ -7287,11 +8761,14 @@ export {
7287
8761
  A2A_STATUS_KEY,
7288
8762
  A2A_X402_EXTENSION_URI_V01,
7289
8763
  A2A_X402_EXTENSION_URI_V02,
8764
+ AGENT_MODES,
7290
8765
  BRAND,
7291
8766
  BUILTIN_DENOMS,
7292
8767
  CHAINS,
7293
8768
  ConfirmationTimeoutError,
8769
+ DEFAULT_AGENT_MODE,
7294
8770
  DEFAULT_EXACT_TRANSFER_METHOD,
8771
+ DEFAULT_SLIPPAGE_BPS,
7295
8772
  DENOM_PRECISION,
7296
8773
  DIRECTORY_INFO,
7297
8774
  EIP3009_TYPES,
@@ -7309,6 +8786,7 @@ export {
7309
8786
  InvalidEnvelopeError,
7310
8787
  KNOWN_EXACT_TRANSFER_METHODS,
7311
8788
  KNOWN_FACILITATORS,
8789
+ MAX_SLIPPAGE_BPS,
7312
8790
  MCP_PAYMENT_META_KEY,
7313
8791
  MCP_PAYMENT_RESPONSE_META_KEY,
7314
8792
  MaxRetriesExceededError,
@@ -7328,6 +8806,7 @@ export {
7328
8806
  PipRailError,
7329
8807
  REGISTER_ATTRIBUTION,
7330
8808
  RecipientNotReadyError,
8809
+ SWAP_PROVIDERS,
7331
8810
  SettlementError,
7332
8811
  SpendLedger,
7333
8812
  UPTO_PROXY_CHAIN_IDS,
@@ -7343,6 +8822,7 @@ export {
7343
8822
  agentGuide,
7344
8823
  appendAttribution,
7345
8824
  appendKeywords,
8825
+ applySlippage,
7346
8826
  buildBazaarExtension,
7347
8827
  buildChallengeHeader,
7348
8828
  buildEndpointInfo,
@@ -7360,6 +8840,7 @@ export {
7360
8840
  buildWellKnownX402,
7361
8841
  buildWellKnownX402Manifest,
7362
8842
  buildX402DnsTxt,
8843
+ canSwapOn,
7363
8844
  chainIdForExactNetwork,
7364
8845
  claim402IndexDomain,
7365
8846
  classifyChallenge,
@@ -7422,10 +8903,14 @@ export {
7422
8903
  renderLandingPage,
7423
8904
  requirePayment,
7424
8905
  resolveChain,
8906
+ resolveSlippageBps,
7425
8907
  scoreResource,
7426
8908
  searchOpenIndexes,
7427
8909
  settleViaFacilitator,
7428
8910
  summarizePlan,
8911
+ summarizeSwap,
8912
+ swapProvidersFor,
8913
+ swappableNetworks,
7429
8914
  toA2AErrorCode,
7430
8915
  toA2APaymentFailed,
7431
8916
  toA2APaymentReceipts,