@piprail/sdk 1.22.1 → 1.24.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 +83 -0
- package/README.md +28 -1
- package/dist/index.cjs +472 -27
- package/dist/index.d.cts +437 -6
- package/dist/index.d.ts +437 -6
- package/dist/index.js +460 -15
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -511,7 +511,6 @@ import {
|
|
|
511
511
|
var EXACT_NETWORK_SLUGS = {
|
|
512
512
|
ethereum: 1,
|
|
513
513
|
base: 8453,
|
|
514
|
-
"base-sepolia": 84532,
|
|
515
514
|
arbitrum: 42161,
|
|
516
515
|
optimism: 10,
|
|
517
516
|
polygon: 137,
|
|
@@ -983,8 +982,6 @@ var PERMIT2_PROXY_CHAIN_IDS = /* @__PURE__ */ new Set([
|
|
|
983
982
|
// Ethereum
|
|
984
983
|
8453,
|
|
985
984
|
// Base
|
|
986
|
-
84532,
|
|
987
|
-
// Base Sepolia
|
|
988
985
|
42161,
|
|
989
986
|
// Arbitrum
|
|
990
987
|
10,
|
|
@@ -3496,6 +3493,33 @@ function rankOptions(options) {
|
|
|
3496
3493
|
return 0;
|
|
3497
3494
|
});
|
|
3498
3495
|
}
|
|
3496
|
+
function rankAcross(plans) {
|
|
3497
|
+
const rank = { payable: 0, unknown: 1, blocked: 2 };
|
|
3498
|
+
return plans.flatMap((p) => p.options).sort((a, b) => rank[a.state] - rank[b.state]);
|
|
3499
|
+
}
|
|
3500
|
+
async function planEachClient(clients, url, init) {
|
|
3501
|
+
const settled = await Promise.allSettled(clients.map((c) => c.planPayment(url, init)));
|
|
3502
|
+
const live = [];
|
|
3503
|
+
let anyReached = false;
|
|
3504
|
+
let firstError;
|
|
3505
|
+
settled.forEach((s, i) => {
|
|
3506
|
+
if (s.status === "fulfilled") {
|
|
3507
|
+
anyReached = true;
|
|
3508
|
+
if (s.value != null) live.push({ client: clients[i], plan: s.value });
|
|
3509
|
+
} else if (firstError === void 0) {
|
|
3510
|
+
firstError = s.reason;
|
|
3511
|
+
}
|
|
3512
|
+
});
|
|
3513
|
+
if (live.length === 0 && !anyReached) {
|
|
3514
|
+
throw firstError ?? new Error("planAcross: every client failed to reach the resource.");
|
|
3515
|
+
}
|
|
3516
|
+
return live;
|
|
3517
|
+
}
|
|
3518
|
+
function mergeDeclineHint(plans) {
|
|
3519
|
+
const actionable = plans.filter((p) => p.options.length > 0 && p.fundingHint).map((p) => p.fundingHint);
|
|
3520
|
+
const chosen = actionable.length ? actionable : plans.map((p) => p.fundingHint).filter(Boolean);
|
|
3521
|
+
return chosen.length ? [...new Set(chosen)].join(" \xB7 ") : null;
|
|
3522
|
+
}
|
|
3499
3523
|
function buildFundingHint(options, chainLabel) {
|
|
3500
3524
|
if (options.length === 0) return null;
|
|
3501
3525
|
const target = [...options].sort((a, b) => a.blockers.length - b.blockers.length)[0];
|
|
@@ -3522,10 +3546,10 @@ function buildFundingHint(options, chainLabel) {
|
|
|
3522
3546
|
return parts.length ? `Can't settle on ${chainLabel}: ${parts.join(" and ")} (to pay ${target.quote.amountFormatted} ${sym}).` : `Can't settle on ${chainLabel} for ${target.quote.amountFormatted} ${sym}.`;
|
|
3523
3547
|
}
|
|
3524
3548
|
async function planAcross(clients, url, init) {
|
|
3525
|
-
|
|
3526
|
-
const live =
|
|
3549
|
+
if (clients.length === 0) return null;
|
|
3550
|
+
const live = (await planEachClient(clients, url, init)).map((p) => p.plan);
|
|
3527
3551
|
if (live.length === 0) return null;
|
|
3528
|
-
const options =
|
|
3552
|
+
const options = rankAcross(live);
|
|
3529
3553
|
const best = options.find((o) => o.state === "payable") ?? null;
|
|
3530
3554
|
const status = best ? "ready" : options.some((o) => o.state === "unknown") ? "unknown" : "blocked";
|
|
3531
3555
|
return {
|
|
@@ -3535,10 +3559,25 @@ async function planAcross(clients, url, init) {
|
|
|
3535
3559
|
payable: best !== null,
|
|
3536
3560
|
best,
|
|
3537
3561
|
options,
|
|
3538
|
-
//
|
|
3539
|
-
|
|
3562
|
+
// Merge EVERY funded chain's blocker into one clear sentence (not just the first) —
|
|
3563
|
+
// see mergeDeclineHint. `null` when a rail is payable.
|
|
3564
|
+
fundingHint: best ? null : mergeDeclineHint(live)
|
|
3540
3565
|
};
|
|
3541
3566
|
}
|
|
3567
|
+
async function fetchAcross(clients, url, init) {
|
|
3568
|
+
if (clients.length === 0) {
|
|
3569
|
+
throw new TypeError("fetchAcross needs at least one PipRailClient.");
|
|
3570
|
+
}
|
|
3571
|
+
const live = await planEachClient(clients, url, init);
|
|
3572
|
+
if (live.length === 0) return clients[0].fetch(url, init);
|
|
3573
|
+
const best = rankAcross(live.map((p) => p.plan)).find((o) => o.state === "payable");
|
|
3574
|
+
if (!best) {
|
|
3575
|
+
const hint = mergeDeclineHint(live.map((p) => p.plan));
|
|
3576
|
+
throw new PaymentDeclinedError(hint || "No funded chain can settle this payment right now.");
|
|
3577
|
+
}
|
|
3578
|
+
const owner = live.find((p) => p.plan.options.includes(best)).client;
|
|
3579
|
+
return owner.fetch(url, { ...init ?? {}, autoRoute: true });
|
|
3580
|
+
}
|
|
3542
3581
|
function railOnNetwork(rail, matches) {
|
|
3543
3582
|
const n = normalizeNetwork(rail.network);
|
|
3544
3583
|
return !n.includes(":") || matches(n);
|
|
@@ -3604,6 +3643,229 @@ async function readInvalidReason(response) {
|
|
|
3604
3643
|
return null;
|
|
3605
3644
|
}
|
|
3606
3645
|
|
|
3646
|
+
// src/payer.ts
|
|
3647
|
+
var MultiChainPayer = class _MultiChainPayer {
|
|
3648
|
+
_clients;
|
|
3649
|
+
/**
|
|
3650
|
+
* Wrap an explicit, ordered set of single-chain clients — use this when a client
|
|
3651
|
+
* needs full control (e.g. a custom EVM chain configured by a viem `Chain`). The
|
|
3652
|
+
* ORDER is your chain preference: across chains the first that can settle wins. Pass
|
|
3653
|
+
* at MOST one client per chain — two clients on the SAME network would double-count in
|
|
3654
|
+
* `spent()`/`budget()` and waste a plan round-trip (`fromWallets` can't produce this).
|
|
3655
|
+
* For the common case, prefer {@link MultiChainPayer.fromWallets}.
|
|
3656
|
+
*/
|
|
3657
|
+
constructor(clients) {
|
|
3658
|
+
if (clients.length === 0) {
|
|
3659
|
+
throw new TypeError("MultiChainPayer needs at least one PipRailClient.");
|
|
3660
|
+
}
|
|
3661
|
+
this._clients = [...clients];
|
|
3662
|
+
}
|
|
3663
|
+
/**
|
|
3664
|
+
* Build one client per funded chain from a `{ chain → wallet }` map — the
|
|
3665
|
+
* ergonomic path. The shared `policy`/`schemes`/`onBeforePay`/`onEvent` apply to
|
|
3666
|
+
* every client; `rpcUrls` are matched per chain. Iteration order of `wallets` is
|
|
3667
|
+
* the chain preference.
|
|
3668
|
+
*
|
|
3669
|
+
* ```ts
|
|
3670
|
+
* const payer = MultiChainPayer.fromWallets({
|
|
3671
|
+
* wallets: {
|
|
3672
|
+
* base: { privateKey: process.env.EVM_KEY! },
|
|
3673
|
+
* solana: { secretKey: process.env.SOLANA_SECRET! },
|
|
3674
|
+
* xrpl: { seed: process.env.XRPL_SEED! },
|
|
3675
|
+
* },
|
|
3676
|
+
* policy: { maxAmount: '1.00', maxTotal: '20.00', tokens: ['USDC', 'USDT'] },
|
|
3677
|
+
* })
|
|
3678
|
+
* const res = await payer.get('https://api.example.com/paid') // pays on the first funded chain that can settle
|
|
3679
|
+
* ```
|
|
3680
|
+
*/
|
|
3681
|
+
static fromWallets(opts) {
|
|
3682
|
+
const entries = Object.entries(opts.wallets);
|
|
3683
|
+
if (entries.length === 0) {
|
|
3684
|
+
throw new TypeError("MultiChainPayer.fromWallets needs at least one wallet.");
|
|
3685
|
+
}
|
|
3686
|
+
const clients = entries.map(
|
|
3687
|
+
([chain, wallet]) => new PipRailClient({
|
|
3688
|
+
chain,
|
|
3689
|
+
wallet,
|
|
3690
|
+
...opts.policy ? { policy: opts.policy } : {},
|
|
3691
|
+
...opts.schemes ? { schemes: opts.schemes } : {},
|
|
3692
|
+
...opts.rpcUrls?.[chain] ? { rpcUrl: opts.rpcUrls[chain] } : {},
|
|
3693
|
+
...opts.onBeforePay ? { onBeforePay: opts.onBeforePay } : {},
|
|
3694
|
+
...opts.onEvent ? { onEvent: opts.onEvent } : {},
|
|
3695
|
+
...opts.maxPaymentRetries != null ? { maxPaymentRetries: opts.maxPaymentRetries } : {},
|
|
3696
|
+
...opts.retryTimeoutMs != null ? { retryTimeoutMs: opts.retryTimeoutMs } : {}
|
|
3697
|
+
})
|
|
3698
|
+
);
|
|
3699
|
+
return new _MultiChainPayer(clients);
|
|
3700
|
+
}
|
|
3701
|
+
/** The underlying single-chain clients, in preference order. Reach for one of
|
|
3702
|
+
* these for chain-specific reads (`estimateCost`, `discoverySigner`, per-chain
|
|
3703
|
+
* `budget()`) that don't make sense merged. */
|
|
3704
|
+
get clients() {
|
|
3705
|
+
return this._clients;
|
|
3706
|
+
}
|
|
3707
|
+
/** Plan a 402 across every funded chain — merged + ranked payable-first. `null`
|
|
3708
|
+
* when the URL needs no payment. (Delegates to {@link planAcross}.) */
|
|
3709
|
+
planPayment(url, init) {
|
|
3710
|
+
return planAcross(this._clients, url, init);
|
|
3711
|
+
}
|
|
3712
|
+
/** Can ANY funded chain settle this URL right now? (A free resource is trivially
|
|
3713
|
+
* "affordable".) No funds move. */
|
|
3714
|
+
async canAfford(url, init) {
|
|
3715
|
+
const plan = await this.planPayment(url, init);
|
|
3716
|
+
return plan == null ? true : plan.payable;
|
|
3717
|
+
}
|
|
3718
|
+
/** Price a gated URL across funded chains — the chosen rail's quote (the first
|
|
3719
|
+
* funded chain that can settle), else the first offered rail's. `null` when the URL
|
|
3720
|
+
* needs no payment. When it IS
|
|
3721
|
+
* gated but none of your chains are offered, surfaces the same informative
|
|
3722
|
+
* `NoCompatibleAcceptError` a single client would (it names the chains the 402 is
|
|
3723
|
+
* payable on) rather than a misleading `null`. No funds move. */
|
|
3724
|
+
async quote(url, init) {
|
|
3725
|
+
const plan = await this.planPayment(url, init);
|
|
3726
|
+
if (plan == null) return null;
|
|
3727
|
+
const opt = plan.best ?? plan.options[0];
|
|
3728
|
+
if (opt) return opt.quote;
|
|
3729
|
+
return this._clients[0].quote(url, init);
|
|
3730
|
+
}
|
|
3731
|
+
/** Pay the first funded chain (in your listed order) that can settle this URL.
|
|
3732
|
+
* Delegates to {@link fetchAcross} — full policy / approval / retry / replay path on
|
|
3733
|
+
* the owning client. The owner re-reads balances at pay time, so the rail paid is the
|
|
3734
|
+
* surfaced `best` on a best-effort basis (it can pick another rail on the SAME chain,
|
|
3735
|
+
* or decline, if balances shift between plan and pay). PROBES the URL with `init`
|
|
3736
|
+
* (method + body) per client — prefer GET / idempotent requests. */
|
|
3737
|
+
fetch(url, init) {
|
|
3738
|
+
return fetchAcross(this._clients, url, init);
|
|
3739
|
+
}
|
|
3740
|
+
/** GET that auto-pays across chains. */
|
|
3741
|
+
get(url, init) {
|
|
3742
|
+
return this.fetch(url, { ...init ?? {}, method: "GET" });
|
|
3743
|
+
}
|
|
3744
|
+
/**
|
|
3745
|
+
* POST that auto-pays across chains. `body` is a string/FormData/URLSearchParams/
|
|
3746
|
+
* ArrayBuffer/Blob (sent as-is) or a plain object (serialised as JSON) — mirrors
|
|
3747
|
+
* {@link PipRailClient.post}.
|
|
3748
|
+
*/
|
|
3749
|
+
post(url, body, init) {
|
|
3750
|
+
const headers = new Headers(init?.headers);
|
|
3751
|
+
let payload;
|
|
3752
|
+
if (body === void 0 || body === null) {
|
|
3753
|
+
payload = void 0;
|
|
3754
|
+
} else if (isBodyInit(body)) {
|
|
3755
|
+
payload = body;
|
|
3756
|
+
} else if (typeof body === "object") {
|
|
3757
|
+
payload = JSON.stringify(body);
|
|
3758
|
+
if (!headers.has("content-type")) headers.set("content-type", "application/json");
|
|
3759
|
+
} else {
|
|
3760
|
+
payload = String(body);
|
|
3761
|
+
}
|
|
3762
|
+
return this.fetch(url, { ...init ?? {}, method: "POST", headers, body: payload });
|
|
3763
|
+
}
|
|
3764
|
+
/**
|
|
3765
|
+
* Find payable resources across every funded chain. With the default
|
|
3766
|
+
* `network: 'self'`, each chain's own results are merged + deduped by URL (so
|
|
3767
|
+
* "self" means "any chain I can pay"). A network-scoped query (a CAIP-2 id or
|
|
3768
|
+
* `'any'`) is chain-independent, so one client answers it. Never throws for a
|
|
3769
|
+
* read problem; moves no funds.
|
|
3770
|
+
*/
|
|
3771
|
+
async discover(opts = {}) {
|
|
3772
|
+
if (opts.network && opts.network !== "self") {
|
|
3773
|
+
return this._clients[0].discover(opts);
|
|
3774
|
+
}
|
|
3775
|
+
const perChain = await Promise.all(
|
|
3776
|
+
this._clients.map((c) => c.discover({ ...opts, network: "self" }).catch(() => []))
|
|
3777
|
+
);
|
|
3778
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3779
|
+
const merged = [];
|
|
3780
|
+
for (const r of perChain.flat()) {
|
|
3781
|
+
if (seen.has(r.resource)) continue;
|
|
3782
|
+
seen.add(r.resource);
|
|
3783
|
+
merged.push(r);
|
|
3784
|
+
}
|
|
3785
|
+
return merged;
|
|
3786
|
+
}
|
|
3787
|
+
/** List a resource YOU run on the open indexes. Registration is a merchant action
|
|
3788
|
+
* independent of which chain you pay FROM, so it goes through your first chain's
|
|
3789
|
+
* client; pass `opts.network` to advertise a specific chain. Moves no funds. */
|
|
3790
|
+
register(url, opts = {}) {
|
|
3791
|
+
return this._clients[0].register(url, opts);
|
|
3792
|
+
}
|
|
3793
|
+
/** Aggregate spend across every chain — counts summed; per-(network,asset) rows
|
|
3794
|
+
* and records concatenated (no cross-chain collisions, never a cross-token sum). */
|
|
3795
|
+
spent() {
|
|
3796
|
+
const summaries = this._clients.map((c) => c.spent());
|
|
3797
|
+
return {
|
|
3798
|
+
count: summaries.reduce((n, s) => n + s.count, 0),
|
|
3799
|
+
byAsset: summaries.flatMap((s) => s.byAsset),
|
|
3800
|
+
records: summaries.flatMap((s) => s.records)
|
|
3801
|
+
};
|
|
3802
|
+
}
|
|
3803
|
+
/** A merged budget view: every chain's per-(network,asset) remaining rows, plus the
|
|
3804
|
+
* MOST-RESTRICTIVE session time envelope across chains (the soonest deadline wins).
|
|
3805
|
+
* Mirrors {@link PipRailClient.budget}'s shape so the agent toolkit reads it
|
|
3806
|
+
* unchanged; per-chain session detail is on each `clients[i].budget()`. */
|
|
3807
|
+
budget() {
|
|
3808
|
+
const budgets = this._clients.map((c) => c.budget());
|
|
3809
|
+
const session = budgets.map((b) => b.session).reduce((soonest, s) => {
|
|
3810
|
+
if (soonest.secondsRemaining == null) return s;
|
|
3811
|
+
if (s.secondsRemaining == null) return soonest;
|
|
3812
|
+
return s.secondsRemaining < soonest.secondsRemaining ? s : soonest;
|
|
3813
|
+
});
|
|
3814
|
+
return { session, byAsset: budgets.flatMap((b) => b.byAsset) };
|
|
3815
|
+
}
|
|
3816
|
+
};
|
|
3817
|
+
function isBodyInit(value) {
|
|
3818
|
+
if (typeof value === "string") return true;
|
|
3819
|
+
if (value instanceof ArrayBuffer) return true;
|
|
3820
|
+
if (ArrayBuffer.isView(value)) return true;
|
|
3821
|
+
if (typeof URLSearchParams !== "undefined" && value instanceof URLSearchParams) return true;
|
|
3822
|
+
if (typeof FormData !== "undefined" && value instanceof FormData) return true;
|
|
3823
|
+
if (typeof Blob !== "undefined" && value instanceof Blob) return true;
|
|
3824
|
+
return false;
|
|
3825
|
+
}
|
|
3826
|
+
|
|
3827
|
+
// src/selfdescribe.ts
|
|
3828
|
+
var BRAND = {
|
|
3829
|
+
name: "PipRail",
|
|
3830
|
+
home: "https://piprail.com",
|
|
3831
|
+
docs: "https://docs.piprail.com",
|
|
3832
|
+
payDocs: "https://docs.piprail.com/paying",
|
|
3833
|
+
sdkInstall: "npm i @piprail/sdk",
|
|
3834
|
+
sdkSnippet: "import { PipRailClient } from '@piprail/sdk'\nconst client = new PipRailClient({ chain: '<your-chain>', wallet })\nawait client.fetch('<this-url>')",
|
|
3835
|
+
mcpRun: "npx -y @piprail/mcp"
|
|
3836
|
+
};
|
|
3837
|
+
var WHAT = 'This is an x402 "402 Payment Required" endpoint. Pay one of the offered rails to access it.';
|
|
3838
|
+
function howFor(scheme) {
|
|
3839
|
+
return scheme === "exact" ? "Standard x402 exact rail \u2014 sign an EIP-3009 / Permit2 / SVM authorization; any stock x402 client (e.g. @x402/fetch) can pay this." : "Pay this amount on-chain to payTo, then resubmit with a payment-signature header carrying the proof ref + nonce. Easiest with @piprail/sdk (see sdk.install).";
|
|
3840
|
+
}
|
|
3841
|
+
function railOf(a) {
|
|
3842
|
+
const extra = a.extra ?? {};
|
|
3843
|
+
return {
|
|
3844
|
+
scheme: a.scheme,
|
|
3845
|
+
network: a.network,
|
|
3846
|
+
asset: a.asset,
|
|
3847
|
+
payTo: a.payTo,
|
|
3848
|
+
amount: a.amount,
|
|
3849
|
+
...extra.amountFormatted ? { amountFormatted: extra.amountFormatted } : {},
|
|
3850
|
+
...extra.symbol ? { symbol: extra.symbol } : {},
|
|
3851
|
+
how: howFor(a.scheme)
|
|
3852
|
+
};
|
|
3853
|
+
}
|
|
3854
|
+
function buildSelfDescription(input) {
|
|
3855
|
+
return {
|
|
3856
|
+
name: "PipRail",
|
|
3857
|
+
protocol: "x402",
|
|
3858
|
+
version: "2",
|
|
3859
|
+
what: WHAT,
|
|
3860
|
+
pay: input.accepts.map(railOf),
|
|
3861
|
+
sdk: { install: BRAND.sdkInstall, snippet: BRAND.sdkSnippet },
|
|
3862
|
+
mcp: { run: BRAND.mcpRun, tool: "piprail_pay_request" },
|
|
3863
|
+
docs: { home: BRAND.home, agents: BRAND.docs, pay: BRAND.payDocs },
|
|
3864
|
+
discovery: { openapi: "/openapi.json", wellKnown: "/.well-known/x402" },
|
|
3865
|
+
...input.instruction ? { instruction: input.instruction } : {}
|
|
3866
|
+
};
|
|
3867
|
+
}
|
|
3868
|
+
|
|
3607
3869
|
// src/render.ts
|
|
3608
3870
|
function summarizePlan(plan) {
|
|
3609
3871
|
if (plan == null) return "No payment required \u2014 the URL is not payment-gated.";
|
|
@@ -3644,6 +3906,18 @@ function formatSpendReport(summary) {
|
|
|
3644
3906
|
(a) => `${a.totalFormatted} ${a.symbol ?? a.asset} on ${a.network} (${a.count} payment${a.count === 1 ? "" : "s"})`
|
|
3645
3907
|
).join("; ");
|
|
3646
3908
|
}
|
|
3909
|
+
function describeChallenge(challenge) {
|
|
3910
|
+
const first = challenge.accepts[0];
|
|
3911
|
+
if (!first) {
|
|
3912
|
+
return `${BRAND.name} x402 payment endpoint. Pay with @piprail/sdk (${BRAND.sdkInstall}). Docs: ${BRAND.home}.`;
|
|
3913
|
+
}
|
|
3914
|
+
const extra = first.extra ?? {};
|
|
3915
|
+
const amount = extra.amountFormatted ?? first.amount;
|
|
3916
|
+
const token = extra.symbol ?? first.asset;
|
|
3917
|
+
const hasExact = challenge.accepts.some((a) => a.scheme === "exact");
|
|
3918
|
+
const standard = hasExact ? "; or any standard x402 client (an exact rail is offered)" : "";
|
|
3919
|
+
return `${BRAND.name} x402 payment endpoint \u2014 pay ${amount} ${token} on ${first.network} to ${first.payTo}. Programmatic: ${BRAND.sdkInstall} then client.fetch(url)${standard}. Docs: ${BRAND.home}.`;
|
|
3920
|
+
}
|
|
3647
3921
|
|
|
3648
3922
|
// src/agentGuide.ts
|
|
3649
3923
|
var PIPRAIL_AGENT_GUIDE = `# Paying with PipRail \u2014 the agent contract
|
|
@@ -3651,6 +3925,13 @@ var PIPRAIL_AGENT_GUIDE = `# Paying with PipRail \u2014 the agent contract
|
|
|
3651
3925
|
You can pay for x402 "402 Payment Required" resources autonomously. Money moves
|
|
3652
3926
|
straight from your wallet to the server; PipRail custodies nothing. Follow this.
|
|
3653
3927
|
|
|
3928
|
+
## Landing cold \u2014 read the self-description
|
|
3929
|
+
Every PipRail 402 self-describes. Read challenge.extensions.piprail for { name, what, pay[]
|
|
3930
|
+
(each rail's how-to-pay), sdk.install, mcp, docs } \u2014 never guess what an endpoint is. If your
|
|
3931
|
+
tooling can't pay a rail (e.g. a stock x402 client can't pay the onchain-proof scheme), the
|
|
3932
|
+
block says how: install @piprail/sdk (npm i @piprail/sdk) or run the MCP (npx -y @piprail/mcp)
|
|
3933
|
+
and pay with the tools below.
|
|
3934
|
+
|
|
3654
3935
|
## The loop: quote \u2192 plan \u2192 pay
|
|
3655
3936
|
1. piprail_quote_payment(url) \u2014 PRICE it. Returns the amount, token, chain, and
|
|
3656
3937
|
whether it is within your spend policy. No funds move. Use it to decide if a
|
|
@@ -3945,7 +4226,12 @@ function paymentTools(client) {
|
|
|
3945
4226
|
url: { type: "string", description: "Full URL of the resource to list." },
|
|
3946
4227
|
name: { type: "string", description: "Display name (defaults to the host)." },
|
|
3947
4228
|
description: { type: "string", description: "What the resource offers." },
|
|
3948
|
-
priceUsd: { type: "number", description: "Advertised price in USD (metadata)." }
|
|
4229
|
+
priceUsd: { type: "number", description: "Advertised price in USD (metadata)." },
|
|
4230
|
+
network: {
|
|
4231
|
+
type: "string",
|
|
4232
|
+
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."
|
|
4233
|
+
},
|
|
4234
|
+
asset: { type: "string", description: "Payment asset symbol, e.g. 'USDC' (metadata)." }
|
|
3949
4235
|
},
|
|
3950
4236
|
required: ["url"],
|
|
3951
4237
|
additionalProperties: false
|
|
@@ -3955,6 +4241,8 @@ function paymentTools(client) {
|
|
|
3955
4241
|
if (typeof args.name === "string") opts.name = args.name;
|
|
3956
4242
|
if (typeof args.description === "string") opts.description = args.description;
|
|
3957
4243
|
if (typeof args.priceUsd === "number") opts.priceUsd = args.priceUsd;
|
|
4244
|
+
if (typeof args.network === "string") opts.network = args.network;
|
|
4245
|
+
if (typeof args.asset === "string") opts.asset = args.asset;
|
|
3958
4246
|
const outcomes = await client.register(String(args.url), opts);
|
|
3959
4247
|
return { outcomes };
|
|
3960
4248
|
}
|
|
@@ -4011,6 +4299,13 @@ function classifyChallenge(challenge, opts) {
|
|
|
4011
4299
|
|
|
4012
4300
|
// src/discovery.ts
|
|
4013
4301
|
var GENERATOR = "@piprail/sdk \xB7 https://piprail.com";
|
|
4302
|
+
var POWERED_BY = "PipRail x402 | https://piprail.com";
|
|
4303
|
+
function discoveryHeaders(opts = {}) {
|
|
4304
|
+
return {
|
|
4305
|
+
link: '</openapi.json>; rel="service-desc", </.well-known/x402>; rel="x402-discovery"',
|
|
4306
|
+
...opts.attribution === false ? {} : { "x-powered-by": POWERED_BY }
|
|
4307
|
+
};
|
|
4308
|
+
}
|
|
4014
4309
|
function buildBazaarExtension(descriptor = {}) {
|
|
4015
4310
|
const method = (descriptor.method ?? "GET").toUpperCase();
|
|
4016
4311
|
const queryParams = descriptor.queryParams ?? {};
|
|
@@ -4090,6 +4385,57 @@ function buildX402DnsTxt(input) {
|
|
|
4090
4385
|
};
|
|
4091
4386
|
}
|
|
4092
4387
|
|
|
4388
|
+
// src/landing.ts
|
|
4389
|
+
function esc(s) {
|
|
4390
|
+
return String(s ?? "").replace(
|
|
4391
|
+
/[&<>"']/g,
|
|
4392
|
+
(c) => c === "&" ? "&" : c === "<" ? "<" : c === ">" ? ">" : c === '"' ? """ : "'"
|
|
4393
|
+
);
|
|
4394
|
+
}
|
|
4395
|
+
var STYLE = `:root{color-scheme:dark}
|
|
4396
|
+
*{box-sizing:border-box}
|
|
4397
|
+
body{margin:0;background:#0a0e0f;color:#e6edf0;font:16px/1.6 ui-sans-serif,system-ui,-apple-system,Inter,sans-serif}
|
|
4398
|
+
main{max-width:680px;margin:0 auto;padding:48px 24px}
|
|
4399
|
+
h1{font-size:1.6rem;margin:0 0 .25rem}
|
|
4400
|
+
.lede{color:#9fb0b5;margin:.25rem 0 2rem}
|
|
4401
|
+
h2{font-size:1rem;text-transform:uppercase;letter-spacing:.05em;color:#34d399;margin:2rem 0 .75rem}
|
|
4402
|
+
table{width:100%;border-collapse:collapse;font-size:.92rem}
|
|
4403
|
+
th,td{text-align:left;padding:.5rem .6rem;border-bottom:1px solid #1c2426;vertical-align:top}
|
|
4404
|
+
th{color:#9fb0b5;font-weight:600}
|
|
4405
|
+
code,pre{font-family:ui-monospace,JetBrains Mono,monospace;font-size:.85rem}
|
|
4406
|
+
pre{background:#11181a;border:1px solid #1c2426;border-radius:8px;padding:14px;overflow-x:auto;color:#cfe9df}
|
|
4407
|
+
a{color:#34d399}
|
|
4408
|
+
.mono{font-family:ui-monospace,monospace;word-break:break-all}
|
|
4409
|
+
.warn{margin:0 0 2rem;padding:14px 16px;border:1px solid #5a4a1f;background:#17130a;border-radius:8px;color:#e8d9a8;font-size:.92rem;line-height:1.55}
|
|
4410
|
+
.warn strong{color:#f5d77a}
|
|
4411
|
+
.muted{color:#5b6b6f;font-weight:400;text-transform:none;letter-spacing:0;font-size:.8rem}
|
|
4412
|
+
footer{margin-top:2.5rem;color:#5b6b6f;font-size:.8rem}`;
|
|
4413
|
+
function renderLandingPage(sd) {
|
|
4414
|
+
const lede = esc(sd.instruction ?? sd.what);
|
|
4415
|
+
const rows = sd.pay.map(
|
|
4416
|
+
(r) => `<tr><td><code>${esc(r.scheme)}</code></td><td>${esc(r.network)}</td><td>${esc(r.amountFormatted ?? r.amount)} ${esc(r.symbol ?? r.asset)}</td><td class="mono">${esc(r.payTo)}</td></tr>`
|
|
4417
|
+
).join("");
|
|
4418
|
+
return `<!doctype html>
|
|
4419
|
+
<html lang="en"><head><meta charset="utf-8">
|
|
4420
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
4421
|
+
<title>${esc(sd.name)} \xB7 x402 payment required</title>
|
|
4422
|
+
<style>${STYLE}</style>
|
|
4423
|
+
</head><body><main>
|
|
4424
|
+
<h1>402 \u2014 Payment Required</h1>
|
|
4425
|
+
<p class="lede">${lede}</p>
|
|
4426
|
+
<div class="warn"><strong>Pay with an x402 client \u2014 not by hand.</strong> This endpoint is paid programmatically: an x402 client (the ${esc(sd.name)} SDK or MCP below, or any x402-compatible wallet) makes the payment <em>and proves it</em>, which is what unlocks the resource. Sending funds straight to the address below from an ordinary wallet will reach the merchant but <strong>will NOT unlock this resource and won't be matched to your request</strong> \u2014 there is no custody and no manual-payment desk. Use one of the methods below.</div>
|
|
4427
|
+
<h2>How to pay</h2>
|
|
4428
|
+
<pre>${esc(sd.sdk.install)}</pre>
|
|
4429
|
+
<pre>${esc(sd.sdk.snippet)}</pre>
|
|
4430
|
+
<p>For AI agents (MCP): <code>${esc(sd.mcp.run)}</code> → tool <code>${esc(sd.mcp.tool)}</code></p>
|
|
4431
|
+
<h2>Payment details <span class="muted">\u2014 what the client pays, NOT a manual-send address</span></h2>
|
|
4432
|
+
<table><thead><tr><th>Scheme</th><th>Chain</th><th>Amount</th><th>Settles to</th></tr></thead>
|
|
4433
|
+
<tbody>${rows}</tbody></table>
|
|
4434
|
+
<p>Docs: <a href="${esc(sd.docs.pay)}">paying</a> · <a href="${esc(sd.docs.home)}">${esc(sd.docs.home)}</a> · <a href="${esc(sd.discovery.openapi)}">${esc(sd.discovery.openapi)}</a></p>
|
|
4435
|
+
<footer>Powered by ${esc(sd.name)} · x402 · no backend, no fee, settled straight to the merchant's wallet (no custody).</footer>
|
|
4436
|
+
</main></body></html>`;
|
|
4437
|
+
}
|
|
4438
|
+
|
|
4093
4439
|
// src/facilitator.ts
|
|
4094
4440
|
async function fetchFacilitatorFeePayer(url, network, timeoutMs = 8e3) {
|
|
4095
4441
|
const base2 = url.replace(/\/+$/, "");
|
|
@@ -4100,7 +4446,8 @@ async function fetchFacilitatorFeePayer(url, network, timeoutMs = 8e3) {
|
|
|
4100
4446
|
if (!res.ok) return void 0;
|
|
4101
4447
|
const body = await res.json();
|
|
4102
4448
|
const kinds = Array.isArray(body?.kinds) ? body.kinds : [];
|
|
4103
|
-
const
|
|
4449
|
+
const want = normalizeNetwork(network);
|
|
4450
|
+
const kind = kinds.find((k) => k?.scheme === "exact" && normalizeNetwork(String(k?.network ?? "")) === want);
|
|
4104
4451
|
const fp = kind?.extra?.feePayer;
|
|
4105
4452
|
return typeof fp === "string" ? fp : void 0;
|
|
4106
4453
|
} catch {
|
|
@@ -4109,6 +4456,33 @@ async function fetchFacilitatorFeePayer(url, network, timeoutMs = 8e3) {
|
|
|
4109
4456
|
clearTimeout(timer);
|
|
4110
4457
|
}
|
|
4111
4458
|
}
|
|
4459
|
+
function parseFacilitatorSupported(body) {
|
|
4460
|
+
const kinds = body?.kinds;
|
|
4461
|
+
if (!Array.isArray(kinds)) return [];
|
|
4462
|
+
const out = [];
|
|
4463
|
+
for (const k of kinds) {
|
|
4464
|
+
if (!k || typeof k !== "object") continue;
|
|
4465
|
+
const o = k;
|
|
4466
|
+
if (typeof o.scheme !== "string" || typeof o.network !== "string") continue;
|
|
4467
|
+
const fp = o.extra?.feePayer;
|
|
4468
|
+
out.push({ scheme: o.scheme, network: o.network, ...typeof fp === "string" ? { feePayer: fp } : {} });
|
|
4469
|
+
}
|
|
4470
|
+
return out;
|
|
4471
|
+
}
|
|
4472
|
+
async function facilitatorCoverage(url, timeoutMs = 8e3) {
|
|
4473
|
+
const base2 = url.replace(/\/+$/, "");
|
|
4474
|
+
const ctrl = new AbortController();
|
|
4475
|
+
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
4476
|
+
try {
|
|
4477
|
+
const res = await fetch(`${base2}/supported`, { signal: ctrl.signal });
|
|
4478
|
+
if (!res.ok) return [];
|
|
4479
|
+
return parseFacilitatorSupported(await res.json());
|
|
4480
|
+
} catch {
|
|
4481
|
+
return [];
|
|
4482
|
+
} finally {
|
|
4483
|
+
clearTimeout(timer);
|
|
4484
|
+
}
|
|
4485
|
+
}
|
|
4112
4486
|
function safeStringify(value) {
|
|
4113
4487
|
return JSON.stringify(value, (_k, v) => typeof v === "bigint" ? v.toString() : v);
|
|
4114
4488
|
}
|
|
@@ -4386,22 +4760,47 @@ function createPaymentGate(options) {
|
|
|
4386
4760
|
const specs = await ready();
|
|
4387
4761
|
const nonce = genNonce();
|
|
4388
4762
|
const bazaar = options.discovery ? { bazaar: buildBazaarExtension(options.discovery === true ? {} : options.discovery) } : void 0;
|
|
4389
|
-
const
|
|
4763
|
+
const accepts = buildAccepts(specs, nonce);
|
|
4764
|
+
const selfDescribe = options.selfDescribe === false ? void 0 : buildSelfDescription({
|
|
4765
|
+
accepts,
|
|
4766
|
+
instruction: describeChallenge({ x402Version: 2, resource: { url: resourceUrl }, accepts })
|
|
4767
|
+
});
|
|
4768
|
+
const rejectionExt = opts?.extensions ?? {};
|
|
4769
|
+
const rejectionPiprail = rejectionExt.piprail ?? {};
|
|
4770
|
+
const bodyPiprail = { ...selfDescribe ?? {}, ...rejectionPiprail };
|
|
4771
|
+
const bodyExtensions = {
|
|
4772
|
+
...bazaar,
|
|
4773
|
+
...rejectionExt,
|
|
4774
|
+
...Object.keys(bodyPiprail).length > 0 ? { piprail: bodyPiprail } : {}
|
|
4775
|
+
};
|
|
4776
|
+
const headerExtensions = {
|
|
4777
|
+
...bazaar,
|
|
4778
|
+
...Object.keys(rejectionPiprail).length > 0 ? { piprail: rejectionPiprail } : {}
|
|
4779
|
+
};
|
|
4390
4780
|
const challenge2 = {
|
|
4391
4781
|
x402Version: 2,
|
|
4392
4782
|
resource: {
|
|
4393
4783
|
url: resourceUrl,
|
|
4394
4784
|
...options.description ? { description: options.description } : {}
|
|
4395
4785
|
},
|
|
4396
|
-
accepts
|
|
4786
|
+
accepts,
|
|
4397
4787
|
...opts?.error ? { error: opts.error } : {},
|
|
4398
|
-
...Object.keys(
|
|
4788
|
+
...Object.keys(bodyExtensions).length > 0 ? { extensions: bodyExtensions } : {}
|
|
4789
|
+
};
|
|
4790
|
+
const headerChallenge = {
|
|
4791
|
+
...challenge2,
|
|
4792
|
+
...Object.keys(headerExtensions).length > 0 ? { extensions: headerExtensions } : { extensions: void 0 }
|
|
4399
4793
|
};
|
|
4400
|
-
return { challenge: challenge2, requiredHeader: buildChallengeHeader(
|
|
4794
|
+
return { challenge: challenge2, requiredHeader: buildChallengeHeader(headerChallenge) };
|
|
4401
4795
|
}
|
|
4402
4796
|
async function challenge(resourceUrl = "") {
|
|
4403
4797
|
return makeChallenge(resourceUrl);
|
|
4404
4798
|
}
|
|
4799
|
+
function landingPage(ch) {
|
|
4800
|
+
return renderLandingPage(
|
|
4801
|
+
buildSelfDescription({ accepts: ch.accepts, instruction: describeChallenge(ch) })
|
|
4802
|
+
);
|
|
4803
|
+
}
|
|
4405
4804
|
async function asChallenge() {
|
|
4406
4805
|
const { challenge: c, requiredHeader } = await makeChallenge("");
|
|
4407
4806
|
return { kind: "challenge", challenge: c, requiredHeader, statusCode: 402 };
|
|
@@ -4597,7 +4996,7 @@ function createPaymentGate(options) {
|
|
|
4597
4996
|
if (exact) return verifyExact(exact);
|
|
4598
4997
|
return asChallenge();
|
|
4599
4998
|
}
|
|
4600
|
-
return { challenge, verify, describe };
|
|
4999
|
+
return { challenge, verify, describe, landingPage };
|
|
4601
5000
|
}
|
|
4602
5001
|
function requirePayment(options) {
|
|
4603
5002
|
const gate = createPaymentGate(options);
|
|
@@ -4637,6 +5036,39 @@ function normaliseHeader(value) {
|
|
|
4637
5036
|
return value;
|
|
4638
5037
|
}
|
|
4639
5038
|
|
|
5039
|
+
// src/facilitators.ts
|
|
5040
|
+
var KNOWN_FACILITATORS = {
|
|
5041
|
+
// PayAI — keyless (no API key), sponsors the gas. Verified 2026-06-14 against
|
|
5042
|
+
// https://facilitator.payai.network/supported (exact · eip155:8453) + the live demo.
|
|
5043
|
+
"eip155:8453": [
|
|
5044
|
+
{
|
|
5045
|
+
url: "https://facilitator.payai.network",
|
|
5046
|
+
keyless: true,
|
|
5047
|
+
schemes: ["exact"],
|
|
5048
|
+
settles: ["eip3009"],
|
|
5049
|
+
note: "PayAI \u2014 keyless, sponsors gas (Base USDC EIP-3009)"
|
|
5050
|
+
}
|
|
5051
|
+
],
|
|
5052
|
+
// PayAI on Solana — keyless fee-payer sponsor for the SVM exact rail. Verified 2026-06-14.
|
|
5053
|
+
"solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp": [
|
|
5054
|
+
{
|
|
5055
|
+
url: "https://facilitator.payai.network",
|
|
5056
|
+
keyless: true,
|
|
5057
|
+
schemes: ["exact"],
|
|
5058
|
+
settles: ["svm"],
|
|
5059
|
+
note: "PayAI \u2014 keyless fee-payer sponsor (Solana SPL SVM)"
|
|
5060
|
+
}
|
|
5061
|
+
]
|
|
5062
|
+
};
|
|
5063
|
+
function knownFacilitatorsFor(network) {
|
|
5064
|
+
return KNOWN_FACILITATORS[network] ?? [];
|
|
5065
|
+
}
|
|
5066
|
+
function firstKeylessFacilitator(network, method) {
|
|
5067
|
+
return knownFacilitatorsFor(network).find(
|
|
5068
|
+
(f) => f.keyless && f.schemes.includes("exact") && (method === void 0 || f.settles.includes(method))
|
|
5069
|
+
);
|
|
5070
|
+
}
|
|
5071
|
+
|
|
4640
5072
|
// src/receipts.ts
|
|
4641
5073
|
var DEFAULT_RETRIES = 5;
|
|
4642
5074
|
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
@@ -4741,6 +5173,7 @@ async function deliverReceipt(receipt, options) {
|
|
|
4741
5173
|
};
|
|
4742
5174
|
}
|
|
4743
5175
|
export {
|
|
5176
|
+
BRAND,
|
|
4744
5177
|
CHAINS,
|
|
4745
5178
|
ConfirmationTimeoutError,
|
|
4746
5179
|
DIRECTORY_INFO,
|
|
@@ -4754,14 +5187,17 @@ export {
|
|
|
4754
5187
|
HEADER_SIGNATURE_V1,
|
|
4755
5188
|
InsufficientFundsError,
|
|
4756
5189
|
InvalidEnvelopeError,
|
|
5190
|
+
KNOWN_FACILITATORS,
|
|
4757
5191
|
MaxRetriesExceededError,
|
|
4758
5192
|
MissingDriverError,
|
|
5193
|
+
MultiChainPayer,
|
|
4759
5194
|
NoCompatibleAcceptError,
|
|
4760
5195
|
NonReplayableBodyError,
|
|
4761
5196
|
PERMIT2_ADDRESS,
|
|
4762
5197
|
PERMIT2_PROXY_CHAIN_IDS,
|
|
4763
5198
|
PERMIT2_WITNESS_TYPES,
|
|
4764
5199
|
PIPRAIL_AGENT_GUIDE,
|
|
5200
|
+
POWERED_BY,
|
|
4765
5201
|
PaymentDeclinedError,
|
|
4766
5202
|
PaymentTimeoutError,
|
|
4767
5203
|
PipRailClient,
|
|
@@ -4784,6 +5220,7 @@ export {
|
|
|
4784
5220
|
buildExactSignatureHeader,
|
|
4785
5221
|
buildOpenApi,
|
|
4786
5222
|
buildReceiptHeader,
|
|
5223
|
+
buildSelfDescription,
|
|
4787
5224
|
buildSignatureHeader,
|
|
4788
5225
|
buildWellKnownX402,
|
|
4789
5226
|
buildX402DnsTxt,
|
|
@@ -4793,17 +5230,24 @@ export {
|
|
|
4793
5230
|
createPaymentGate,
|
|
4794
5231
|
decorateOutcome,
|
|
4795
5232
|
deliverReceipt,
|
|
5233
|
+
describeChallenge,
|
|
5234
|
+
discoveryHeaders,
|
|
4796
5235
|
eip3009Abi,
|
|
4797
5236
|
encodeXPaymentHeader,
|
|
4798
5237
|
evaluatePolicy,
|
|
4799
5238
|
explainDecline,
|
|
5239
|
+
facilitatorCoverage,
|
|
5240
|
+
fetchAcross,
|
|
5241
|
+
firstKeylessFacilitator,
|
|
4800
5242
|
formatSpendReport,
|
|
4801
5243
|
getDirectoryInfo,
|
|
4802
5244
|
isPermit2ProxyChain,
|
|
5245
|
+
knownFacilitatorsFor,
|
|
4803
5246
|
normalizeNetwork,
|
|
4804
5247
|
parseChallenge,
|
|
4805
5248
|
parseExactPaymentHeader,
|
|
4806
5249
|
parseExactRequirements,
|
|
5250
|
+
parseFacilitatorSupported,
|
|
4807
5251
|
parseReceipt,
|
|
4808
5252
|
parseSettleResponse,
|
|
4809
5253
|
parseSignatureHeader,
|
|
@@ -4814,6 +5258,7 @@ export {
|
|
|
4814
5258
|
register402Index,
|
|
4815
5259
|
registerDriver,
|
|
4816
5260
|
registerX402Scan,
|
|
5261
|
+
renderLandingPage,
|
|
4817
5262
|
requirePayment,
|
|
4818
5263
|
resolveChain,
|
|
4819
5264
|
searchOpenIndexes,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@piprail/sdk",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.24.0",
|
|
4
4
|
"description": "Accept x402 crypto payments across 29 chains — every major EVM chain plus Solana, TON, Tron, NEAR, Sui, Aptos, Algorand, Stellar & XRPL — in a couple of lines. No backend, no database, no fee; payments settle straight to your wallet.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|