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