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