@piprail/sdk 1.21.0 → 1.21.1

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 CHANGED
@@ -4,6 +4,38 @@ All notable changes to `@piprail/sdk` are documented here. The format
4
4
  follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the
5
5
  versions follow [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## [1.21.1] — 2026-06-13 — facilitator hardening + gasless auto-routing + agent-facing docs
8
+
9
+ A correctness/robustness patch over 1.21.0, after a full re-review and **live mainnet tests of the
10
+ facilitator path on both Solana and Base (EVM)**. Opt-in surface unchanged; `onchain-proof` still the
11
+ default everywhere.
12
+
13
+ ### Fixed
14
+ - **Gasless auto-routing now works on Solana.** The Solana driver's `estimateCost` now reports the
15
+ `exact` rail as **~0 buyer gas** (the fee payer — a facilitator like PayAI, or your relayer —
16
+ broadcasts and pays the SOL fee), mirroring the EVM driver. Before, it reported the same fee as
17
+ `onchain-proof`, so `planPayment()`/`fetch({ autoRoute: true })` wouldn't prefer the gasless rail —
18
+ now they correctly pick it. (Live-proven: autoRoute chooses `exact` even when the buyer holds SOL.)
19
+ - **Buyer EIP-3009 domain read is resilient to a flaky RPC.** `payExactEvm` now retries the on-chain
20
+ EIP-712 domain read once before concluding a token "isn't EIP-3009", so a rate-limited public RPC can
21
+ no longer misreport real USDC as un-payable and block an otherwise-valid gasless payment. The error
22
+ message, if it still fails, now names the transient-RPC possibility instead of asserting non-EIP-3009.
23
+
24
+ ### Changed
25
+ - **Permit2 can't be facilitator-settled — the gate now says so clearly.** A third-party facilitator
26
+ settles the standard EIP-3009 (EVM) / SVM (Solana) schemes, not PipRail's `x402ExactPermit2Proxy`. A
27
+ *forced* `exact: { method: 'permit2', settle: { facilitator } }` now throws a clear config error, and
28
+ an *auto*-selected Permit2 token is dropped to `onchain-proof`-only over a facilitator (rather than
29
+ advertising a rail it could never settle). Keyed off the resolved method, so Solana (`svm`) is unaffected.
30
+ - **Clearer facilitator-unreachable error.** When a Solana facilitator's `GET /supported` can't be read
31
+ at challenge time, the gate now explains the real cause (and points at `exact.settle.feePayer` /
32
+ `settle: 'self'`) instead of the misleading "none of the offered rails support it".
33
+ - **`PIPRAIL_AGENT_GUIDE` now teaches the gasless `exact` rail** — the two rails, that it's operator-opt-in,
34
+ that the on-chain method is auto-selected, and that on a timeout the `exact` `.ref` is an authorization
35
+ **nonce** (re-present the same authorization, never re-sign). Plus docs: the "whole model in 30 seconds"
36
+ (gas vs `onchain-proof` vs `exact`'s three methods), a "when the facilitator fails" breakdown, and
37
+ agent-toolkit/MCP gasless guidance.
38
+
7
39
  ## [1.21.0] — 2026-06-13 — standard `exact` rail on Solana (SVM) + fully-gasless facilitator mode
8
40
 
9
41
  Opt-in, defaults unchanged. `onchain-proof` stays the default on every chain and is byte-identical.
package/dist/index.cjs CHANGED
@@ -666,10 +666,14 @@ async function payExactEvm(input) {
666
666
  `exact buyer rail requires an EOA signer; ${account.address} is a contract / EIP-1271 / EIP-7702-delegated account (no recoverable ECDSA signature). Pay via onchain-proof.`
667
667
  );
668
668
  }
669
- const domain = await readExactDomain(publicClient, accept.asset);
669
+ let domain = await readExactDomain(publicClient, accept.asset);
670
+ if (!domain) {
671
+ await new Promise((r) => setTimeout(r, 300));
672
+ domain = await readExactDomain(publicClient, accept.asset);
673
+ }
670
674
  if (!domain) {
671
675
  throw new (0, _chunkPA6YD3HLcjs.UnsupportedSchemeError)(
672
- `exact: ${accept.asset} on ${accept.network} isn't an EIP-3009 token (USDT needs Permit2; native coin and plain ERC-20s aren't exact-payable). Pay via onchain-proof.`
676
+ `exact: couldn't derive the EIP-712 domain for ${accept.asset} on ${accept.network}. Either it isn't an EIP-3009 token (USDT needs Permit2; native/plain ERC-20 aren't exact-payable) \u2014 pay via onchain-proof \u2014 OR your RPC couldn't read it (transient): if the gate advertised eip3009, pass a reliable rpcUrl and retry.`
673
677
  );
674
678
  }
675
679
  const g = globalThis.crypto;
@@ -1791,7 +1795,7 @@ var loaders = {
1791
1795
  solana: async () => {
1792
1796
  let mod;
1793
1797
  try {
1794
- mod = await Promise.resolve().then(() => _interopRequireWildcard(require("./solana-CCVSMOKS.cjs")));
1798
+ mod = await Promise.resolve().then(() => _interopRequireWildcard(require("./solana-WG7RGDSI.cjs")));
1795
1799
  } catch (cause) {
1796
1800
  throw new (0, _chunkPA6YD3HLcjs.MissingDriverError)(
1797
1801
  `Solana selected, but its packages aren't installed. Run: npm install @solana/web3.js @solana/spl-token bs58`,
@@ -3647,6 +3651,18 @@ straight from your wallet to the server; PipRail custodies nothing. Follow this.
3647
3651
  and return the result.
3648
3652
  Always plan before you pay so you never commit to a payment you cannot finish.
3649
3653
 
3654
+ ## Gasless \u2014 the exact rail (zero gas for you)
3655
+ A 402 may offer up to two rails; you don't choose per payment \u2014 the client does, automatically:
3656
+ - onchain-proof (PipRail's default): you broadcast the payment yourself and pay the network gas
3657
+ (the native coin \u2014 ETH/SOL/\u2026). Works on every chain.
3658
+ - exact (the ratified x402 rail, opt-in): you only SIGN; the server \u2014 or a facilitator it chose
3659
+ (e.g. PayAI) \u2014 broadcasts it, so you pay ZERO gas (you need only the token, no native coin). It
3660
+ works on EVM + Solana, and the on-chain method (EIP-3009 / Permit2 / SVM) is picked automatically.
3661
+ When the exact scheme is enabled AND balance-aware routing is on, paying picks the cheapest
3662
+ settleable rail \u2014 i.e. the gasless exact one. Nothing changes in your loop: quote \u2192 plan \u2192 pay is
3663
+ identical. The exact scheme is OPT-IN by the operator (MCP: PIPRAIL_SCHEMES=onchain-proof,exact);
3664
+ you can't enable it yourself, but you can report when a 402 needs it (see UNSUPPORTED_SCHEME below).
3665
+
3650
3666
  ## Reading a refusal \u2014 never crash, never double-spend
3651
3667
  A failed pay returns a STRUCTURED object, never a thrown error you must catch:
3652
3668
  { ok:false, code, reason, explain, ref?, reasonCode?, declined? }
@@ -3664,9 +3680,13 @@ Branch on \`code\` (always reliable). Key cases:
3664
3680
  - code:'INSUFFICIENT_FUNDS' \u2014 top up the wallet (token and/or native gas), retry.
3665
3681
  - code:'PAYMENT_TIMEOUT' / 'MAX_RETRIES_EXCEEDED' / 'CONFIRMATION_TIMEOUT' \u2014 the
3666
3682
  payment may ALREADY be on-chain. Recover using the proof on \`.ref\` (re-verify
3667
- or re-submit it); never re-pay \u2014 a fresh payment would double-spend.
3683
+ or re-submit it); never re-pay \u2014 a fresh payment would double-spend. On a gasless
3684
+ exact rail \`.ref\` is the authorization NONCE, not a tx hash: re-present the SAME
3685
+ signed authorization, never sign a fresh one (that would risk a double-spend).
3668
3686
  - code:'NO_COMPATIBLE_ACCEPT' / 'UNSUPPORTED_SCHEME' \u2014 the 402 isn't payable on
3669
3687
  your chain/scheme; \`explain\` says whether it's the wrong chain or a scheme to enable.
3688
+ If it's a standard x402 server offering an exact rail, that's a config fix the operator makes
3689
+ once (enable the exact scheme); report it, don't retry the same call blindly.
3670
3690
 
3671
3691
  ## Knowing your leash \u2014 call piprail_budget
3672
3692
  piprail_budget tells you how much budget and time you have left, per
@@ -4199,6 +4219,7 @@ function createPaymentGate(options) {
4199
4219
  if (resolved) return resolved;
4200
4220
  const p = (async () => {
4201
4221
  const accepts = normaliseAccepts(options);
4222
+ const exactSkips = [];
4202
4223
  const specs = await Promise.all(
4203
4224
  accepts.map(async (a) => {
4204
4225
  const net = await resolveNetwork2({ chain: a.chain, rpcUrl: _nullishCoalesce(a.rpcUrl, () => ( options.rpcUrl)) });
@@ -4212,13 +4233,17 @@ function createPaymentGate(options) {
4212
4233
  const { asset, decimals, symbol } = net.resolveToken(a.token);
4213
4234
  const amountBase = _chunkPA6YD3HLcjs.parseUnits.call(void 0, a.amount, decimals);
4214
4235
  const spec = { net, asset, decimals, symbol, amountBase, amountFormatted: a.amount, payTo };
4215
- if (options.exact) spec.exact = await resolveExactRail(net, asset);
4236
+ if (options.exact) {
4237
+ const outcome = await resolveExactRail(net, asset);
4238
+ if (outcome.rail) spec.exact = outcome.rail;
4239
+ else if (outcome.skipReason) exactSkips.push(outcome.skipReason);
4240
+ }
4216
4241
  return spec;
4217
4242
  })
4218
4243
  );
4219
4244
  if (options.exact && !specs.some((s) => s.exact)) {
4220
4245
  throw new Error(
4221
- "requirePayment: `exact` was requested but none of the offered rails support it. The standard `exact` rail is EVM ERC-20 (EIP-3009 \u2014 USDC / EURC \u2014 or Permit2, e.g. Binance-Peg USDC on BNB) or a Solana SPL token (SVM) \u2014 NOT native coins, NOT families without a standard `exact` scheme. Offer an EVM ERC-20 / Solana SPL token, or drop `exact`."
4246
+ "requirePayment: `exact` was requested but none of the offered rails support it. " + (exactSkips.length > 0 ? exactSkips.join(" ") : "The standard `exact` rail is EVM ERC-20 (EIP-3009 \u2014 USDC / EURC \u2014 or Permit2, e.g. Binance-Peg USDC on BNB) or a Solana SPL token (SVM) \u2014 NOT native coins, NOT families without a standard `exact` scheme. Offer an EVM ERC-20 / Solana SPL token, or drop `exact`.")
4222
4247
  );
4223
4248
  }
4224
4249
  return specs;
@@ -4231,10 +4256,11 @@ function createPaymentGate(options) {
4231
4256
  }
4232
4257
  async function resolveExactRail(net, asset) {
4233
4258
  const cfg = options.exact;
4234
- if (!net.resolveExactRail) return void 0;
4259
+ const settle = cfg.settle;
4260
+ if (!net.resolveExactRail) return {};
4235
4261
  let relayer;
4236
4262
  let feePayer;
4237
- if (cfg.settle === "self") {
4263
+ if (settle === "self") {
4238
4264
  if (cfg.relayer === void 0) {
4239
4265
  throw new Error(
4240
4266
  "requirePayment: exact `settle: 'self'` needs a `relayer` wallet (the gas-paying key that broadcasts the settle), e.g. exact: { settle: 'self', relayer: { privateKey } }."
@@ -4242,21 +4268,36 @@ function createPaymentGate(options) {
4242
4268
  }
4243
4269
  relayer = net.bindWallet(cfg.relayer);
4244
4270
  } else {
4245
- feePayer = cfg.settle.feePayer;
4271
+ feePayer = settle.feePayer;
4246
4272
  }
4247
4273
  const method = _nullishCoalesce(cfg.method, () => ( "auto"));
4248
4274
  let info = await net.resolveExactRail({ asset, method, relayer, feePayer });
4249
- if (!info && cfg.settle !== "self" && !feePayer) {
4250
- const discovered = await fetchFacilitatorFeePayer(cfg.settle.facilitator, net.network);
4251
- if (discovered) info = await net.resolveExactRail({ asset, method, relayer, feePayer: discovered });
4275
+ if (!info && settle !== "self" && !feePayer && asset !== "native") {
4276
+ const discovered = await fetchFacilitatorFeePayer(settle.facilitator, net.network);
4277
+ if (!discovered) {
4278
+ return {
4279
+ skipReason: `${net.network}: couldn't read a fee payer from the facilitator (${settle.facilitator}/supported) \u2014 it may be down, or may not sponsor this network. Set \`exact.settle.feePayer\` explicitly to remove the runtime dependency, switch to \`settle: 'self'\` with your own relayer, or retry.`
4280
+ };
4281
+ }
4282
+ info = await net.resolveExactRail({ asset, method, relayer, feePayer: discovered });
4283
+ }
4284
+ if (!info) return {};
4285
+ if (settle !== "self" && info.method === "permit2") {
4286
+ if (cfg.method === "permit2") {
4287
+ throw new Error(
4288
+ "requirePayment: exact `method: 'permit2'` can't be settled by a third-party facilitator \u2014 facilitators settle the standard EIP-3009 (EVM) / SVM (Solana) schemes, not PipRail\u2019s Permit2 proxy. Use an EIP-3009 token (USDC / EURC) with the facilitator, or `settle: 'self'` (your own relayer) to settle Permit2 yourself."
4289
+ );
4290
+ }
4291
+ return {
4292
+ skipReason: `${net.network}: this token isn't EIP-3009 (auto-selected Permit2), which a third-party facilitator can't settle \u2014 it would serve onchain-proof only here. Use an EIP-3009 token (USDC / EURC) with the facilitator, or \`settle: 'self'\` to settle Permit2 yourself.`
4293
+ };
4252
4294
  }
4253
- if (!info) return void 0;
4254
- const mode = cfg.settle === "self" ? { kind: "self", relayer } : {
4295
+ const mode = settle === "self" ? { kind: "self", relayer } : {
4255
4296
  kind: "facilitator",
4256
- url: cfg.settle.facilitator,
4257
- ...cfg.settle.authHeaders ? { authHeaders: cfg.settle.authHeaders } : {}
4297
+ url: settle.facilitator,
4298
+ ...settle.authHeaders ? { authHeaders: settle.authHeaders } : {}
4258
4299
  };
4259
- return { method: info.method, ...info.extra ? { extra: info.extra } : {}, mode };
4300
+ return { rail: { method: info.method, ...info.extra ? { extra: info.extra } : {}, mode } };
4260
4301
  }
4261
4302
  const hasCustomStore = Boolean(options.isUsed || options.markUsed);
4262
4303
  const localUsed = /* @__PURE__ */ new Map();
package/dist/index.d.cts CHANGED
@@ -5545,7 +5545,7 @@ declare function formatSpendReport(summary: SpendSummary): string;
5545
5545
  * literally, so a wrong name or order actively misleads. A test pins the load-
5546
5546
  * bearing phrases.
5547
5547
  */
5548
- declare const PIPRAIL_AGENT_GUIDE = "# Paying with PipRail \u2014 the agent contract\n\nYou can pay for x402 \"402 Payment Required\" resources autonomously. Money moves\nstraight from your wallet to the server; PipRail custodies nothing. Follow this.\n\n## The loop: quote \u2192 plan \u2192 pay\n1. piprail_quote_payment(url) \u2014 PRICE it. Returns the amount, token, chain, and\n whether it is within your spend policy. No funds move. Use it to decide if a\n resource is worth buying.\n2. piprail_plan_payment(url) \u2014 can I afford it NOW? Reads your balance, native gas,\n and recipient-readiness across every rail, and returns { payable, best,\n fundingHint, session? }. If payable is false, do NOT attempt the payment \u2014\n fundingHint says exactly what to fix.\n3. piprail_pay_request(url, method?, body?) \u2014 PAY (only if the plan was payable)\n and return the result.\nAlways plan before you pay so you never commit to a payment you cannot finish.\n\n## Reading a refusal \u2014 never crash, never double-spend\nA failed pay returns a STRUCTURED object, never a thrown error you must catch:\n { ok:false, code, reason, explain, ref?, reasonCode?, declined? }\nBranch on `code` (always reliable). Key cases:\n- declined:true with reasonCode:'SESSION_EXPIRED' \u2014 your time budget is over. This\n is TERMINAL: STOP. Do not retry ANY payment this process; it cannot be undone\n without a restart / a longer TTL.\n- declined:true with reasonCode:'APPROVAL' \u2014 a human (or hook) declined this\n payment. Terminal for this pay: do NOT auto-retry \u2014 they said no, or no one\n answered.\n- declined:true with reasonCode:'OUTSIDE_WINDOW' \u2014 your rolling rate-limit is\n exhausted. Wait for it to free, then retry; do not raise the amount.\n- declined:true with reasonCode:'POLICY' or 'BUDGET' \u2014 a spend cap or allowlist\n refused it. Don't retry the same payment; pick a cheaper/allowed one.\n- code:'INSUFFICIENT_FUNDS' \u2014 top up the wallet (token and/or native gas), retry.\n- code:'PAYMENT_TIMEOUT' / 'MAX_RETRIES_EXCEEDED' / 'CONFIRMATION_TIMEOUT' \u2014 the\n payment may ALREADY be on-chain. Recover using the proof on `.ref` (re-verify\n or re-submit it); never re-pay \u2014 a fresh payment would double-spend.\n- code:'NO_COMPATIBLE_ACCEPT' / 'UNSUPPORTED_SCHEME' \u2014 the 402 isn't payable on\n your chain/scheme; `explain` says whether it's the wrong chain or a scheme to enable.\n\n## Knowing your leash \u2014 call piprail_budget\npiprail_budget tells you how much budget and time you have left, per\n(network, asset), plus your spend so far. Read-only; moves no funds. Use it in\nMode A to self-check before paying.\n\n## Two modes\n- Mode A (headless, default): you run FREE inside a pre-set budget + time\n envelope. The policy IS the consent \u2014 there is no per-payment prompt. Stay\n inside it; piprail_budget shows what's left.\n- Mode B (supervised): the host may ask a human to approve each payment. A\n decline/cancel/timeout comes back as declined:true (reasonCode:'APPROVAL') \u2014\n do NOT retry it as if it were a transient error.\n\n## Hard facts\n- Spend caps are PER (network, asset). There is no single cross-token dollar cap \u2014\n budgets aren't summed across tokens (no price oracle).\n- Spend totals and the time envelope live IN-MEMORY for THIS process; they reset on restart\n (a convenience, not a durable ledger).\n";
5548
+ declare const PIPRAIL_AGENT_GUIDE = "# Paying with PipRail \u2014 the agent contract\n\nYou can pay for x402 \"402 Payment Required\" resources autonomously. Money moves\nstraight from your wallet to the server; PipRail custodies nothing. Follow this.\n\n## The loop: quote \u2192 plan \u2192 pay\n1. piprail_quote_payment(url) \u2014 PRICE it. Returns the amount, token, chain, and\n whether it is within your spend policy. No funds move. Use it to decide if a\n resource is worth buying.\n2. piprail_plan_payment(url) \u2014 can I afford it NOW? Reads your balance, native gas,\n and recipient-readiness across every rail, and returns { payable, best,\n fundingHint, session? }. If payable is false, do NOT attempt the payment \u2014\n fundingHint says exactly what to fix.\n3. piprail_pay_request(url, method?, body?) \u2014 PAY (only if the plan was payable)\n and return the result.\nAlways plan before you pay so you never commit to a payment you cannot finish.\n\n## Gasless \u2014 the exact rail (zero gas for you)\nA 402 may offer up to two rails; you don't choose per payment \u2014 the client does, automatically:\n- onchain-proof (PipRail's default): you broadcast the payment yourself and pay the network gas\n (the native coin \u2014 ETH/SOL/\u2026). Works on every chain.\n- exact (the ratified x402 rail, opt-in): you only SIGN; the server \u2014 or a facilitator it chose\n (e.g. PayAI) \u2014 broadcasts it, so you pay ZERO gas (you need only the token, no native coin). It\n works on EVM + Solana, and the on-chain method (EIP-3009 / Permit2 / SVM) is picked automatically.\nWhen the exact scheme is enabled AND balance-aware routing is on, paying picks the cheapest\nsettleable rail \u2014 i.e. the gasless exact one. Nothing changes in your loop: quote \u2192 plan \u2192 pay is\nidentical. The exact scheme is OPT-IN by the operator (MCP: PIPRAIL_SCHEMES=onchain-proof,exact);\nyou can't enable it yourself, but you can report when a 402 needs it (see UNSUPPORTED_SCHEME below).\n\n## Reading a refusal \u2014 never crash, never double-spend\nA failed pay returns a STRUCTURED object, never a thrown error you must catch:\n { ok:false, code, reason, explain, ref?, reasonCode?, declined? }\nBranch on `code` (always reliable). Key cases:\n- declined:true with reasonCode:'SESSION_EXPIRED' \u2014 your time budget is over. This\n is TERMINAL: STOP. Do not retry ANY payment this process; it cannot be undone\n without a restart / a longer TTL.\n- declined:true with reasonCode:'APPROVAL' \u2014 a human (or hook) declined this\n payment. Terminal for this pay: do NOT auto-retry \u2014 they said no, or no one\n answered.\n- declined:true with reasonCode:'OUTSIDE_WINDOW' \u2014 your rolling rate-limit is\n exhausted. Wait for it to free, then retry; do not raise the amount.\n- declined:true with reasonCode:'POLICY' or 'BUDGET' \u2014 a spend cap or allowlist\n refused it. Don't retry the same payment; pick a cheaper/allowed one.\n- code:'INSUFFICIENT_FUNDS' \u2014 top up the wallet (token and/or native gas), retry.\n- code:'PAYMENT_TIMEOUT' / 'MAX_RETRIES_EXCEEDED' / 'CONFIRMATION_TIMEOUT' \u2014 the\n payment may ALREADY be on-chain. Recover using the proof on `.ref` (re-verify\n or re-submit it); never re-pay \u2014 a fresh payment would double-spend. On a gasless\n exact rail `.ref` is the authorization NONCE, not a tx hash: re-present the SAME\n signed authorization, never sign a fresh one (that would risk a double-spend).\n- code:'NO_COMPATIBLE_ACCEPT' / 'UNSUPPORTED_SCHEME' \u2014 the 402 isn't payable on\n your chain/scheme; `explain` says whether it's the wrong chain or a scheme to enable.\n If it's a standard x402 server offering an exact rail, that's a config fix the operator makes\n once (enable the exact scheme); report it, don't retry the same call blindly.\n\n## Knowing your leash \u2014 call piprail_budget\npiprail_budget tells you how much budget and time you have left, per\n(network, asset), plus your spend so far. Read-only; moves no funds. Use it in\nMode A to self-check before paying.\n\n## Two modes\n- Mode A (headless, default): you run FREE inside a pre-set budget + time\n envelope. The policy IS the consent \u2014 there is no per-payment prompt. Stay\n inside it; piprail_budget shows what's left.\n- Mode B (supervised): the host may ask a human to approve each payment. A\n decline/cancel/timeout comes back as declined:true (reasonCode:'APPROVAL') \u2014\n do NOT retry it as if it were a transient error.\n\n## Hard facts\n- Spend caps are PER (network, asset). There is no single cross-token dollar cap \u2014\n budgets aren't summed across tokens (no price oracle).\n- Spend totals and the time envelope live IN-MEMORY for THIS process; they reset on restart\n (a convenience, not a durable ledger).\n";
5549
5549
  /** Returns {@link PIPRAIL_AGENT_GUIDE} (a parity accessor for callers that prefer a function). */
5550
5550
  declare function agentGuide(): string;
5551
5551
 
package/dist/index.d.ts CHANGED
@@ -5545,7 +5545,7 @@ declare function formatSpendReport(summary: SpendSummary): string;
5545
5545
  * literally, so a wrong name or order actively misleads. A test pins the load-
5546
5546
  * bearing phrases.
5547
5547
  */
5548
- declare const PIPRAIL_AGENT_GUIDE = "# Paying with PipRail \u2014 the agent contract\n\nYou can pay for x402 \"402 Payment Required\" resources autonomously. Money moves\nstraight from your wallet to the server; PipRail custodies nothing. Follow this.\n\n## The loop: quote \u2192 plan \u2192 pay\n1. piprail_quote_payment(url) \u2014 PRICE it. Returns the amount, token, chain, and\n whether it is within your spend policy. No funds move. Use it to decide if a\n resource is worth buying.\n2. piprail_plan_payment(url) \u2014 can I afford it NOW? Reads your balance, native gas,\n and recipient-readiness across every rail, and returns { payable, best,\n fundingHint, session? }. If payable is false, do NOT attempt the payment \u2014\n fundingHint says exactly what to fix.\n3. piprail_pay_request(url, method?, body?) \u2014 PAY (only if the plan was payable)\n and return the result.\nAlways plan before you pay so you never commit to a payment you cannot finish.\n\n## Reading a refusal \u2014 never crash, never double-spend\nA failed pay returns a STRUCTURED object, never a thrown error you must catch:\n { ok:false, code, reason, explain, ref?, reasonCode?, declined? }\nBranch on `code` (always reliable). Key cases:\n- declined:true with reasonCode:'SESSION_EXPIRED' \u2014 your time budget is over. This\n is TERMINAL: STOP. Do not retry ANY payment this process; it cannot be undone\n without a restart / a longer TTL.\n- declined:true with reasonCode:'APPROVAL' \u2014 a human (or hook) declined this\n payment. Terminal for this pay: do NOT auto-retry \u2014 they said no, or no one\n answered.\n- declined:true with reasonCode:'OUTSIDE_WINDOW' \u2014 your rolling rate-limit is\n exhausted. Wait for it to free, then retry; do not raise the amount.\n- declined:true with reasonCode:'POLICY' or 'BUDGET' \u2014 a spend cap or allowlist\n refused it. Don't retry the same payment; pick a cheaper/allowed one.\n- code:'INSUFFICIENT_FUNDS' \u2014 top up the wallet (token and/or native gas), retry.\n- code:'PAYMENT_TIMEOUT' / 'MAX_RETRIES_EXCEEDED' / 'CONFIRMATION_TIMEOUT' \u2014 the\n payment may ALREADY be on-chain. Recover using the proof on `.ref` (re-verify\n or re-submit it); never re-pay \u2014 a fresh payment would double-spend.\n- code:'NO_COMPATIBLE_ACCEPT' / 'UNSUPPORTED_SCHEME' \u2014 the 402 isn't payable on\n your chain/scheme; `explain` says whether it's the wrong chain or a scheme to enable.\n\n## Knowing your leash \u2014 call piprail_budget\npiprail_budget tells you how much budget and time you have left, per\n(network, asset), plus your spend so far. Read-only; moves no funds. Use it in\nMode A to self-check before paying.\n\n## Two modes\n- Mode A (headless, default): you run FREE inside a pre-set budget + time\n envelope. The policy IS the consent \u2014 there is no per-payment prompt. Stay\n inside it; piprail_budget shows what's left.\n- Mode B (supervised): the host may ask a human to approve each payment. A\n decline/cancel/timeout comes back as declined:true (reasonCode:'APPROVAL') \u2014\n do NOT retry it as if it were a transient error.\n\n## Hard facts\n- Spend caps are PER (network, asset). There is no single cross-token dollar cap \u2014\n budgets aren't summed across tokens (no price oracle).\n- Spend totals and the time envelope live IN-MEMORY for THIS process; they reset on restart\n (a convenience, not a durable ledger).\n";
5548
+ declare const PIPRAIL_AGENT_GUIDE = "# Paying with PipRail \u2014 the agent contract\n\nYou can pay for x402 \"402 Payment Required\" resources autonomously. Money moves\nstraight from your wallet to the server; PipRail custodies nothing. Follow this.\n\n## The loop: quote \u2192 plan \u2192 pay\n1. piprail_quote_payment(url) \u2014 PRICE it. Returns the amount, token, chain, and\n whether it is within your spend policy. No funds move. Use it to decide if a\n resource is worth buying.\n2. piprail_plan_payment(url) \u2014 can I afford it NOW? Reads your balance, native gas,\n and recipient-readiness across every rail, and returns { payable, best,\n fundingHint, session? }. If payable is false, do NOT attempt the payment \u2014\n fundingHint says exactly what to fix.\n3. piprail_pay_request(url, method?, body?) \u2014 PAY (only if the plan was payable)\n and return the result.\nAlways plan before you pay so you never commit to a payment you cannot finish.\n\n## Gasless \u2014 the exact rail (zero gas for you)\nA 402 may offer up to two rails; you don't choose per payment \u2014 the client does, automatically:\n- onchain-proof (PipRail's default): you broadcast the payment yourself and pay the network gas\n (the native coin \u2014 ETH/SOL/\u2026). Works on every chain.\n- exact (the ratified x402 rail, opt-in): you only SIGN; the server \u2014 or a facilitator it chose\n (e.g. PayAI) \u2014 broadcasts it, so you pay ZERO gas (you need only the token, no native coin). It\n works on EVM + Solana, and the on-chain method (EIP-3009 / Permit2 / SVM) is picked automatically.\nWhen the exact scheme is enabled AND balance-aware routing is on, paying picks the cheapest\nsettleable rail \u2014 i.e. the gasless exact one. Nothing changes in your loop: quote \u2192 plan \u2192 pay is\nidentical. The exact scheme is OPT-IN by the operator (MCP: PIPRAIL_SCHEMES=onchain-proof,exact);\nyou can't enable it yourself, but you can report when a 402 needs it (see UNSUPPORTED_SCHEME below).\n\n## Reading a refusal \u2014 never crash, never double-spend\nA failed pay returns a STRUCTURED object, never a thrown error you must catch:\n { ok:false, code, reason, explain, ref?, reasonCode?, declined? }\nBranch on `code` (always reliable). Key cases:\n- declined:true with reasonCode:'SESSION_EXPIRED' \u2014 your time budget is over. This\n is TERMINAL: STOP. Do not retry ANY payment this process; it cannot be undone\n without a restart / a longer TTL.\n- declined:true with reasonCode:'APPROVAL' \u2014 a human (or hook) declined this\n payment. Terminal for this pay: do NOT auto-retry \u2014 they said no, or no one\n answered.\n- declined:true with reasonCode:'OUTSIDE_WINDOW' \u2014 your rolling rate-limit is\n exhausted. Wait for it to free, then retry; do not raise the amount.\n- declined:true with reasonCode:'POLICY' or 'BUDGET' \u2014 a spend cap or allowlist\n refused it. Don't retry the same payment; pick a cheaper/allowed one.\n- code:'INSUFFICIENT_FUNDS' \u2014 top up the wallet (token and/or native gas), retry.\n- code:'PAYMENT_TIMEOUT' / 'MAX_RETRIES_EXCEEDED' / 'CONFIRMATION_TIMEOUT' \u2014 the\n payment may ALREADY be on-chain. Recover using the proof on `.ref` (re-verify\n or re-submit it); never re-pay \u2014 a fresh payment would double-spend. On a gasless\n exact rail `.ref` is the authorization NONCE, not a tx hash: re-present the SAME\n signed authorization, never sign a fresh one (that would risk a double-spend).\n- code:'NO_COMPATIBLE_ACCEPT' / 'UNSUPPORTED_SCHEME' \u2014 the 402 isn't payable on\n your chain/scheme; `explain` says whether it's the wrong chain or a scheme to enable.\n If it's a standard x402 server offering an exact rail, that's a config fix the operator makes\n once (enable the exact scheme); report it, don't retry the same call blindly.\n\n## Knowing your leash \u2014 call piprail_budget\npiprail_budget tells you how much budget and time you have left, per\n(network, asset), plus your spend so far. Read-only; moves no funds. Use it in\nMode A to self-check before paying.\n\n## Two modes\n- Mode A (headless, default): you run FREE inside a pre-set budget + time\n envelope. The policy IS the consent \u2014 there is no per-payment prompt. Stay\n inside it; piprail_budget shows what's left.\n- Mode B (supervised): the host may ask a human to approve each payment. A\n decline/cancel/timeout comes back as declined:true (reasonCode:'APPROVAL') \u2014\n do NOT retry it as if it were a transient error.\n\n## Hard facts\n- Spend caps are PER (network, asset). There is no single cross-token dollar cap \u2014\n budgets aren't summed across tokens (no price oracle).\n- Spend totals and the time envelope live IN-MEMORY for THIS process; they reset on restart\n (a convenience, not a durable ledger).\n";
5549
5549
  /** Returns {@link PIPRAIL_AGENT_GUIDE} (a parity accessor for callers that prefer a function). */
5550
5550
  declare function agentGuide(): string;
5551
5551
 
package/dist/index.js CHANGED
@@ -666,10 +666,14 @@ async function payExactEvm(input) {
666
666
  `exact buyer rail requires an EOA signer; ${account.address} is a contract / EIP-1271 / EIP-7702-delegated account (no recoverable ECDSA signature). Pay via onchain-proof.`
667
667
  );
668
668
  }
669
- const domain = await readExactDomain(publicClient, accept.asset);
669
+ let domain = await readExactDomain(publicClient, accept.asset);
670
+ if (!domain) {
671
+ await new Promise((r) => setTimeout(r, 300));
672
+ domain = await readExactDomain(publicClient, accept.asset);
673
+ }
670
674
  if (!domain) {
671
675
  throw new UnsupportedSchemeError(
672
- `exact: ${accept.asset} on ${accept.network} isn't an EIP-3009 token (USDT needs Permit2; native coin and plain ERC-20s aren't exact-payable). Pay via onchain-proof.`
676
+ `exact: couldn't derive the EIP-712 domain for ${accept.asset} on ${accept.network}. Either it isn't an EIP-3009 token (USDT needs Permit2; native/plain ERC-20 aren't exact-payable) \u2014 pay via onchain-proof \u2014 OR your RPC couldn't read it (transient): if the gate advertised eip3009, pass a reliable rpcUrl and retry.`
673
677
  );
674
678
  }
675
679
  const g = globalThis.crypto;
@@ -1791,7 +1795,7 @@ var loaders = {
1791
1795
  solana: async () => {
1792
1796
  let mod;
1793
1797
  try {
1794
- mod = await import("./solana-4EMMGGDR.js");
1798
+ mod = await import("./solana-IBVUZS54.js");
1795
1799
  } catch (cause) {
1796
1800
  throw new MissingDriverError(
1797
1801
  `Solana selected, but its packages aren't installed. Run: npm install @solana/web3.js @solana/spl-token bs58`,
@@ -3647,6 +3651,18 @@ straight from your wallet to the server; PipRail custodies nothing. Follow this.
3647
3651
  and return the result.
3648
3652
  Always plan before you pay so you never commit to a payment you cannot finish.
3649
3653
 
3654
+ ## Gasless \u2014 the exact rail (zero gas for you)
3655
+ A 402 may offer up to two rails; you don't choose per payment \u2014 the client does, automatically:
3656
+ - onchain-proof (PipRail's default): you broadcast the payment yourself and pay the network gas
3657
+ (the native coin \u2014 ETH/SOL/\u2026). Works on every chain.
3658
+ - exact (the ratified x402 rail, opt-in): you only SIGN; the server \u2014 or a facilitator it chose
3659
+ (e.g. PayAI) \u2014 broadcasts it, so you pay ZERO gas (you need only the token, no native coin). It
3660
+ works on EVM + Solana, and the on-chain method (EIP-3009 / Permit2 / SVM) is picked automatically.
3661
+ When the exact scheme is enabled AND balance-aware routing is on, paying picks the cheapest
3662
+ settleable rail \u2014 i.e. the gasless exact one. Nothing changes in your loop: quote \u2192 plan \u2192 pay is
3663
+ identical. The exact scheme is OPT-IN by the operator (MCP: PIPRAIL_SCHEMES=onchain-proof,exact);
3664
+ you can't enable it yourself, but you can report when a 402 needs it (see UNSUPPORTED_SCHEME below).
3665
+
3650
3666
  ## Reading a refusal \u2014 never crash, never double-spend
3651
3667
  A failed pay returns a STRUCTURED object, never a thrown error you must catch:
3652
3668
  { ok:false, code, reason, explain, ref?, reasonCode?, declined? }
@@ -3664,9 +3680,13 @@ Branch on \`code\` (always reliable). Key cases:
3664
3680
  - code:'INSUFFICIENT_FUNDS' \u2014 top up the wallet (token and/or native gas), retry.
3665
3681
  - code:'PAYMENT_TIMEOUT' / 'MAX_RETRIES_EXCEEDED' / 'CONFIRMATION_TIMEOUT' \u2014 the
3666
3682
  payment may ALREADY be on-chain. Recover using the proof on \`.ref\` (re-verify
3667
- or re-submit it); never re-pay \u2014 a fresh payment would double-spend.
3683
+ or re-submit it); never re-pay \u2014 a fresh payment would double-spend. On a gasless
3684
+ exact rail \`.ref\` is the authorization NONCE, not a tx hash: re-present the SAME
3685
+ signed authorization, never sign a fresh one (that would risk a double-spend).
3668
3686
  - code:'NO_COMPATIBLE_ACCEPT' / 'UNSUPPORTED_SCHEME' \u2014 the 402 isn't payable on
3669
3687
  your chain/scheme; \`explain\` says whether it's the wrong chain or a scheme to enable.
3688
+ If it's a standard x402 server offering an exact rail, that's a config fix the operator makes
3689
+ once (enable the exact scheme); report it, don't retry the same call blindly.
3670
3690
 
3671
3691
  ## Knowing your leash \u2014 call piprail_budget
3672
3692
  piprail_budget tells you how much budget and time you have left, per
@@ -4199,6 +4219,7 @@ function createPaymentGate(options) {
4199
4219
  if (resolved) return resolved;
4200
4220
  const p = (async () => {
4201
4221
  const accepts = normaliseAccepts(options);
4222
+ const exactSkips = [];
4202
4223
  const specs = await Promise.all(
4203
4224
  accepts.map(async (a) => {
4204
4225
  const net = await resolveNetwork2({ chain: a.chain, rpcUrl: a.rpcUrl ?? options.rpcUrl });
@@ -4212,13 +4233,17 @@ function createPaymentGate(options) {
4212
4233
  const { asset, decimals, symbol } = net.resolveToken(a.token);
4213
4234
  const amountBase = parseUnits(a.amount, decimals);
4214
4235
  const spec = { net, asset, decimals, symbol, amountBase, amountFormatted: a.amount, payTo };
4215
- if (options.exact) spec.exact = await resolveExactRail(net, asset);
4236
+ if (options.exact) {
4237
+ const outcome = await resolveExactRail(net, asset);
4238
+ if (outcome.rail) spec.exact = outcome.rail;
4239
+ else if (outcome.skipReason) exactSkips.push(outcome.skipReason);
4240
+ }
4216
4241
  return spec;
4217
4242
  })
4218
4243
  );
4219
4244
  if (options.exact && !specs.some((s) => s.exact)) {
4220
4245
  throw new Error(
4221
- "requirePayment: `exact` was requested but none of the offered rails support it. The standard `exact` rail is EVM ERC-20 (EIP-3009 \u2014 USDC / EURC \u2014 or Permit2, e.g. Binance-Peg USDC on BNB) or a Solana SPL token (SVM) \u2014 NOT native coins, NOT families without a standard `exact` scheme. Offer an EVM ERC-20 / Solana SPL token, or drop `exact`."
4246
+ "requirePayment: `exact` was requested but none of the offered rails support it. " + (exactSkips.length > 0 ? exactSkips.join(" ") : "The standard `exact` rail is EVM ERC-20 (EIP-3009 \u2014 USDC / EURC \u2014 or Permit2, e.g. Binance-Peg USDC on BNB) or a Solana SPL token (SVM) \u2014 NOT native coins, NOT families without a standard `exact` scheme. Offer an EVM ERC-20 / Solana SPL token, or drop `exact`.")
4222
4247
  );
4223
4248
  }
4224
4249
  return specs;
@@ -4231,10 +4256,11 @@ function createPaymentGate(options) {
4231
4256
  }
4232
4257
  async function resolveExactRail(net, asset) {
4233
4258
  const cfg = options.exact;
4234
- if (!net.resolveExactRail) return void 0;
4259
+ const settle = cfg.settle;
4260
+ if (!net.resolveExactRail) return {};
4235
4261
  let relayer;
4236
4262
  let feePayer;
4237
- if (cfg.settle === "self") {
4263
+ if (settle === "self") {
4238
4264
  if (cfg.relayer === void 0) {
4239
4265
  throw new Error(
4240
4266
  "requirePayment: exact `settle: 'self'` needs a `relayer` wallet (the gas-paying key that broadcasts the settle), e.g. exact: { settle: 'self', relayer: { privateKey } }."
@@ -4242,21 +4268,36 @@ function createPaymentGate(options) {
4242
4268
  }
4243
4269
  relayer = net.bindWallet(cfg.relayer);
4244
4270
  } else {
4245
- feePayer = cfg.settle.feePayer;
4271
+ feePayer = settle.feePayer;
4246
4272
  }
4247
4273
  const method = cfg.method ?? "auto";
4248
4274
  let info = await net.resolveExactRail({ asset, method, relayer, feePayer });
4249
- if (!info && cfg.settle !== "self" && !feePayer) {
4250
- const discovered = await fetchFacilitatorFeePayer(cfg.settle.facilitator, net.network);
4251
- if (discovered) info = await net.resolveExactRail({ asset, method, relayer, feePayer: discovered });
4275
+ if (!info && settle !== "self" && !feePayer && asset !== "native") {
4276
+ const discovered = await fetchFacilitatorFeePayer(settle.facilitator, net.network);
4277
+ if (!discovered) {
4278
+ return {
4279
+ skipReason: `${net.network}: couldn't read a fee payer from the facilitator (${settle.facilitator}/supported) \u2014 it may be down, or may not sponsor this network. Set \`exact.settle.feePayer\` explicitly to remove the runtime dependency, switch to \`settle: 'self'\` with your own relayer, or retry.`
4280
+ };
4281
+ }
4282
+ info = await net.resolveExactRail({ asset, method, relayer, feePayer: discovered });
4283
+ }
4284
+ if (!info) return {};
4285
+ if (settle !== "self" && info.method === "permit2") {
4286
+ if (cfg.method === "permit2") {
4287
+ throw new Error(
4288
+ "requirePayment: exact `method: 'permit2'` can't be settled by a third-party facilitator \u2014 facilitators settle the standard EIP-3009 (EVM) / SVM (Solana) schemes, not PipRail\u2019s Permit2 proxy. Use an EIP-3009 token (USDC / EURC) with the facilitator, or `settle: 'self'` (your own relayer) to settle Permit2 yourself."
4289
+ );
4290
+ }
4291
+ return {
4292
+ skipReason: `${net.network}: this token isn't EIP-3009 (auto-selected Permit2), which a third-party facilitator can't settle \u2014 it would serve onchain-proof only here. Use an EIP-3009 token (USDC / EURC) with the facilitator, or \`settle: 'self'\` to settle Permit2 yourself.`
4293
+ };
4252
4294
  }
4253
- if (!info) return void 0;
4254
- const mode = cfg.settle === "self" ? { kind: "self", relayer } : {
4295
+ const mode = settle === "self" ? { kind: "self", relayer } : {
4255
4296
  kind: "facilitator",
4256
- url: cfg.settle.facilitator,
4257
- ...cfg.settle.authHeaders ? { authHeaders: cfg.settle.authHeaders } : {}
4297
+ url: settle.facilitator,
4298
+ ...settle.authHeaders ? { authHeaders: settle.authHeaders } : {}
4258
4299
  };
4259
- return { method: info.method, ...info.extra ? { extra: info.extra } : {}, mode };
4300
+ return { rail: { method: info.method, ...info.extra ? { extra: info.extra } : {}, mode } };
4260
4301
  }
4261
4302
  const hasCustomStore = Boolean(options.isUsed || options.markUsed);
4262
4303
  const localUsed = /* @__PURE__ */ new Map();
@@ -624,6 +624,15 @@ function makeSolanaNetwork(preset, rpcUrl) {
624
624
  return { height: String(info.slot) };
625
625
  },
626
626
  async estimateCost(accept) {
627
+ if (accept.scheme === "exact") {
628
+ return nativeCost({
629
+ symbol: "SOL",
630
+ decimals: SOL_DECIMALS,
631
+ fee: 0n,
632
+ basis: "estimated",
633
+ detail: "gasless \u2014 the fee payer (facilitator/relayer) broadcasts and pays the SOL fee"
634
+ });
635
+ }
627
636
  const base = 5000n;
628
637
  if (accept.asset === "native") {
629
638
  return nativeCost({
@@ -624,6 +624,15 @@ function makeSolanaNetwork(preset, rpcUrl) {
624
624
  return { height: String(info.slot) };
625
625
  },
626
626
  async estimateCost(accept) {
627
+ if (accept.scheme === "exact") {
628
+ return _chunkPA6YD3HLcjs.nativeCost.call(void 0, {
629
+ symbol: "SOL",
630
+ decimals: SOL_DECIMALS,
631
+ fee: 0n,
632
+ basis: "estimated",
633
+ detail: "gasless \u2014 the fee payer (facilitator/relayer) broadcasts and pays the SOL fee"
634
+ });
635
+ }
627
636
  const base = 5000n;
628
637
  if (accept.asset === "native") {
629
638
  return _chunkPA6YD3HLcjs.nativeCost.call(void 0, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@piprail/sdk",
3
- "version": "1.21.0",
3
+ "version": "1.21.1",
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",