@piprail/sdk 1.13.1 → 1.15.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.
Files changed (30) hide show
  1. package/CHAINS.md +3 -2
  2. package/CHANGELOG.md +69 -0
  3. package/ERRORS.md +17 -2
  4. package/README.md +57 -4
  5. package/STANDARDS.md +4 -0
  6. package/dist/{algorand-MXUSKX46.cjs → algorand-EJ3S2V7E.cjs} +17 -17
  7. package/dist/{algorand-WGVF4KTU.js → algorand-F3OYB534.js} +1 -1
  8. package/dist/{aptos-YT7SXWPF.cjs → aptos-GJGIZHNI.cjs} +16 -16
  9. package/dist/{aptos-LPBLSEIQ.js → aptos-SUXOVP7B.js} +1 -1
  10. package/dist/{chunk-SVMGHASK.js → chunk-ILPABTI2.js} +18 -1
  11. package/dist/{chunk-MDLZJGLY.cjs → chunk-PA6YD3HL.cjs} +35 -18
  12. package/dist/index.cjs +800 -163
  13. package/dist/index.d.cts +472 -42
  14. package/dist/index.d.ts +472 -42
  15. package/dist/index.js +695 -58
  16. package/dist/{near-K6BDBABG.js → near-LM7S3WUD.js} +1 -1
  17. package/dist/{near-7ZDNISUX.cjs → near-ZJLZE26R.cjs} +19 -19
  18. package/dist/{solana-PU7N2M64.cjs → solana-MPPE6K24.cjs} +14 -14
  19. package/dist/{solana-S3UFI3FE.js → solana-WDKWWF33.js} +1 -1
  20. package/dist/{stellar-Q5PO23SC.js → stellar-FIJPQZVW.js} +1 -1
  21. package/dist/{stellar-VDQOFQEO.cjs → stellar-XHLLNHQP.cjs} +21 -21
  22. package/dist/{sui-FKSMLKRF.cjs → sui-6CVLEXLA.cjs} +17 -17
  23. package/dist/{sui-WOXRKJXS.js → sui-B7AVN7NK.js} +1 -1
  24. package/dist/{ton-WPTXGLVK.js → ton-CHJ26BVA.js} +1 -1
  25. package/dist/{ton-VK6KRJHP.cjs → ton-RNEFN25G.cjs} +14 -14
  26. package/dist/{tron-6GXBXTR4.js → tron-DD3JDROV.js} +1 -1
  27. package/dist/{tron-WLOF5OUV.cjs → tron-TKJHNFGM.cjs} +24 -24
  28. package/dist/{xrpl-HEAPEXAM.js → xrpl-GTUPP6SK.js} +1 -1
  29. package/dist/{xrpl-CMNI25BV.cjs → xrpl-XN2NBNGI.cjs} +21 -21
  30. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -13,6 +13,7 @@ import {
13
13
  SettlementError,
14
14
  UnknownTokenError,
15
15
  UnsupportedNetworkError,
16
+ UnsupportedSchemeError,
16
17
  WrongChainError,
17
18
  WrongFamilyError,
18
19
  floorUnits,
@@ -21,7 +22,7 @@ import {
21
22
  parseUnits,
22
23
  rejectForeignToken,
23
24
  toInsufficientFundsError
24
- } from "./chunk-SVMGHASK.js";
25
+ } from "./chunk-ILPABTI2.js";
25
26
 
26
27
  // src/drivers/registry.ts
27
28
  var byFamily = /* @__PURE__ */ new Map();
