@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/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
- const plans = await Promise.all(clients.map((c) => c.planPayment(url, init).catch(() => null)));
3526
- const live = plans.filter((p) => p != null);
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 = rankOptions(live.flatMap((p) => p.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
- // First non-null hint across clients each already names its chain.
3539
- fundingHint: best ? null : live.map((p) => p.fundingHint).find(Boolean) ?? null
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 === "&" ? "&amp;" : c === "<" ? "&lt;" : c === ">" ? "&gt;" : c === '"' ? "&quot;" : "&#39;"
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> &rarr; 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> &middot; <a href="${esc(sd.docs.home)}">${esc(sd.docs.home)}</a> &middot; <a href="${esc(sd.discovery.openapi)}">${esc(sd.discovery.openapi)}</a></p>
4435
+ <footer>Powered by ${esc(sd.name)} &middot; x402 &middot; 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 kind = kinds.find((k) => k?.scheme === "exact" && k?.network === network);
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 extensions = { ...bazaar, ...opts?.extensions };
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: buildAccepts(specs, nonce),
4786
+ accepts,
4397
4787
  ...opts?.error ? { error: opts.error } : {},
4398
- ...Object.keys(extensions).length > 0 ? { extensions } : {}
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(challenge2) };
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.22.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",