@@ -97,13 +98,18 @@ var CHAINS = {
97
98
  defaultRpc: "https://ethereum-rpc.publicnode.com",
98
99
  tokens: {
99
100
  USDC: { address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", decimals: 6, symbol: "USDC" },
100
- USDT: { address: "0xdAC17F958D2ee523a2206206994597C13D831ec7", decimals: 6, symbol: "USDT" }
101
+ USDT: { address: "0xdAC17F958D2ee523a2206206994597C13D831ec7", decimals: 6, symbol: "USDT" },
102
+ // Circle EURC — EIP-3009 (exact-payable). On-chain EIP-712 domain name is "Euro Coin" here
103
+ // (NOT "EURC"); the buyer re-derives it on-chain, so the symbol below is display-only.
104
+ EURC: { address: "0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c", decimals: 6, symbol: "EURC" }
101
105
  }
102
106
  },
103
107
  base: {
104
108
  chain: base,
105
109
  tokens: {
106
- USDC: { address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", decimals: 6, symbol: "USDC" }
110
+ USDC: { address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", decimals: 6, symbol: "USDC" },
111
+ // Circle EURC — EIP-3009 (exact-payable). On-chain EIP-712 domain name is "EURC" here.
112
+ EURC: { address: "0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42", decimals: 6, symbol: "EURC" }
107
113
  }
108
114
  },
109
115
  arbitrum: {
@@ -139,7 +145,9 @@ var CHAINS = {
139
145
  chain: avalanche,
140
146
  tokens: {
141
147
  USDC: { address: "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E", decimals: 6, symbol: "USDC" },
142
- USDT: { address: "0x9702230A8Ea53601f5cD2dc00fDBc13d4dF4A8c7", decimals: 6, symbol: "USDT" }
148
+ USDT: { address: "0x9702230A8Ea53601f5cD2dc00fDBc13d4dF4A8c7", decimals: 6, symbol: "USDT" },
149
+ // Circle EURC — EIP-3009 (exact-payable). On-chain EIP-712 domain name is "Euro Coin" here.
150
+ EURC: { address: "0xC891EB4cbdEFf6e073e859e987815Ed1505c2ACD", decimals: 6, symbol: "EURC" }
143
151
  }
144
152
  },
145
153
  // ── More popular EVM mainnets. Every address below was verified on-chain
@@ -591,6 +599,53 @@ function encodeXPaymentHeader(input) {
591
599
  };
592
600
  return base64(JSON.stringify(payload));
593
601
  }
602
+ async function payExactEvm(input) {
603
+ const { publicClient, walletClient, account, chainId, accept } = input;
604
+ let code;
605
+ try {
606
+ code = await publicClient.getCode({ address: account.address });
607
+ } catch {
608
+ code = void 0;
609
+ }
610
+ if (code && code !== "0x") {
611
+ throw new UnsupportedSchemeError(
612
+ `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.`
613
+ );
614
+ }
615
+ const domain = await readExactDomain(publicClient, accept.asset);
616
+ if (!domain) {
617
+ throw new UnsupportedSchemeError(
618
+ `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.`
619
+ );
620
+ }
621
+ const g = globalThis.crypto;
622
+ if (!g?.getRandomValues) {
623
+ throw new UnsupportedSchemeError(
624
+ "this runtime lacks Web Crypto (globalThis.crypto.getRandomValues); the exact rail needs a CSPRNG nonce."
625
+ );
626
+ }
627
+ const raw = new Uint8Array(32);
628
+ g.getRandomValues(raw);
629
+ const nonce = `0x${[...raw].map((b) => b.toString(16).padStart(2, "0")).join("")}`;
630
+ const now = Math.floor(Date.now() / 1e3);
631
+ const from = account.address;
632
+ const to = getAddress2(accept.payTo);
633
+ const value = accept.amount;
634
+ const validAfter = "0";
635
+ const validBefore = String(now + accept.maxTimeoutSeconds);
636
+ const signature = await walletClient.signTypedData({
637
+ account,
638
+ domain: { name: domain.name, version: domain.version, chainId, verifyingContract: getAddress2(accept.asset) },
639
+ types: EIP3009_TYPES,
640
+ primaryType: "TransferWithAuthorization",
641
+ message: { from, to, value: BigInt(value), validAfter: 0n, validBefore: BigInt(validBefore), nonce }
642
+ });
643
+ return {
644
+ payload: { signature, authorization: { from, to, value, validAfter, validBefore, nonce } },
645
+ payerFrom: from,
646
+ nonce
647
+ };
648
+ }
594
649
  var eip3009Abi = [
595
650
  {
596
651
  type: "function",
@@ -873,6 +928,9 @@ function buildReceiptHeader(receipt) {
873
928
  function buildSignatureHeader(signature) {
874
929
  return toBase64Json(signature);
875
930
  }
931
+ function buildExactSignatureHeader(input) {
932
+ return toBase64Json({ x402Version: 2, accepted: input.accepted, payload: input.payload });
933
+ }
876
934
  async function parseChallenge(response) {
877
935
  const headerValue = response.headers.get(HEADER_REQUIRED);
878
936
  if (headerValue) {
@@ -887,11 +945,24 @@ async function parseChallenge(response) {
887
945
  return null;
888
946
  }
889
947
  function parseReceipt(response) {
890
- const headerValue = response.headers.get(HEADER_RESPONSE);
948
+ const headerValue = response.headers.get(HEADER_RESPONSE) ?? response.headers.get(HEADER_RESPONSE_V1);
891
949
  if (!headerValue) return null;
892
950
  const parsed = fromBase64Json(headerValue);
893
951
  return isValidReceipt(parsed) ? parsed : null;
894
952
  }
953
+ function parseSettleResponse(response) {
954
+ const headerValue = response.headers.get(HEADER_RESPONSE) ?? response.headers.get(HEADER_RESPONSE_V1);
955
+ if (!headerValue) return null;
956
+ const parsed = fromBase64Json(headerValue);
957
+ if (!parsed || typeof parsed !== "object" || typeof parsed.success !== "boolean") return null;
958
+ return {
959
+ success: parsed.success,
960
+ ...typeof parsed.transaction === "string" ? { transaction: parsed.transaction } : {},
961
+ ...typeof parsed.network === "string" ? { network: parsed.network } : {},
962
+ ...typeof parsed.payer === "string" ? { payer: parsed.payer } : {},
963
+ ...typeof parsed.errorReason === "string" ? { errorReason: parsed.errorReason } : {}
964
+ };
965
+ }
895
966
  function parseSignatureHeader(value) {
896
967
  const parsed = fromBase64Json(value);
897
968
  if (!parsed || typeof parsed !== "object") return null;
@@ -1085,6 +1156,15 @@ function makeEvmNetwork(resolved) {
1085
1156
  },
1086
1157
  async estimateCost(accept) {
1087
1158
  const { decimals, symbol } = resolved.chain.nativeCurrency;
1159
+ if (accept.scheme === "exact") {
1160
+ return nativeCost({
1161
+ symbol,
1162
+ decimals,
1163
+ fee: 0n,
1164
+ basis: "estimated",
1165
+ detail: "gasless \u2014 the server/facilitator settles the signed authorization"
1166
+ });
1167
+ }
1088
1168
  const gasLimit = accept.asset === "native" ? 21000n : 65000n;
1089
1169
  try {
1090
1170
  const gasPrice = await publicClient.getGasPrice();
@@ -1146,6 +1226,21 @@ function makeEvmNetwork(resolved) {
1146
1226
  minConfirmations: accept.extra.minConfirmations
1147
1227
  });
1148
1228
  },
1229
+ // Standard x402 `exact` rail (EIP-3009), BUYER side — EVM only. Re-derives the
1230
+ // token's EIP-712 domain on-chain, signs an authorization with the agent's own
1231
+ // key, and returns it for the client to frame into PAYMENT-SIGNATURE. Never
1232
+ // broadcasts. Throws UnsupportedSchemeError for a non-EIP-3009 token / contract signer.
1233
+ async payExact(wallet, accept) {
1234
+ const a = wallet._native;
1235
+ const { payload, payerFrom, nonce } = await payExactEvm({
1236
+ publicClient,
1237
+ walletClient: a.walletClient,
1238
+ account: a.account,
1239
+ chainId: resolved.chainId,
1240
+ accept
1241
+ });
1242
+ return { payload, accepted: accept, payerFrom, nonce };
1243
+ },
1149
1244
  // Standard x402 `exact` rail (EIP-3009), seller side — EVM only.
1150
1245
  async exactDomain(asset) {
1151
1246
  return readExactDomain(publicClient, asset);
@@ -1176,7 +1271,7 @@ var loaders = {
1176
1271
  solana: async () => {
1177
1272
  let mod;
1178
1273
  try {
1179
- mod = await import("./solana-S3UFI3FE.js");
1274
+ mod = await import("./solana-WDKWWF33.js");
1180
1275
  } catch (cause) {
1181
1276
  throw new MissingDriverError(
1182
1277
  `Solana selected, but its packages aren't installed. Run: npm install @solana/web3.js @solana/spl-token bs58`,
@@ -1188,7 +1283,7 @@ var loaders = {
1188
1283
  ton: async () => {
1189
1284
  let mod;
1190
1285
  try {
1191
- mod = await import("./ton-WPTXGLVK.js");
1286
+ mod = await import("./ton-CHJ26BVA.js");
1192
1287
  } catch (cause) {
1193
1288
  throw new MissingDriverError(
1194
1289
  `TON selected, but its packages aren't installed. Run: npm install @ton/ton @ton/core @ton/crypto`,
@@ -1200,7 +1295,7 @@ var loaders = {
1200
1295
  stellar: async () => {
1201
1296
  let mod;
1202
1297
  try {
1203
- mod = await import("./stellar-Q5PO23SC.js");
1298
+ mod = await import("./stellar-FIJPQZVW.js");
1204
1299
  } catch (cause) {
1205
1300
  throw new MissingDriverError(
1206
1301
  `Stellar selected, but its package isn't installed. Run: npm install @stellar/stellar-sdk`,
@@ -1212,7 +1307,7 @@ var loaders = {
1212
1307
  xrpl: async () => {
1213
1308
  let mod;
1214
1309
  try {
1215
- mod = await import("./xrpl-HEAPEXAM.js");
1310
+ mod = await import("./xrpl-GTUPP6SK.js");
1216
1311
  } catch (cause) {
1217
1312
  throw new MissingDriverError(
1218
1313
  `XRPL selected, but its package isn't installed. Run: npm install xrpl`,
@@ -1224,7 +1319,7 @@ var loaders = {
1224
1319
  tron: async () => {
1225
1320
  let mod;
1226
1321
  try {
1227
- mod = await import("./tron-6GXBXTR4.js");
1322
+ mod = await import("./tron-DD3JDROV.js");
1228
1323
  } catch (cause) {
1229
1324
  throw new MissingDriverError(
1230
1325
  `Tron selected, but its package isn't installed. Run: npm install tronweb`,
@@ -1236,7 +1331,7 @@ var loaders = {
1236
1331
  sui: async () => {
1237
1332
  let mod;
1238
1333
  try {
1239
- mod = await import("./sui-WOXRKJXS.js");
1334
+ mod = await import("./sui-B7AVN7NK.js");
1240
1335
  } catch (cause) {
1241
1336
  throw new MissingDriverError(
1242
1337
  `Sui selected, but its package isn't installed. Run: npm install @mysten/sui`,
@@ -1248,7 +1343,7 @@ var loaders = {
1248
1343
  near: async () => {
1249
1344
  let mod;
1250
1345
  try {
1251
- mod = await import("./near-K6BDBABG.js");
1346
+ mod = await import("./near-LM7S3WUD.js");
1252
1347
  } catch (cause) {
1253
1348
  throw new MissingDriverError(
1254
1349
  `NEAR selected, but its package isn't installed. Run: npm install near-api-js`,
@@ -1260,7 +1355,7 @@ var loaders = {
1260
1355
  aptos: async () => {
1261
1356
  let mod;
1262
1357
  try {
1263
- mod = await import("./aptos-LPBLSEIQ.js");
1358
+ mod = await import("./aptos-SUXOVP7B.js");
1264
1359
  } catch (cause) {
1265
1360
  throw new MissingDriverError(
1266
1361
  `Aptos selected, but its package isn't installed. Run: npm install @aptos-labs/ts-sdk`,
@@ -1272,7 +1367,7 @@ var loaders = {
1272
1367
  algorand: async () => {
1273
1368
  let mod;
1274
1369
  try {
1275
- mod = await import("./algorand-WGVF4KTU.js");
1370
+ mod = await import("./algorand-F3OYB534.js");
1276
1371
  } catch (cause) {
1277
1372
  throw new MissingDriverError(
1278
1373
  `Algorand selected, but its package isn't installed. Run: npm install algosdk`,
@@ -1747,7 +1842,18 @@ function encodeBase642(str) {
1747
1842
 
1748
1843
  // src/policy.ts
1749
1844
  var ALLOW = { allowed: true };
1750
- var deny = (reason) => ({ allowed: false, reason });
1845
+ var deny = (code, reason) => ({
1846
+ allowed: false,
1847
+ reason,
1848
+ code
1849
+ });
1850
+ function resolveDeadline(policy, sessionStart) {
1851
+ const fromTtl = policy.ttlSeconds != null && Number.isSafeInteger(sessionStart + policy.ttlSeconds * 1e3) ? sessionStart + policy.ttlSeconds * 1e3 : null;
1852
+ const fromAbs = policy.expiresAt != null ? policy.expiresAt : null;
1853
+ if (fromTtl == null) return fromAbs;
1854
+ if (fromAbs == null) return fromTtl;
1855
+ return Math.min(fromTtl, fromAbs);
1856
+ }
1751
1857
  function hostMatches(host, pattern) {
1752
1858
  if (pattern.startsWith("*.")) {
1753
1859
  const suffix = pattern.slice(1);
@@ -1763,18 +1869,26 @@ function chainMatches(intent, allowed) {
1763
1869
  const id = "id" in allowed ? allowed.id : void 0;
1764
1870
  return id !== void 0 && intent.network === `eip155:${id}`;
1765
1871
  }
1766
- function evaluatePolicy(intent, policy, spentForAssetBase) {
1872
+ function evaluatePolicy(intent, policy, spentForAssetBase, ctx) {
1767
1873
  if (!policy) return ALLOW;
1874
+ if (ctx) {
1875
+ const deadline = resolveDeadline(policy, ctx.sessionStart);
1876
+ if (deadline != null && ctx.now >= deadline) {
1877
+ return deny("SESSION_EXPIRED", "session expired (TTL elapsed) \u2014 refusing to pay.");
1878
+ }
1879
+ }
1768
1880
  if (policy.chains && !policy.chains.some((c) => chainMatches(intent, c))) {
1769
1881
  return deny(
1882
+ "CHAIN",
1770
1883
  `chain ${intent.network} is not in the allowed set (policy.chains).`
1771
1884
  );
1772
1885
  }
1773
1886
  if (policy.hosts && !policy.hosts.some((h) => hostMatches(intent.host, h))) {
1774
- return deny(`host ${intent.host} is not in the allowed set (policy.hosts).`);
1887
+ return deny("HOST", `host ${intent.host} is not in the allowed set (policy.hosts).`);
1775
1888
  }
1776
1889
  if (!intent.recognized && !policy.allowUnknownTokens) {
1777
1890
  return deny(
1891
+ "UNKNOWN_TOKEN",
1778
1892
  `asset ${intent.asset} on ${intent.network} isn't a token the SDK can price; refusing to pay it on trust. Set policy.allowUnknownTokens to override.`
1779
1893
  );
1780
1894
  }
@@ -1787,6 +1901,7 @@ function evaluatePolicy(intent, policy, spentForAssetBase) {
1787
1901
  });
1788
1902
  if (!matches) {
1789
1903
  return deny(
1904
+ "TOKEN",
1790
1905
  `token ${intent.symbol ?? intent.asset} is not in the allowed set (policy.tokens).`
1791
1906
  );
1792
1907
  }
@@ -1795,6 +1910,7 @@ function evaluatePolicy(intent, policy, spentForAssetBase) {
1795
1910
  const cap = floorUnits(policy.maxAmount, intent.decimals);
1796
1911
  if (intent.amountBase > cap) {
1797
1912
  return deny(
1913
+ "MAX_AMOUNT",
1798
1914
  `payment of ${intent.amountBase} base units exceeds policy.maxAmount ` + `(${policy.maxAmount} ${intent.symbol ?? ""}).`.trimEnd()
1799
1915
  );
1800
1916
  }
@@ -1803,10 +1919,20 @@ function evaluatePolicy(intent, policy, spentForAssetBase) {
1803
1919
  const cap = floorUnits(policy.maxTotal, intent.decimals);
1804
1920
  if (spentForAssetBase + intent.amountBase > cap) {
1805
1921
  return deny(
1922
+ "MAX_TOTAL",
1806
1923
  `this payment would push spend on ${intent.symbol ?? intent.asset} past policy.maxTotal (${policy.maxTotal}); already spent ${spentForAssetBase} base units.`
1807
1924
  );
1808
1925
  }
1809
1926
  }
1927
+ if (ctx && policy.windowTotal !== void 0 && policy.windowSeconds !== void 0) {
1928
+ const cap = floorUnits(policy.windowTotal, intent.decimals);
1929
+ if (ctx.spentInWindowBase + intent.amountBase > cap) {
1930
+ return deny(
1931
+ "WINDOW_TOTAL",
1932
+ `this payment would exceed policy.windowTotal (${policy.windowTotal}) within the last ${policy.windowSeconds}s on ${intent.symbol ?? intent.asset}.`
1933
+ );
1934
+ }
1935
+ }
1810
1936
  return ALLOW;
1811
1937
  }
1812
1938
 
@@ -1815,6 +1941,12 @@ var keyFor = (network, asset) => `${network}|${asset}`;
1815
1941
  var SpendLedger = class {
1816
1942
  records = [];
1817
1943
  buckets = /* @__PURE__ */ new Map();
1944
+ /**
1945
+ * Session clock origin (epoch-ms) — process/session start = ledger
1946
+ * construction. In-memory; a new process is a new session. The client reads it
1947
+ * to compute the `ttlSeconds` deadline and the rolling-window slice.
1948
+ */
1949
+ sessionStart = Date.now();
1818
1950
  /** Record a settled payment. `decimals` is the TRUE token decimals (for the
1819
1951
  * per-asset running total used by maxTotal + the formatted summary). */
1820
1952
  record(r, decimals) {
@@ -1840,6 +1972,38 @@ var SpendLedger = class {
1840
1972
  totalFor(network, asset) {
1841
1973
  return this.buckets.get(keyFor(network, asset))?.total ?? 0n;
1842
1974
  }
1975
+ /**
1976
+ * Sum of base-unit amounts for (network, asset) whose record `at` (ISO
1977
+ * timestamp) is at or after `sinceMs` (epoch-ms). Backs the rolling window
1978
+ * (`sinceMs = now - windowSeconds*1000`). A linear scan of `records` —
1979
+ * agent-session cardinality is small (tens), and it only runs when a window
1980
+ * policy is set, so it's negligible against the network round-trip.
1981
+ */
1982
+ totalSince(network, asset, sinceMs) {
1983
+ let sum = 0n;
1984
+ for (const r of this.records) {
1985
+ if (r.network === network && r.asset === asset && Date.parse(r.at) >= sinceMs) {
1986
+ sum += BigInt(r.amountBase);
1987
+ }
1988
+ }
1989
+ return sum;
1990
+ }
1991
+ /**
1992
+ * The per-(network, asset) buckets, as read-only tuples — `network`, `asset`,
1993
+ * `symbol`, the TRUE `decimals` (frozen from the first record), and the running
1994
+ * `totalBase`. Lets the client compose a budget view WITHOUT coupling the ledger
1995
+ * to the policy (the cap math lives in the client). Decimals only exist for a
1996
+ * pair once it's been spent on — a never-spent pair simply isn't a bucket.
1997
+ */
1998
+ assetBuckets() {
1999
+ return [...this.buckets.values()].map((b) => ({
2000
+ network: b.network,
2001
+ asset: b.asset,
2002
+ ...b.symbol ? { symbol: b.symbol } : {},
2003
+ decimals: b.decimals,
2004
+ totalBase: b.total
2005
+ }));
2006
+ }
1843
2007
  /** An immutable snapshot of all spend so far. */
1844
2008
  summary() {
1845
2009
  return {
@@ -1859,6 +2023,7 @@ var SpendLedger = class {
1859
2023
  };
1860
2024
 
1861
2025
  // src/client.ts
2026
+ var DEFAULT_SCHEMES = ["onchain-proof"];
1862
2027
  var RECIPIENT_FIX = {
1863
2028
  NO_TRUSTLINE: "the recipient needs a one-time trustline for this asset before it can receive",
1864
2029
  NOT_REGISTERED: "the recipient must be storage_deposit-registered on this token (NEP-145, one-time)",
@@ -1881,6 +2046,37 @@ var PipRailClient = class {
1881
2046
  this.maxRetries = Math.max(1, opts.maxPaymentRetries ?? 3);
1882
2047
  this.retryTimeoutMs = opts.retryTimeoutMs ?? 3e4;
1883
2048
  this.onEvent = opts.onEvent ?? (() => void 0);
2049
+ this.assertPolicyTimeOptions(opts.policy);
2050
+ }
2051
+ /**
2052
+ * Fail LOUDLY at construction on a misconfigured time policy — a security
2053
+ * boundary must never silently half-arm. Two invariants (a misconfiguration is
2054
+ * a programmer error → `TypeError`, no new SDK error code):
2055
+ * - the rolling window needs BOTH `windowTotal` and `windowSeconds`, or NEITHER
2056
+ * (one alone is a leash that silently doesn't bite);
2057
+ * - `ttlSeconds` must be a positive, safe integer whose `*1000` deadline stays
2058
+ * within `Number.MAX_SAFE_INTEGER` (else the arithmetic would lose precision).
2059
+ */
2060
+ assertPolicyTimeOptions(policy) {
2061
+ if (!policy) return;
2062
+ const hasWindowTotal = policy.windowTotal !== void 0;
2063
+ const hasWindowSeconds = policy.windowSeconds !== void 0;
2064
+ if (hasWindowTotal !== hasWindowSeconds) {
2065
+ throw new TypeError(
2066
+ "policy.windowTotal and policy.windowSeconds must be set together \u2014 a rolling-window cap can't be half-armed (set both, or neither)."
2067
+ );
2068
+ }
2069
+ if (hasWindowSeconds && !(Number.isSafeInteger(policy.windowSeconds) && policy.windowSeconds > 0)) {
2070
+ throw new TypeError("policy.windowSeconds must be a positive integer number of seconds.");
2071
+ }
2072
+ if (policy.ttlSeconds !== void 0) {
2073
+ const ttl = policy.ttlSeconds;
2074
+ if (!Number.isSafeInteger(ttl) || ttl <= 0 || !Number.isSafeInteger(this.ledger.sessionStart + ttl * 1e3)) {
2075
+ throw new TypeError(
2076
+ "policy.ttlSeconds must be a positive integer number of seconds small enough that the resulting deadline stays a safe integer."
2077
+ );
2078
+ }
2079
+ }
1884
2080
  }
1885
2081
  /** Emit an observability event, never letting a throwing handler break the
1886
2082
  * payment flow (mirrors the server gate's `onPaid` isolation). */
@@ -1901,6 +2097,11 @@ var PipRailClient = class {
1901
2097
  return { net, wallet };
1902
2098
  })();
1903
2099
  }
2100
+ /** Resolve the effective scheme set: a per-call override, else the constructor's
2101
+ * `schemes`, else the `onchain-proof`-only default. */
2102
+ resolveSchemes(perCall) {
2103
+ return perCall ?? this.opts.schemes ?? DEFAULT_SCHEMES;
2104
+ }
1904
2105
  /** GET that auto-handles 402. Pass a full URL to any x402-gated endpoint. */
1905
2106
  get(url, init) {
1906
2107
  return this.fetch(url, { ...init ?? {}, method: "GET" });
@@ -1945,7 +2146,7 @@ var PipRailClient = class {
1945
2146
  async quote(url, init) {
1946
2147
  const res = await fetch(url, { ...init ?? {}, method: init?.method ?? "GET" });
1947
2148
  if (res.status !== 402) return null;
1948
- const { quote } = await this.resolveChallenge(url, res);
2149
+ const { quote } = await this.resolveChallenge(url, res, this.resolveSchemes());
1949
2150
  return quote;
1950
2151
  }
1951
2152
  /**
@@ -1964,7 +2165,7 @@ var PipRailClient = class {
1964
2165
  async estimateCost(url, init) {
1965
2166
  const res = await fetch(url, { ...init ?? {}, method: init?.method ?? "GET" });
1966
2167
  if (res.status !== 402) return null;
1967
- const { net, accept, quote } = await this.resolveChallenge(url, res);
2168
+ const { net, accept, quote } = await this.resolveChallenge(url, res, this.resolveSchemes());
1968
2169
  const cost = await net.estimateCost(accept);
1969
2170
  return { quote, cost };
1970
2171
  }
@@ -1973,6 +2174,64 @@ var PipRailClient = class {
1973
2174
  spent() {
1974
2175
  return this.ledger.summary();
1975
2176
  }
2177
+ /**
2178
+ * Read-only budget + time leash for a Mode-A (headless) agent — the policy IS
2179
+ * the consent, and this is how the agent SEES what's left of it before paying.
2180
+ * Composes the in-memory ledger with the configured policy; never throws, moves
2181
+ * no funds. PROCESS-SCOPED — every figure resets on restart (see {@link SessionBudget}).
2182
+ */
2183
+ budget() {
2184
+ const view = this.sessionView();
2185
+ const start = new Date(this.ledger.sessionStart).toISOString();
2186
+ return {
2187
+ session: {
2188
+ start,
2189
+ expiresAt: view?.expiresAt != null ? new Date(view.expiresAt).toISOString() : null,
2190
+ secondsRemaining: view?.secondsRemaining ?? null
2191
+ },
2192
+ byAsset: this.remaining()
2193
+ };
2194
+ }
2195
+ /**
2196
+ * Per-(network, asset) remaining budget — ONE row per pair the ledger already
2197
+ * holds (decimals are known only after the first spend), so a fresh client with
2198
+ * a `maxTotal` set returns `[]` until its first payment. `cap`/`remaining` are
2199
+ * `undefined` when no `maxTotal` is configured (unbounded). Pure + in-memory;
2200
+ * never throws, never sums across tokens (no price oracle). PROCESS-SCOPED.
2201
+ */
2202
+ remaining() {
2203
+ const maxTotal = this.opts.policy?.maxTotal;
2204
+ return this.ledger.assetBuckets().map((b) => {
2205
+ const base2 = {
2206
+ network: b.network,
2207
+ asset: b.asset,
2208
+ ...b.symbol ? { symbol: b.symbol } : {},
2209
+ decimals: b.decimals,
2210
+ spentBase: b.totalBase.toString()
2211
+ };
2212
+ if (maxTotal === void 0) return base2;
2213
+ const capBase = floorUnits(maxTotal, b.decimals);
2214
+ const remainingBase = capBase > b.totalBase ? capBase - b.totalBase : 0n;
2215
+ return {
2216
+ ...base2,
2217
+ capBase: capBase.toString(),
2218
+ remainingBase: remainingBase.toString(),
2219
+ remainingFormatted: formatUnits(remainingBase, b.decimals)
2220
+ };
2221
+ });
2222
+ }
2223
+ /** The read-only TIME envelope for the plan/budget surfaces, or `undefined`
2224
+ * when no session deadline (`ttlSeconds`/`expiresAt`) is set. `secondsRemaining`
2225
+ * is clamped ≥ 0 — a best-effort host wall-clock estimate. */
2226
+ sessionView(now = Date.now()) {
2227
+ const policy = this.opts.policy;
2228
+ if (!policy || policy.ttlSeconds == null && policy.expiresAt == null) return void 0;
2229
+ const deadline = resolveDeadline(policy, this.ledger.sessionStart);
2230
+ return {
2231
+ expiresAt: deadline,
2232
+ secondsRemaining: deadline == null ? null : Math.max(0, Math.floor((deadline - now) / 1e3))
2233
+ };
2234
+ }
1976
2235
  /**
1977
2236
  * Plan a payment for a gated URL — WITHOUT paying. The read-only completion of
1978
2237
  * the `quote()` → `estimateCost()` → **`planPayment()`** trio: it surveys every
@@ -2000,7 +2259,7 @@ var PipRailClient = class {
2000
2259
  throw new InvalidEnvelopeError("402 response did not include a parseable x402 challenge.");
2001
2260
  }
2002
2261
  const { net, wallet } = await this.ensure();
2003
- return this.planFromChallenge(net, wallet, challenge, url);
2262
+ return this.planFromChallenge(net, wallet, challenge, url, this.resolveSchemes());
2004
2263
  }
2005
2264
  /**
2006
2265
  * Convenience over {@link planPayment}: can the wallet settle this URL right now?
@@ -2028,8 +2287,8 @@ var PipRailClient = class {
2028
2287
  * - A resource just listed via {@link register} may not appear yet — 402 Index reviews
2029
2288
  * before publishing, so retry with a brief backoff if a fresh listing is missing.
2030
2289
  * - Results are cross-scheme (mostly the mainstream `exact` scheme); `fetch()` pays
2031
- * only `onchain-proof` rails directly (pay `exact` resources with the experimental
2032
- * `drivers/evm/exact.ts`).
2290
+ * `onchain-proof` rails by default, and standard `exact` rails too once you opt in
2291
+ * with `schemes: ['onchain-proof', 'exact']` (EVM + EIP-3009 — USDC/EURC).
2033
2292
  */
2034
2293
  async discover(opts = {}) {
2035
2294
  const found = await searchOpenIndexes({
@@ -2168,13 +2427,14 @@ var PipRailClient = class {
2168
2427
  }
2169
2428
  const firstResponse = await fetch(url, init);
2170
2429
  if (firstResponse.status !== 402) return firstResponse;
2171
- const resolved = await this.resolveChallenge(url, firstResponse);
2430
+ const schemes = this.resolveSchemes(init?.schemes);
2431
+ const resolved = await this.resolveChallenge(url, firstResponse, schemes);
2172
2432
  const { net, wallet, challenge } = resolved;
2173
2433
  let accept = resolved.accept;
2174
2434
  let quote = resolved.quote;
2175
2435
  const autoRoute = init?.autoRoute ?? this.opts.autoRoute ?? false;
2176
2436
  if (autoRoute) {
2177
- const plan = await this.planFromChallenge(net, wallet, challenge, url);
2437
+ const plan = await this.planFromChallenge(net, wallet, challenge, url, schemes);
2178
2438
  if (!plan.best) {
2179
2439
  throw new PaymentDeclinedError(plan.fundingHint ?? "No rail is settleable for this payment.");
2180
2440
  }
@@ -2183,6 +2443,9 @@ var PipRailClient = class {
2183
2443
  }
2184
2444
  this.safeEmit({ kind: "payment-required", challenge, accept });
2185
2445
  await this.authorize(quote);
2446
+ if (accept.scheme === "exact") {
2447
+ return this.payExactRail(net, wallet, accept, url, init, quote);
2448
+ }
2186
2449
  const { ref, confirmed } = await this.payAndConfirm(net, wallet, accept);
2187
2450
  const response = await this.retryWithProof(url, init, accept, ref, confirmed);
2188
2451
  this.recordSpend(quote, ref);
@@ -2194,7 +2457,7 @@ var PipRailClient = class {
2194
2457
  * network, pick the accept the client can pay, and build its quote. Shared by
2195
2458
  * `quote()` (read-only) and `fetch()` (which then authorises + pays).
2196
2459
  */
2197
- async resolveChallenge(url, response) {
2460
+ async resolveChallenge(url, response, schemes) {
2198
2461
  const challenge = await parseChallenge(response);
2199
2462
  if (!challenge) {
2200
2463
  throw new InvalidEnvelopeError(
@@ -2202,11 +2465,29 @@ var PipRailClient = class {
2202
2465
  );
2203
2466
  }
2204
2467
  const { net, wallet } = await this.ensure();
2205
- const candidates = this.gatherCandidates(net, challenge);
2468
+ const candidates = this.gatherCandidates(net, challenge, schemes);
2206
2469
  if (candidates.length === 0) {
2207
- const networks = challenge.accepts.map((a) => a.network).join(", ");
2470
+ const exactOnNet = challenge.accepts.some(
2471
+ (a) => a.scheme === "exact" && net.supports(a.network)
2472
+ );
2473
+ if (schemes.includes("exact") && exactOnNet && typeof net.payExact !== "function") {
2474
+ throw new UnsupportedSchemeError(
2475
+ `This 402 offers a standard 'exact' rail on ${net.network}, but the ${net.family} family can't pay 'exact' (EVM + EIP-3009 only), and no 'onchain-proof' rail was offered.`
2476
+ );
2477
+ }
2478
+ if (!schemes.includes("exact") && exactOnNet && typeof net.payExact === "function") {
2479
+ const payable = challenge.accepts.some(
2480
+ (a) => a.scheme === "exact" && net.supports(a.network) && net.describeAsset(a.asset) != null
2481
+ );
2482
+ if (payable) {
2483
+ throw new NoCompatibleAcceptError(
2484
+ `This 402 is payable only via the standard 'exact' rail on ${net.network}, which is OFF by default. Enable it: new PipRailClient({ \u2026, schemes: ['onchain-proof', 'exact'] }) or per call fetch(url, { schemes: ['exact'] }) (MCP: PIPRAIL_SCHEMES=onchain-proof,exact).`
2485
+ );
2486
+ }
2487
+ }
2488
+ const networks = [...new Set(challenge.accepts.map((a) => a.network))].join(", ");
2208
2489
  throw new NoCompatibleAcceptError(
2209
- `No accepts[] entry for ${net.network} (challenge offered: ${networks || "none"}).`
2490
+ `No accepts[] entry payable by this client on ${net.network} (schemes: ${schemes.join(", ")}; challenge offered: ${networks || "none"}).`
2210
2491
  );
2211
2492
  }
2212
2493
  const priced = candidates.map((accept) => ({
@@ -2216,20 +2497,38 @@ var PipRailClient = class {
2216
2497
  const chosen = priced.find((p) => p.quote.withinPolicy) ?? priced[0];
2217
2498
  return { net, wallet, accept: chosen.accept, challenge, quote: chosen.quote };
2218
2499
  }
2219
- /** The candidate accepts this client could pay: our scheme, on the bound network.
2220
- * A dual-advertised challenge may also carry standard `exact` rails the PipRail
2221
- * client ignores those (it pays the backendless `onchain-proof` rail); the type
2222
- * predicate narrows the `X402AnyAccept` union to the rails we settle. */
2223
- gatherCandidates(net, challenge) {
2224
- return challenge.accepts.filter(
2225
- (a) => a.scheme === "onchain-proof" && net.supports(a.network)
2226
- );
2500
+ /** The candidate accepts this client could pay, on the bound network. Always the
2501
+ * backendless `onchain-proof` rails; PLUS standard `exact` rails when `schemes`
2502
+ * enables them AND the driver can settle them (EVM `payExact` + a recognised
2503
+ * EIP-3009 token). `onchain-proof` is gathered FIRST so default selection is
2504
+ * unchanged when `exact` is off. */
2505
+ gatherCandidates(net, challenge, schemes) {
2506
+ const out = [];
2507
+ if (schemes.includes("onchain-proof")) {
2508
+ out.push(
2509
+ ...challenge.accepts.filter(
2510
+ (a) => a.scheme === "onchain-proof" && net.supports(a.network)
2511
+ )
2512
+ );
2513
+ }
2514
+ if (schemes.includes("exact")) {
2515
+ out.push(
2516
+ ...challenge.accepts.filter(
2517
+ (a) => a.scheme === "exact" && net.supports(a.network) && typeof net.payExact === "function" && net.describeAsset(a.asset) != null && // a foreign rail's maxTimeoutSeconds must be a usable positive integer, or
2518
+ // signing it would build a NaN/garbage validBefore — drop it silently
2519
+ // (symmetric with an unrecognised token) rather than leak a raw SyntaxError.
2520
+ Number.isInteger(a.maxTimeoutSeconds) && a.maxTimeoutSeconds > 0
2521
+ )
2522
+ );
2523
+ }
2524
+ return out;
2227
2525
  }
2228
2526
  /** Build the full {@link PaymentPlan} from an already-parsed challenge + bound
2229
2527
  * net/wallet. Shared by `planPayment` (read-only) and `fetch`'s autoRoute. */
2230
- async planFromChallenge(net, wallet, challenge, url) {
2528
+ async planFromChallenge(net, wallet, challenge, url, schemes) {
2231
2529
  const chainLabel = typeof this.opts.chain === "string" ? this.opts.chain : net.network;
2232
- const candidates = this.gatherCandidates(net, challenge);
2530
+ const session = this.sessionView();
2531
+ const candidates = this.gatherCandidates(net, challenge, schemes);
2233
2532
  if (candidates.length === 0) {
2234
2533
  const offered = [...new Set(challenge.accepts.map((a) => a.network))].join(", ") || "none";
2235
2534
  return {
@@ -2239,7 +2538,8 @@ var PipRailClient = class {
2239
2538
  payable: false,
2240
2539
  best: null,
2241
2540
  options: [],
2242
- fundingHint: `This 402 isn't offered on your chain (${chainLabel}); it's payable on: ${offered}.`
2541
+ fundingHint: `This 402 isn't offered on your chain (${chainLabel}); it's payable on: ${offered}.`,
2542
+ ...session ? { session } : {}
2243
2543
  };
2244
2544
  }
2245
2545
  const analysed = await Promise.all(
@@ -2257,7 +2557,8 @@ var PipRailClient = class {
2257
2557
  payable: best !== null,
2258
2558
  best,
2259
2559
  options,
2260
- fundingHint: best ? null : buildFundingHint(options, chainLabel)
2560
+ fundingHint: best ? null : buildFundingHint(options, chainLabel),
2561
+ ...session ? { session } : {}
2261
2562
  };
2262
2563
  }
2263
2564
  /** Analyse ONE rail against the wallet's holdings — quote (existing) + gas
@@ -2269,17 +2570,27 @@ var PipRailClient = class {
2269
2570
  const rr = await net.recipientReady(accept.payTo, accept.asset).catch(() => ({ ready: "unknown" }));
2270
2571
  const amount = BigInt(accept.amount);
2271
2572
  const fee = safeBig(cost.fee);
2573
+ const isExact = accept.scheme === "exact";
2272
2574
  const isNative = accept.asset === "native";
2273
2575
  const blockers = [];
2274
2576
  const warnings = [];
2275
2577
  const shortfall = {};
2276
- if (!quote.withinPolicy) blockers.push("OUTSIDE_POLICY");
2578
+ if (!quote.withinPolicy) {
2579
+ blockers.push(
2580
+ quote.policyCode === "SESSION_EXPIRED" || quote.policyCode === "WINDOW_TOTAL" ? "OUTSIDE_WINDOW" : "OUTSIDE_POLICY"
2581
+ );
2582
+ }
2277
2583
  if (quote.symbolMismatch) warnings.push("SYMBOL_MISMATCH");
2278
- if (cost.basis === "heuristic") warnings.push("GAS_HEURISTIC");
2584
+ if (!isExact && cost.basis === "heuristic") warnings.push("GAS_HEURISTIC");
2279
2585
  const tokenKnown = bal.token != null;
2280
2586
  const nativeKnown = bal.native != null;
2281
- if (!tokenKnown || !nativeKnown) warnings.push("BALANCE_UNREADABLE");
2282
- if (isNative) {
2587
+ if (isExact ? !tokenKnown : !tokenKnown || !nativeKnown) warnings.push("BALANCE_UNREADABLE");
2588
+ if (isExact) {
2589
+ if (tokenKnown && bal.token < amount) {
2590
+ blockers.push("INSUFFICIENT_TOKEN");
2591
+ shortfall.token = formatUnits(amount - bal.token, quote.decimals);
2592
+ }
2593
+ } else if (isNative) {
2283
2594
  if (nativeKnown && bal.native < amount + fee) {
2284
2595
  blockers.push("INSUFFICIENT_TOKEN");
2285
2596
  shortfall.token = formatUnits(amount + fee - bal.native, quote.decimals);
@@ -2306,7 +2617,7 @@ var PipRailClient = class {
2306
2617
  } else {
2307
2618
  recipient = { ready: rr.ready };
2308
2619
  }
2309
- const unreadable = isNative ? !nativeKnown : !tokenKnown || !nativeKnown;
2620
+ const unreadable = isExact ? !tokenKnown : isNative ? !nativeKnown : !tokenKnown || !nativeKnown;
2310
2621
  const state = blockers.length ? "blocked" : unreadable || rr.ready === "unknown" ? "unknown" : "payable";
2311
2622
  return {
2312
2623
  accept,
@@ -2335,6 +2646,11 @@ var PipRailClient = class {
2335
2646
  const amountBase = BigInt(accept.amount);
2336
2647
  const described = net.describeAsset(accept.asset);
2337
2648
  const decimals = described?.decimals ?? accept.extra.decimals;
2649
+ if (decimals === void 0) {
2650
+ throw new InvalidEnvelopeError(
2651
+ `challenge for ${accept.asset} on ${accept.network} states no decimals and the SDK doesn't recognise the token \u2014 refusing to price it.`
2652
+ );
2653
+ }
2338
2654
  const symbol = described?.symbol ?? accept.extra.symbol;
2339
2655
  const amountFormatted = formatUnits(amountBase, decimals);
2340
2656
  const intent = {
@@ -2347,10 +2663,25 @@ var PipRailClient = class {
2347
2663
  symbol,
2348
2664
  recognized: described != null
2349
2665
  };
2666
+ const policy = this.opts.policy;
2667
+ const hasWindow = !!policy && policy.windowTotal != null && policy.windowSeconds != null;
2668
+ const hasTimePolicy = !!policy && (policy.ttlSeconds != null || policy.expiresAt != null || hasWindow);
2669
+ const now = Date.now();
2670
+ const ctx = hasTimePolicy ? {
2671
+ now,
2672
+ sessionStart: this.ledger.sessionStart,
2673
+ // Window slice ONLY when BOTH fields are set — never a `?? 0` width.
2674
+ spentInWindowBase: hasWindow ? this.ledger.totalSince(
2675
+ accept.network,
2676
+ accept.asset,
2677
+ now - policy.windowSeconds * 1e3
2678
+ ) : 0n
2679
+ } : void 0;
2350
2680
  const decision = evaluatePolicy(
2351
2681
  intent,
2352
2682
  this.opts.policy,
2353
- this.ledger.totalFor(accept.network, accept.asset)
2683
+ this.ledger.totalFor(accept.network, accept.asset),
2684
+ ctx
2354
2685
  );
2355
2686
  const serverSymbol = accept.extra.symbol;
2356
2687
  const symbolMismatch = intent.recognized && !!serverSymbol && !!symbol && serverSymbol.toUpperCase() !== symbol.toUpperCase();
@@ -2369,15 +2700,19 @@ var PipRailClient = class {
2369
2700
  recognized: intent.recognized,
2370
2701
  symbolMismatch,
2371
2702
  withinPolicy: decision.allowed,
2372
- ...decision.reason ? { policyReason: decision.reason } : {}
2703
+ ...decision.reason ? { policyReason: decision.reason } : {},
2704
+ ...decision.code ? { policyCode: decision.code } : {}
2373
2705
  };
2374
2706
  }
2375
2707
  /** Enforce the spend policy and the onBeforePay hook — both refuse by
2376
- * throwing PaymentDeclinedError, before any funds move. */
2708
+ * throwing PaymentDeclinedError, before any funds move. Every refusal carries
2709
+ * a typed `reasonCode` so an agent can branch on the cause (and spot a
2710
+ * TERMINAL expiry/approval decline it must not retry) without parsing prose. */
2377
2711
  async authorize(quote) {
2378
2712
  if (!quote.withinPolicy) {
2379
2713
  throw new PaymentDeclinedError(
2380
- `Payment refused by policy: ${quote.policyReason ?? "not allowed"}`
2714
+ `Payment refused by policy: ${quote.policyReason ?? "not allowed"}`,
2715
+ { reasonCode: reasonCodeForPolicy(quote.policyCode) }
2381
2716
  );
2382
2717
  }
2383
2718
  const hook = this.opts.onBeforePay;
@@ -2387,12 +2722,14 @@ var PipRailClient = class {
2387
2722
  approved = await hook(quote);
2388
2723
  } catch (err) {
2389
2724
  throw new PaymentDeclinedError("onBeforePay threw \u2014 refusing to pay.", {
2390
- cause: err
2725
+ cause: err,
2726
+ reasonCode: "APPROVAL"
2391
2727
  });
2392
2728
  }
2393
2729
  if (!approved) {
2394
2730
  throw new PaymentDeclinedError(
2395
- `onBeforePay declined ${quote.amountFormatted} ${quote.symbol ?? ""}`.trimEnd() + ` on ${quote.network}.`
2731
+ `onBeforePay declined ${quote.amountFormatted} ${quote.symbol ?? ""}`.trimEnd() + ` on ${quote.network}.`,
2732
+ { reasonCode: "APPROVAL" }
2396
2733
  );
2397
2734
  }
2398
2735
  }
@@ -2495,7 +2832,102 @@ var PipRailClient = class {
2495
2832
  { ref }
2496
2833
  );
2497
2834
  }
2835
+ /**
2836
+ * Pay a standard x402 `exact` rail — a SEPARATE, fundamentally more conservative
2837
+ * path than {@link retryWithProof}. The buyer SIGNS an EIP-3009 authorization ONCE
2838
+ * (the driver's `payExact`) and the server / merchant-chosen facilitator BROADCASTS
2839
+ * it synchronously, so a blind re-POST of a still-in-flight authorization could
2840
+ * double-BROADCAST it. Hence, unlike the onchain-proof loop:
2841
+ *
2842
+ * • sign exactly once — reuse the SAME header on every retry, never re-sign;
2843
+ * • retry ONLY an explicit 402 (a definitive pre-broadcast rejection), bounded
2844
+ * well under `maxTimeoutSeconds` so the loop can't outlive the authorization;
2845
+ * • a post-POST transport error/timeout → {@link PaymentTimeoutError} carrying the
2846
+ * nonce (the facilitator MAY have settled — verify on-chain, NEVER re-pay);
2847
+ * • a 5xx → return as-is (server settle failure; the authorization stays valid +
2848
+ * its nonce unused) — no settled event, no spend;
2849
+ * • a 200 whose SettleResponse says `success:false` → a rejection, NEVER a spend;
2850
+ * • the spend is recorded EXACTLY ONCE, on an affirmative settlement only.
2851
+ */
2852
+ async payExactRail(net, wallet, accept, url, init, quote) {
2853
+ if (!net.payExact) {
2854
+ throw new UnsupportedSchemeError(
2855
+ `the ${net.family} family can't pay a standard 'exact' rail (EVM + EIP-3009 only).`
2856
+ );
2857
+ }
2858
+ throwIfAborted(init?.signal);
2859
+ const { payload, accepted, payerFrom, nonce } = await net.payExact(wallet, accept);
2860
+ const headers = new Headers(init?.headers);
2861
+ headers.set(HEADER_SIGNATURE, buildExactSignatureHeader({ accepted, payload }));
2862
+ const rejectDefinitive = (why2) => {
2863
+ this.safeEmit({ kind: "payment-failed", reason: `exact: facilitator rejected nonce=${nonce} (${why2})` });
2864
+ throw new MaxRetriesExceededError(
2865
+ `exact: the facilitator rejected the payment (${why2}). Fix the cause, then re-present the SAME signed authorization (nonce=${nonce}) \u2014 do NOT re-sign a fresh nonce. ref=${nonce}.`,
2866
+ { ref: nonce }
2867
+ );
2868
+ };
2869
+ const deadline = Date.now() + Math.max(1, Math.floor(accept.maxTimeoutSeconds / 2)) * 1e3;
2870
+ const maxAttempts = Math.min(this.maxRetries, 3);
2871
+ let lastReason = null;
2872
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
2873
+ if (attempt > 0) {
2874
+ if (Date.now() >= deadline) break;
2875
+ await new Promise((r) => setTimeout(r, Math.min(2e3, 400 * 2 ** (attempt - 1))));
2876
+ }
2877
+ throwIfAborted(init?.signal);
2878
+ const budget = Math.min(this.retryTimeoutMs, deadline - Date.now());
2879
+ if (budget <= 0) break;
2880
+ const timeoutController = new AbortController();
2881
+ const timeoutId = setTimeout(() => timeoutController.abort(), budget);
2882
+ const signal = init?.signal && typeof AbortSignal.any === "function" ? AbortSignal.any([timeoutController.signal, init.signal]) : timeoutController.signal;
2883
+ let response;
2884
+ try {
2885
+ response = await fetch(url, { ...init ?? {}, headers, signal });
2886
+ } catch (err) {
2887
+ throw new PaymentTimeoutError(
2888
+ `exact: no response after submitting the authorization (nonce=${nonce}) to ${hostOf2(url)}. The facilitator may have already settled it \u2014 verify on-chain with authorizationState(${payerFrom}, ${nonce}) before re-presenting; do NOT re-pay.`,
2889
+ { cause: err, ref: nonce }
2890
+ );
2891
+ } finally {
2892
+ clearTimeout(timeoutId);
2893
+ }
2894
+ const settle = parseSettleResponse(response);
2895
+ if (response.status === 402) {
2896
+ if (settle && settle.success === false) rejectDefinitive(settle.errorReason ?? "the facilitator reported success:false");
2897
+ lastReason = await readInvalidReason(response) ?? lastReason;
2898
+ continue;
2899
+ }
2900
+ if (response.ok && !(settle && settle.success === false)) {
2901
+ const receipt = parseReceipt(response);
2902
+ this.safeEmit({ kind: "payment-settled", receipt, ...settle ? { settle } : {} });
2903
+ const ref = settle?.transaction || receipt?.transaction || `eip3009-nonce:${nonce}`;
2904
+ this.recordSpend(quote, ref);
2905
+ return response;
2906
+ }
2907
+ if (response.status >= 500) {
2908
+ this.safeEmit({ kind: "payment-failed", reason: `exact: server ${response.status} \u2014 authorization nonce=${nonce} not settled` });
2909
+ return response;
2910
+ }
2911
+ if (settle && settle.success === false) rejectDefinitive(settle.errorReason ?? "the facilitator reported success:false");
2912
+ this.safeEmit({ kind: "payment-failed", reason: `exact: server ${response.status} \u2014 authorization nonce=${nonce} not settled` });
2913
+ return response;
2914
+ }
2915
+ const why = lastReason ? `${lastReason.error}${lastReason.detail ? ` \u2014 ${lastReason.detail}` : ""}` : "server gave no reason";
2916
+ this.safeEmit({
2917
+ kind: "payment-failed",
2918
+ reason: `exact: 402 after submitting authorization nonce=${nonce} (${why})`
2919
+ });
2920
+ throw new MaxRetriesExceededError(
2921
+ `exact: server still returned 402 after submitting the signed authorization (nonce=${nonce}). Last rejection: ${why}. Re-present the SAME authorization \u2014 do NOT re-sign a fresh nonce; verify authorizationState(${payerFrom}, ${nonce}) first. ref=${nonce}.`,
2922
+ { ref: nonce }
2923
+ );
2924
+ }
2498
2925
  };
2926
+ function throwIfAborted(signal) {
2927
+ if (signal?.aborted) {
2928
+ throw signal.reason ?? new DOMException("This operation was aborted.", "AbortError");
2929
+ }
2930
+ }
2499
2931
  function safeBig(s) {
2500
2932
  try {
2501
2933
  return BigInt(s);
@@ -2525,6 +2957,9 @@ function buildFundingHint(options, chainLabel) {
2525
2957
  if (target.blockers.includes("RECIPIENT_NOT_READY")) {
2526
2958
  return `Recipient ${shortAddr(target.accept.payTo)} can't receive on ${chainLabel} yet \u2014 ${target.recipient.fix ?? "recipient not ready"}.`;
2527
2959
  }
2960
+ if (target.blockers.includes("OUTSIDE_WINDOW")) {
2961
+ return target.quote.policyCode === "SESSION_EXPIRED" ? `Session is over on ${chainLabel} \u2014 restart the process or extend the TTL; no retry will succeed.` : `Budget window exhausted on ${chainLabel} \u2014 wait for it to free, or raise policy.windowTotal.`;
2962
+ }
2528
2963
  if (target.blockers.includes("OUTSIDE_POLICY")) {
2529
2964
  return `Refused by spend policy: ${target.quote.policyReason ?? "not allowed"}.`;
2530
2965
  }
@@ -2562,6 +2997,20 @@ function railOnNetwork(rail, matches) {
2562
2997
  const n = normalizeNetwork(rail.network);
2563
2998
  return !n.includes(":") || matches(n);
2564
2999
  }
3000
+ function reasonCodeForPolicy(code) {
3001
+ switch (code) {
3002
+ case "SESSION_EXPIRED":
3003
+ return "SESSION_EXPIRED";
3004
+ case "WINDOW_TOTAL":
3005
+ return "OUTSIDE_WINDOW";
3006
+ case "MAX_TOTAL":
3007
+ return "BUDGET";
3008
+ case void 0:
3009
+ return void 0;
3010
+ default:
3011
+ return "POLICY";
3012
+ }
3013
+ }
2565
3014
  function hostOf2(url) {
2566
3015
  try {
2567
3016
  return new URL(url).hostname;
@@ -2596,12 +3045,124 @@ async function readInvalidReason(response) {
2596
3045
  detail: typeof body.detail === "string" ? body.detail : ""
2597
3046
  };
2598
3047
  }
3048
+ if (body && body.isValid === false && typeof body.invalidReason === "string") {
3049
+ return {
3050
+ error: body.invalidReason,
3051
+ detail: typeof body.invalidMessage === "string" ? body.invalidMessage : ""
3052
+ };
3053
+ }
2599
3054
  } catch {
2600
3055
  }
3056
+ const settle = parseSettleResponse(response);
3057
+ if (settle?.errorReason) return { error: settle.errorReason, detail: "" };
2601
3058
  return null;
2602
3059
  }
2603
3060
 
3061
+ // src/render.ts
3062
+ function summarizePlan(plan) {
3063
+ if (plan == null) return "No payment required \u2014 the URL is not payment-gated.";
3064
+ if (plan.payable && plan.best) {
3065
+ const q = plan.best.quote;
3066
+ const c = plan.best.cost;
3067
+ const otherRails = plan.options.length - 1;
3068
+ const note = otherRails > 0 ? ` ${otherRails} other rail(s) not settleable.` : "";
3069
+ return `Payable: ${q.amountFormatted} ${q.symbol ?? q.asset} on ${plan.best.accept.network} (gas ~${c.feeFormatted} ${c.feeSymbol}).${note}`;
3070
+ }
3071
+ return `NOT payable: ${plan.fundingHint ?? `no settleable rail on ${plan.network}`}`;
3072
+ }
3073
+ function explainDecline(err) {
3074
+ if (!(err instanceof PipRailError)) {
3075
+ return `Payment failed: ${err instanceof Error ? err.message : String(err)}`;
3076
+ }
3077
+ switch (err.code) {
3078
+ case "PAYMENT_DECLINED":
3079
+ return err.message;
3080
+ case "PAYMENT_TIMEOUT":
3081
+ case "MAX_RETRIES_EXCEEDED":
3082
+ case "CONFIRMATION_TIMEOUT":
3083
+ return `${err.message} Recover using the proof on .ref (re-verify or re-submit it); never re-pay \u2014 a fresh payment would double-spend.`;
3084
+ case "INSUFFICIENT_FUNDS":
3085
+ return "The wallet cannot cover the payment + gas \u2014 top up the payer (token and/or native gas) and retry.";
3086
+ case "RECIPIENT_NOT_READY":
3087
+ return `${err.message} The fix is on the RECIPIENT (trustline / registration / opt-in / activation), not your balance.`;
3088
+ case "NO_COMPATIBLE_ACCEPT":
3089
+ case "UNSUPPORTED_SCHEME":
3090
+ return err.message;
3091
+ default:
3092
+ return `Payment failed: ${err.message}`;
3093
+ }
3094
+ }
3095
+ function formatSpendReport(summary) {
3096
+ if (summary.count === 0) return "No payments yet.";
3097
+ return summary.byAsset.map(
3098
+ (a) => `${a.totalFormatted} ${a.symbol ?? a.asset} on ${a.network} (${a.count} payment${a.count === 1 ? "" : "s"})`
3099
+ ).join("; ");
3100
+ }
3101
+
3102
+ // src/agentGuide.ts
3103
+ var PIPRAIL_AGENT_GUIDE = `# Paying with PipRail \u2014 the agent contract
3104
+
3105
+ You can pay for x402 "402 Payment Required" resources autonomously. Money moves
3106
+ straight from your wallet to the server; PipRail custodies nothing. Follow this.
3107
+
3108
+ ## The loop: quote \u2192 plan \u2192 pay
3109
+ 1. piprail_quote_payment(url) \u2014 PRICE it. Returns the amount, token, chain, and
3110
+ whether it is within your spend policy. No funds move. Use it to decide if a
3111
+ resource is worth buying.
3112
+ 2. piprail_plan_payment(url) \u2014 can I afford it NOW? Reads your balance, native gas,
3113
+ and recipient-readiness across every rail, and returns { payable, best,
3114
+ fundingHint, session? }. If payable is false, do NOT attempt the payment \u2014
3115
+ fundingHint says exactly what to fix.
3116
+ 3. piprail_pay_request(url, method?, body?) \u2014 PAY (only if the plan was payable)
3117
+ and return the result.
3118
+ Always plan before you pay so you never commit to a payment you cannot finish.
3119
+
3120
+ ## Reading a refusal \u2014 never crash, never double-spend
3121
+ A failed pay returns a STRUCTURED object, never a thrown error you must catch:
3122
+ { ok:false, code, reason, explain, ref?, reasonCode?, declined? }
3123
+ Branch on \`code\` (always reliable). Key cases:
3124
+ - declined:true with reasonCode:'SESSION_EXPIRED' \u2014 your time budget is over. This
3125
+ is TERMINAL: STOP. Do not retry ANY payment this process; it cannot be undone
3126
+ without a restart / a longer TTL.
3127
+ - declined:true with reasonCode:'APPROVAL' \u2014 a human (or hook) declined this
3128
+ payment. Terminal for this pay: do NOT auto-retry \u2014 they said no, or no one
3129
+ answered.
3130
+ - declined:true with reasonCode:'OUTSIDE_WINDOW' \u2014 your rolling rate-limit is
3131
+ exhausted. Wait for it to free, then retry; do not raise the amount.
3132
+ - declined:true with reasonCode:'POLICY' or 'BUDGET' \u2014 a spend cap or allowlist
3133
+ refused it. Don't retry the same payment; pick a cheaper/allowed one.
3134
+ - code:'INSUFFICIENT_FUNDS' \u2014 top up the wallet (token and/or native gas), retry.
3135
+ - code:'PAYMENT_TIMEOUT' / 'MAX_RETRIES_EXCEEDED' / 'CONFIRMATION_TIMEOUT' \u2014 the
3136
+ payment may ALREADY be on-chain. Recover using the proof on \`.ref\` (re-verify
3137
+ or re-submit it); never re-pay \u2014 a fresh payment would double-spend.
3138
+ - code:'NO_COMPATIBLE_ACCEPT' / 'UNSUPPORTED_SCHEME' \u2014 the 402 isn't payable on
3139
+ your chain/scheme; \`explain\` says whether it's the wrong chain or a scheme to enable.
3140
+
3141
+ ## Knowing your leash \u2014 call piprail_budget
3142
+ piprail_budget tells you how much budget and time you have left, per
3143
+ (network, asset), plus your spend so far. Read-only; moves no funds. Use it in
3144
+ Mode A to self-check before paying.
3145
+
3146
+ ## Two modes
3147
+ - Mode A (headless, default): you run FREE inside a pre-set budget + time
3148
+ envelope. The policy IS the consent \u2014 there is no per-payment prompt. Stay
3149
+ inside it; piprail_budget shows what's left.
3150
+ - Mode B (supervised): the host may ask a human to approve each payment. A
3151
+ decline/cancel/timeout comes back as declined:true (reasonCode:'APPROVAL') \u2014
3152
+ do NOT retry it as if it were a transient error.
3153
+
3154
+ ## Hard facts
3155
+ - Spend caps are PER (network, asset). There is no single cross-token dollar cap \u2014
3156
+ budgets aren't summed across tokens (no price oracle).
3157
+ - Spend totals and the time envelope live IN-MEMORY for THIS process; they reset on restart
3158
+ (a convenience, not a durable ledger).
3159
+ `;
3160
+ function agentGuide() {
3161
+ return PIPRAIL_AGENT_GUIDE;
3162
+ }
3163
+
2604
3164
  // src/agent.ts
3165
+ var OPEN_OBJECT = { type: "object", additionalProperties: true };
2605
3166
  async function readBody(res) {
2606
3167
  const text = await res.text();
2607
3168
  if (!text) return null;
@@ -2674,6 +3235,7 @@ function paymentTools(client) {
2674
3235
  required: ["url"],
2675
3236
  additionalProperties: false
2676
3237
  },
3238
+ outputSchema: OPEN_OBJECT,
2677
3239
  invoke: async (args) => {
2678
3240
  const quote = await client.quote(String(args.url));
2679
3241
  return quote ? { gated: true, ...quote } : { gated: false, url: String(args.url) };
@@ -2697,6 +3259,7 @@ function paymentTools(client) {
2697
3259
  required: ["url"],
2698
3260
  additionalProperties: false
2699
3261
  },
3262
+ outputSchema: OPEN_OBJECT,
2700
3263
  invoke: async (args) => {
2701
3264
  const plan = await client.planPayment(String(args.url));
2702
3265
  if (plan == null) return { gated: false, url: String(args.url) };
@@ -2705,6 +3268,8 @@ function paymentTools(client) {
2705
3268
  payable: plan.payable,
2706
3269
  status: plan.status,
2707
3270
  fundingHint: plan.fundingHint,
3271
+ // One model-readable line distilling the whole plan.
3272
+ summary: summarizePlan(plan),
2708
3273
  best: plan.best ? {
2709
3274
  network: plan.best.accept.network,
2710
3275
  symbol: plan.best.quote.symbol,
@@ -2720,23 +3285,25 @@ function paymentTools(client) {
2720
3285
  blockers: o.blockers,
2721
3286
  warnings: o.warnings,
2722
3287
  recipientReady: o.recipient.ready
2723
- }))
3288
+ })),
3289
+ // The session's time leash, present only when a time policy is configured.
3290
+ ...plan.session ? { session: plan.session } : {}
2724
3291
  };
2725
3292
  }
2726
3293
  },
2727
3294
  {
2728
3295
  name: "piprail_pay_request",
2729
- description: "Fetch an x402 payment-gated URL, automatically paying the required on-chain payment if needed (subject to the spend policy + approval hook). Returns the HTTP status, the response body, and a payment receipt if one settled. If the payment is refused by policy or the approval hook, returns { declined: true, reason } \u2014 no funds moved.",
3296
+ description: "Fetch an x402 payment-gated URL, automatically making the required payment if needed (subject to the spend policy + approval hook). Pays whichever rail the client is configured for \u2014 PipRail's backendless on-chain rail, or, when enabled, the standard `exact` rail (where the buyer signs and the server settles, so no buyer gas). Returns the HTTP status, the response body, and a payment receipt if one settled. If the payment is refused by policy or the approval hook, returns { declined: true, reason } \u2014 no funds moved.",
2730
3297
  annotations: {
2731
3298
  title: "Pay an x402 request",
2732
3299
  readOnlyHint: false,
2733
3300
  // this is the one tool that MOVES FUNDS
2734
3301
  destructiveHint: true,
2735
- // an on-chain payment is value-moving and not reversible
3302
+ // a payment is value-moving and not reversible
2736
3303
  idempotentHint: false,
2737
3304
  // paying twice = two payments
2738
3305
  openWorldHint: true
2739
- // fetches a URL and settles on-chain
3306
+ // fetches a URL and settles a payment
2740
3307
  },
2741
3308
  parameters: {
2742
3309
  type: "object",
@@ -2778,8 +3345,20 @@ function paymentTools(client) {
2778
3345
  receipt: parseReceipt(res)
2779
3346
  };
2780
3347
  } catch (err) {
2781
- if (err instanceof PaymentDeclinedError) {
2782
- return { declined: true, reason: err.message };
3348
+ if (err instanceof PipRailError) {
3349
+ const out = {
3350
+ ok: false,
3351
+ code: err.code,
3352
+ reason: err.message,
3353
+ explain: explainDecline(err)
3354
+ };
3355
+ if (err instanceof PaymentDeclinedError) {
3356
+ out.declined = true;
3357
+ if (err.reasonCode) out.reasonCode = err.reasonCode;
3358
+ }
3359
+ const ref = err.ref;
3360
+ if (typeof ref === "string") out.ref = ref;
3361
+ return out;
2783
3362
  }
2784
3363
  throw err;
2785
3364
  }
@@ -2817,10 +3396,57 @@ function paymentTools(client) {
2817
3396
  const outcomes = await client.register(String(args.url), opts);
2818
3397
  return { outcomes };
2819
3398
  }
3399
+ },
3400
+ {
3401
+ name: "piprail_budget",
3402
+ description: "Read how much of your spend budget and time leash is left \u2014 per (network, asset) remaining, the session time envelope, and your spend so far. Use it in Mode A (headless) to self-check BEFORE paying, so you never discover the leash by hitting a decline. Read-only; moves no funds. NOTE: totals and the time envelope are in-memory for THIS process and reset on restart.",
3403
+ annotations: {
3404
+ title: "Check remaining budget",
3405
+ readOnlyHint: true,
3406
+ // reads the in-memory ledger + policy; never pays
3407
+ idempotentHint: true
3408
+ // a pure read
3409
+ },
3410
+ parameters: { type: "object", properties: {}, additionalProperties: false },
3411
+ outputSchema: OPEN_OBJECT,
3412
+ invoke: async () => {
3413
+ const spent = client.spent();
3414
+ const budget = client.budget();
3415
+ return {
3416
+ spent,
3417
+ remaining: budget.byAsset,
3418
+ session: budget.session,
3419
+ report: formatSpendReport(spent)
3420
+ };
3421
+ }
3422
+ },
3423
+ {
3424
+ name: "piprail_guide",
3425
+ description: "Read the PipRail agent contract \u2014 the quote \u2192 plan \u2192 pay loop, how to read a refusal (and which declines are TERMINAL), the never-re-pay rule for broadcast-but-unconfirmed payments, and Mode A (headless) vs Mode B (supervised). Read-only; call it once if unsure how to use these tools.",
3426
+ annotations: {
3427
+ title: "How to use PipRail",
3428
+ readOnlyHint: true,
3429
+ idempotentHint: true
3430
+ },
3431
+ parameters: { type: "object", properties: {}, additionalProperties: false },
3432
+ invoke: async () => ({ guide: PIPRAIL_AGENT_GUIDE })
2820
3433
  }
2821
3434
  ];
2822
3435
  }
2823
3436
 
3437
+ // src/classify.ts
3438
+ function classifyChallenge(challenge, opts) {
3439
+ const accepts = challenge.accepts ?? [];
3440
+ const offeredSchemes = [...new Set(accepts.map((a) => a.scheme))];
3441
+ const offeredNetworks = [...new Set(accepts.map((a) => a.network))];
3442
+ const onClientChain = accepts.some((a) => a.network === opts.network);
3443
+ const payableScheme = accepts.some(
3444
+ (a) => a.network === opts.network && opts.schemes.includes(a.scheme)
3445
+ );
3446
+ const verdict = accepts.length === 0 ? "NO_RAIL" : payableScheme ? "PAYABLE_RAIL" : onClientChain ? "UNPAYABLE_SCHEME" : "WRONG_CHAIN";
3447
+ return { onClientChain, payableScheme, offeredSchemes, offeredNetworks, verdict };
3448
+ }
3449
+
2824
3450
  // src/discovery.ts
2825
3451
  var GENERATOR = "@piprail/sdk \xB7 https://piprail.com";
2826
3452
  function buildBazaarExtension(descriptor = {}) {
@@ -3278,7 +3904,9 @@ function createPaymentGate(options) {
3278
3904
  amount: accept.amount,
3279
3905
  payTo: accept.payTo,
3280
3906
  maxTimeoutSeconds: accept.maxTimeoutSeconds,
3281
- extra: { name: accept.extra.name, version: accept.extra.version }
3907
+ // name/version are OPTIONAL on the wire type (a foreign rail may omit them), but the
3908
+ // gate's OWN exact rail always read them on-chain at resolution — so they're present here.
3909
+ extra: { name: accept.extra.name ?? "", version: accept.extra.version ?? "" }
3282
3910
  },
3283
3911
  receipt: { network: accept.network, asset: accept.asset, payTo: accept.payTo, amount: accept.amount },
3284
3912
  payerHint: exact.payload.authorization.from
@@ -3364,6 +3992,7 @@ export {
3364
3992
  MissingDriverError,
3365
3993
  NoCompatibleAcceptError,
3366
3994
  NonReplayableBodyError,
3995
+ PIPRAIL_AGENT_GUIDE,
3367
3996
  PaymentDeclinedError,
3368
3997
  PaymentTimeoutError,
3369
3998
  PipRailClient,
@@ -3372,11 +4001,14 @@ export {
3372
4001
  SettlementError,
3373
4002
  UnknownTokenError,
3374
4003
  UnsupportedNetworkError,
4004
+ UnsupportedSchemeError,
3375
4005
  WrongChainError,
3376
4006
  WrongFamilyError,
4007
+ agentGuide,
3377
4008
  buildBazaarExtension,
3378
4009
  buildChallengeHeader,
3379
4010
  buildExactAuthorization,
4011
+ buildExactSignatureHeader,
3380
4012
  buildOpenApi,
3381
4013
  buildReceiptHeader,
3382
4014
  buildSignatureHeader,
@@ -3384,17 +4016,21 @@ export {
3384
4016
  buildX402DnsTxt,
3385
4017
  chainIdForExactNetwork,
3386
4018
  claim402IndexDomain,
4019
+ classifyChallenge,
3387
4020
  createPaymentGate,
3388
4021
  decorateOutcome,
3389
4022
  eip3009Abi,
3390
4023
  encodeXPaymentHeader,
3391
4024
  evaluatePolicy,
4025
+ explainDecline,
4026
+ formatSpendReport,
3392
4027
  getDirectoryInfo,
3393
4028
  normalizeNetwork,
3394
4029
  parseChallenge,
3395
4030
  parseExactPaymentHeader,
3396
4031
  parseExactRequirements,
3397
4032
  parseReceipt,
4033
+ parseSettleResponse,
3398
4034
  parseSignatureHeader,
3399
4035
  paymentTools,
3400
4036
  pickAccept,
@@ -3407,6 +4043,7 @@ export {
3407
4043
  resolveChain,
3408
4044
  searchOpenIndexes,
3409
4045
  settleViaFacilitator,
4046
+ summarizePlan,
3410
4047
  toInsufficientFundsError,
3411
4048
  toInvalidBody,
3412
4049
  verify402IndexDomain