@piprail/sdk 1.14.0 → 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 (29) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/ERRORS.md +15 -1
  3. package/README.md +38 -2
  4. package/STANDARDS.md +4 -2
  5. package/dist/{algorand-OIHGJN5S.cjs → algorand-EJ3S2V7E.cjs} +17 -17
  6. package/dist/{algorand-7EUZYL2Z.js → algorand-F3OYB534.js} +1 -1
  7. package/dist/{aptos-WDWZOU25.cjs → aptos-GJGIZHNI.cjs} +16 -16
  8. package/dist/{aptos-CDEYDDM5.js → aptos-SUXOVP7B.js} +1 -1
  9. package/dist/{chunk-H3A4KWLJ.js → chunk-ILPABTI2.js} +6 -0
  10. package/dist/{chunk-FTKVCP6K.cjs → chunk-PA6YD3HL.cjs} +17 -11
  11. package/dist/index.cjs +493 -115
  12. package/dist/index.d.cts +264 -12
  13. package/dist/index.d.ts +264 -12
  14. package/dist/index.js +403 -25
  15. package/dist/{near-DT6LRIKB.js → near-LM7S3WUD.js} +1 -1
  16. package/dist/{near-FUH3VAXT.cjs → near-ZJLZE26R.cjs} +19 -19
  17. package/dist/{solana-QUVXPKBZ.cjs → solana-MPPE6K24.cjs} +14 -14
  18. package/dist/{solana-3TRYD4QB.js → solana-WDKWWF33.js} +1 -1
  19. package/dist/{stellar-IK3UML6O.js → stellar-FIJPQZVW.js} +1 -1
  20. package/dist/{stellar-APZEBFAD.cjs → stellar-XHLLNHQP.cjs} +21 -21
  21. package/dist/{sui-L7BQNJWO.cjs → sui-6CVLEXLA.cjs} +17 -17
  22. package/dist/{sui-VSE63WQM.js → sui-B7AVN7NK.js} +1 -1
  23. package/dist/{ton-QHGQLJX2.js → ton-CHJ26BVA.js} +1 -1
  24. package/dist/{ton-5DLKKOFE.cjs → ton-RNEFN25G.cjs} +14 -14
  25. package/dist/{tron-2N2GA62O.js → tron-DD3JDROV.js} +1 -1
  26. package/dist/{tron-HHIT6WKY.cjs → tron-TKJHNFGM.cjs} +24 -24
  27. package/dist/{xrpl-2GZMDYW5.js → xrpl-GTUPP6SK.js} +1 -1
  28. package/dist/{xrpl-USEG4AHX.cjs → xrpl-XN2NBNGI.cjs} +21 -21
  29. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -22,7 +22,7 @@
22
22
 
23
23
 
24
24
 
25
- var _chunkFTKVCP6Kcjs = require('./chunk-FTKVCP6K.cjs');
25
+ var _chunkPA6YD3HLcjs = require('./chunk-PA6YD3HL.cjs');
26
26
 
27
27
  // src/drivers/registry.ts
28
28
  var byFamily = /* @__PURE__ */ new Map();
@@ -51,13 +51,13 @@ function resolveNetwork(opts) {
51
51
  const family = familyForChain(opts.chain);
52
52
  const driver = byFamily.get(family);
53
53
  if (!driver) {
54
- throw new (0, _chunkFTKVCP6Kcjs.UnsupportedNetworkError)(
54
+ throw new (0, _chunkPA6YD3HLcjs.UnsupportedNetworkError)(
55
55
  `No driver registered for the "${family}" family \u2014 it may not be mounted yet (use the async resolveNetwork()).`
56
56
  );
57
57
  }
58
58
  const net = driver.resolve(opts);
59
59
  if (!net) {
60
- throw new (0, _chunkFTKVCP6Kcjs.UnsupportedNetworkError)(
60
+ throw new (0, _chunkPA6YD3HLcjs.UnsupportedNetworkError)(
61
61
  `The ${family} driver didn't recognise this chain input.`
62
62
  );
63
63
  }
@@ -320,12 +320,12 @@ function createWalletAdapter(config, resolved) {
320
320
  }
321
321
  const wc = config.walletClient;
322
322
  if (!wc.account) {
323
- throw new (0, _chunkFTKVCP6Kcjs.WrongFamilyError)(
323
+ throw new (0, _chunkPA6YD3HLcjs.WrongFamilyError)(
324
324
  "chain is EVM; the provided walletClient has no attached account. Use `createWalletClient({ account, chain, transport })`, or pass { privateKey }."
325
325
  );
326
326
  }
327
327
  if (wc.chain && wc.chain.id !== resolved.chainId) {
328
- throw new (0, _chunkFTKVCP6Kcjs.WrongChainError)(
328
+ throw new (0, _chunkPA6YD3HLcjs.WrongChainError)(
329
329
  `PipRailClient: walletClient is on chain ${wc.chain.id} but the SDK was configured with chain ${resolved.chainId}. They must match.`
330
330
  );
331
331
  }
@@ -608,19 +608,19 @@ async function payExactEvm(input) {
608
608
  code = void 0;
609
609
  }
610
610
  if (code && code !== "0x") {
611
- throw new (0, _chunkFTKVCP6Kcjs.UnsupportedSchemeError)(
611
+ throw new (0, _chunkPA6YD3HLcjs.UnsupportedSchemeError)(
612
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
613
  );
614
614
  }
615
615
  const domain = await readExactDomain(publicClient, accept.asset);
616
616
  if (!domain) {
617
- throw new (0, _chunkFTKVCP6Kcjs.UnsupportedSchemeError)(
617
+ throw new (0, _chunkPA6YD3HLcjs.UnsupportedSchemeError)(
618
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
619
  );
620
620
  }
621
621
  const g = globalThis.crypto;
622
622
  if (!_optionalChain([g, 'optionalAccess', _5 => _5.getRandomValues])) {
623
- throw new (0, _chunkFTKVCP6Kcjs.UnsupportedSchemeError)(
623
+ throw new (0, _chunkPA6YD3HLcjs.UnsupportedSchemeError)(
624
624
  "this runtime lacks Web Crypto (globalThis.crypto.getRandomValues); the exact rail needs a CSPRNG nonce."
625
625
  );
626
626
  }
@@ -841,7 +841,7 @@ async function verifyAndSettleExactEvm(input) {
841
841
  args: writeArgs
842
842
  });
843
843
  } catch (err) {
844
- throw new (0, _chunkFTKVCP6Kcjs.SettlementError)(
844
+ throw new (0, _chunkPA6YD3HLcjs.SettlementError)(
845
845
  `exact settle: the merchant relayer failed to broadcast transferWithAuthorization (${shorten(err instanceof Error ? err.message : String(err))}). The payer's authorization is still valid and unused \u2014 fund/fix the relayer and the payer can retry.`,
846
846
  { cause: err }
847
847
  );
@@ -853,7 +853,7 @@ async function verifyAndSettleExactEvm(input) {
853
853
  return { ok: false, error: "tx_reverted", detail: `Settlement tx ${txHash} reverted on-chain.` };
854
854
  }
855
855
  } catch (err) {
856
- throw new (0, _chunkFTKVCP6Kcjs.SettlementError)(
856
+ throw new (0, _chunkPA6YD3HLcjs.SettlementError)(
857
857
  `exact settle: broadcast ${txHash} but couldn't confirm it (${shorten(err instanceof Error ? err.message : String(err))}).`,
858
858
  { cause: err }
859
859
  );
@@ -1068,15 +1068,15 @@ function makeEvmNetwork(resolved) {
1068
1068
  const info = resolved.tokens[token.toUpperCase()];
1069
1069
  if (!info) {
1070
1070
  const known = Object.keys(resolved.tokens).join(", ") || "(none built in)";
1071
- throw new (0, _chunkFTKVCP6Kcjs.UnknownTokenError)(
1071
+ throw new (0, _chunkPA6YD3HLcjs.UnknownTokenError)(
1072
1072
  `token "${token}" isn't built in for ${resolved.chain.name} (known: ${known}). Pass { address, decimals } instead, or use 'native'.`
1073
1073
  );
1074
1074
  }
1075
1075
  return { asset: info.address, decimals: info.decimals, symbol: info.symbol };
1076
1076
  }
1077
- _chunkFTKVCP6Kcjs.rejectForeignToken.call(void 0, token, "evm", network);
1077
+ _chunkPA6YD3HLcjs.rejectForeignToken.call(void 0, token, "evm", network);
1078
1078
  if (!("address" in token)) {
1079
- throw new (0, _chunkFTKVCP6Kcjs.WrongFamilyError)(
1079
+ throw new (0, _chunkPA6YD3HLcjs.WrongFamilyError)(
1080
1080
  `chain ${network} is EVM; a custom token must be { address, decimals }.`
1081
1081
  );
1082
1082
  }
@@ -1108,14 +1108,14 @@ function makeEvmNetwork(resolved) {
1108
1108
  },
1109
1109
  assertValidPayTo(payTo) {
1110
1110
  if (!_viem.isAddress.call(void 0, payTo)) {
1111
- throw new (0, _chunkFTKVCP6Kcjs.WrongFamilyError)(
1111
+ throw new (0, _chunkPA6YD3HLcjs.WrongFamilyError)(
1112
1112
  `chain ${network} is EVM, but payTo "${payTo}" is not a valid 0x address.`
1113
1113
  );
1114
1114
  }
1115
1115
  },
1116
1116
  bindWallet(wallet) {
1117
1117
  if (typeof wallet !== "object" || wallet === null || !("privateKey" in wallet) && !("walletClient" in wallet)) {
1118
- throw new (0, _chunkFTKVCP6Kcjs.WrongFamilyError)(
1118
+ throw new (0, _chunkPA6YD3HLcjs.WrongFamilyError)(
1119
1119
  `chain ${network} is EVM; wallet must be { privateKey } or { walletClient }.`
1120
1120
  );
1121
1121
  }
@@ -1132,12 +1132,12 @@ function makeEvmNetwork(resolved) {
1132
1132
  });
1133
1133
  } catch (err) {
1134
1134
  if (isViemInsufficientFunds(err)) {
1135
- throw new (0, _chunkFTKVCP6Kcjs.InsufficientFundsError)(
1135
+ throw new (0, _chunkPA6YD3HLcjs.InsufficientFundsError)(
1136
1136
  err instanceof Error ? err.message : "Insufficient funds for payment.",
1137
1137
  { cause: err }
1138
1138
  );
1139
1139
  }
1140
- throw _nullishCoalesce(_chunkFTKVCP6Kcjs.toInsufficientFundsError.call(void 0, err), () => ( err));
1140
+ throw _nullishCoalesce(_chunkPA6YD3HLcjs.toInsufficientFundsError.call(void 0, err), () => ( err));
1141
1141
  }
1142
1142
  },
1143
1143
  async confirm(ref, minConfirmations) {
@@ -1148,7 +1148,7 @@ function makeEvmNetwork(resolved) {
1148
1148
  });
1149
1149
  return { height: receipt.blockNumber.toString() };
1150
1150
  } catch (err) {
1151
- throw new (0, _chunkFTKVCP6Kcjs.ConfirmationTimeoutError)(
1151
+ throw new (0, _chunkPA6YD3HLcjs.ConfirmationTimeoutError)(
1152
1152
  `EVM tx ${ref} did not reach ${minConfirmations} confirmation(s) in time.`,
1153
1153
  { cause: err }
1154
1154
  );
@@ -1157,7 +1157,7 @@ function makeEvmNetwork(resolved) {
1157
1157
  async estimateCost(accept) {
1158
1158
  const { decimals, symbol } = resolved.chain.nativeCurrency;
1159
1159
  if (accept.scheme === "exact") {
1160
- return _chunkFTKVCP6Kcjs.nativeCost.call(void 0, {
1160
+ return _chunkPA6YD3HLcjs.nativeCost.call(void 0, {
1161
1161
  symbol,
1162
1162
  decimals,
1163
1163
  fee: 0n,
@@ -1168,7 +1168,7 @@ function makeEvmNetwork(resolved) {
1168
1168
  const gasLimit = accept.asset === "native" ? 21000n : 65000n;
1169
1169
  try {
1170
1170
  const gasPrice = await publicClient.getGasPrice();
1171
- return _chunkFTKVCP6Kcjs.nativeCost.call(void 0, {
1171
+ return _chunkPA6YD3HLcjs.nativeCost.call(void 0, {
1172
1172
  symbol,
1173
1173
  decimals,
1174
1174
  fee: gasPrice * gasLimit,
@@ -1177,7 +1177,7 @@ function makeEvmNetwork(resolved) {
1177
1177
  });
1178
1178
  } catch (e15) {
1179
1179
  const gasPrice = 5000000000n;
1180
- return _chunkFTKVCP6Kcjs.nativeCost.call(void 0, {
1180
+ return _chunkPA6YD3HLcjs.nativeCost.call(void 0, {
1181
1181
  symbol,
1182
1182
  decimals,
1183
1183
  fee: gasPrice * gasLimit,
@@ -1271,9 +1271,9 @@ var loaders = {
1271
1271
  solana: async () => {
1272
1272
  let mod;
1273
1273
  try {
1274
- mod = await Promise.resolve().then(() => _interopRequireWildcard(require("./solana-QUVXPKBZ.cjs")));
1274
+ mod = await Promise.resolve().then(() => _interopRequireWildcard(require("./solana-MPPE6K24.cjs")));
1275
1275
  } catch (cause) {
1276
- throw new (0, _chunkFTKVCP6Kcjs.MissingDriverError)(
1276
+ throw new (0, _chunkPA6YD3HLcjs.MissingDriverError)(
1277
1277
  `Solana selected, but its packages aren't installed. Run: npm install @solana/web3.js @solana/spl-token bs58`,
1278
1278
  { cause }
1279
1279
  );
@@ -1283,9 +1283,9 @@ var loaders = {
1283
1283
  ton: async () => {
1284
1284
  let mod;
1285
1285
  try {
1286
- mod = await Promise.resolve().then(() => _interopRequireWildcard(require("./ton-5DLKKOFE.cjs")));
1286
+ mod = await Promise.resolve().then(() => _interopRequireWildcard(require("./ton-RNEFN25G.cjs")));
1287
1287
  } catch (cause) {
1288
- throw new (0, _chunkFTKVCP6Kcjs.MissingDriverError)(
1288
+ throw new (0, _chunkPA6YD3HLcjs.MissingDriverError)(
1289
1289
  `TON selected, but its packages aren't installed. Run: npm install @ton/ton @ton/core @ton/crypto`,
1290
1290
  { cause }
1291
1291
  );
@@ -1295,9 +1295,9 @@ var loaders = {
1295
1295
  stellar: async () => {
1296
1296
  let mod;
1297
1297
  try {
1298
- mod = await Promise.resolve().then(() => _interopRequireWildcard(require("./stellar-APZEBFAD.cjs")));
1298
+ mod = await Promise.resolve().then(() => _interopRequireWildcard(require("./stellar-XHLLNHQP.cjs")));
1299
1299
  } catch (cause) {
1300
- throw new (0, _chunkFTKVCP6Kcjs.MissingDriverError)(
1300
+ throw new (0, _chunkPA6YD3HLcjs.MissingDriverError)(
1301
1301
  `Stellar selected, but its package isn't installed. Run: npm install @stellar/stellar-sdk`,
1302
1302
  { cause }
1303
1303
  );
@@ -1307,9 +1307,9 @@ var loaders = {
1307
1307
  xrpl: async () => {
1308
1308
  let mod;
1309
1309
  try {
1310
- mod = await Promise.resolve().then(() => _interopRequireWildcard(require("./xrpl-USEG4AHX.cjs")));
1310
+ mod = await Promise.resolve().then(() => _interopRequireWildcard(require("./xrpl-XN2NBNGI.cjs")));
1311
1311
  } catch (cause) {
1312
- throw new (0, _chunkFTKVCP6Kcjs.MissingDriverError)(
1312
+ throw new (0, _chunkPA6YD3HLcjs.MissingDriverError)(
1313
1313
  `XRPL selected, but its package isn't installed. Run: npm install xrpl`,
1314
1314
  { cause }
1315
1315
  );
@@ -1319,9 +1319,9 @@ var loaders = {
1319
1319
  tron: async () => {
1320
1320
  let mod;
1321
1321
  try {
1322
- mod = await Promise.resolve().then(() => _interopRequireWildcard(require("./tron-HHIT6WKY.cjs")));
1322
+ mod = await Promise.resolve().then(() => _interopRequireWildcard(require("./tron-TKJHNFGM.cjs")));
1323
1323
  } catch (cause) {
1324
- throw new (0, _chunkFTKVCP6Kcjs.MissingDriverError)(
1324
+ throw new (0, _chunkPA6YD3HLcjs.MissingDriverError)(
1325
1325
  `Tron selected, but its package isn't installed. Run: npm install tronweb`,
1326
1326
  { cause }
1327
1327
  );
@@ -1331,9 +1331,9 @@ var loaders = {
1331
1331
  sui: async () => {
1332
1332
  let mod;
1333
1333
  try {
1334
- mod = await Promise.resolve().then(() => _interopRequireWildcard(require("./sui-L7BQNJWO.cjs")));
1334
+ mod = await Promise.resolve().then(() => _interopRequireWildcard(require("./sui-6CVLEXLA.cjs")));
1335
1335
  } catch (cause) {
1336
- throw new (0, _chunkFTKVCP6Kcjs.MissingDriverError)(
1336
+ throw new (0, _chunkPA6YD3HLcjs.MissingDriverError)(
1337
1337
  `Sui selected, but its package isn't installed. Run: npm install @mysten/sui`,
1338
1338
  { cause }
1339
1339
  );
@@ -1343,9 +1343,9 @@ var loaders = {
1343
1343
  near: async () => {
1344
1344
  let mod;
1345
1345
  try {
1346
- mod = await Promise.resolve().then(() => _interopRequireWildcard(require("./near-FUH3VAXT.cjs")));
1346
+ mod = await Promise.resolve().then(() => _interopRequireWildcard(require("./near-ZJLZE26R.cjs")));
1347
1347
  } catch (cause) {
1348
- throw new (0, _chunkFTKVCP6Kcjs.MissingDriverError)(
1348
+ throw new (0, _chunkPA6YD3HLcjs.MissingDriverError)(
1349
1349
  `NEAR selected, but its package isn't installed. Run: npm install near-api-js`,
1350
1350
  { cause }
1351
1351
  );
@@ -1355,9 +1355,9 @@ var loaders = {
1355
1355
  aptos: async () => {
1356
1356
  let mod;
1357
1357
  try {
1358
- mod = await Promise.resolve().then(() => _interopRequireWildcard(require("./aptos-WDWZOU25.cjs")));
1358
+ mod = await Promise.resolve().then(() => _interopRequireWildcard(require("./aptos-GJGIZHNI.cjs")));
1359
1359
  } catch (cause) {
1360
- throw new (0, _chunkFTKVCP6Kcjs.MissingDriverError)(
1360
+ throw new (0, _chunkPA6YD3HLcjs.MissingDriverError)(
1361
1361
  `Aptos selected, but its package isn't installed. Run: npm install @aptos-labs/ts-sdk`,
1362
1362
  { cause }
1363
1363
  );
@@ -1367,9 +1367,9 @@ var loaders = {
1367
1367
  algorand: async () => {
1368
1368
  let mod;
1369
1369
  try {
1370
- mod = await Promise.resolve().then(() => _interopRequireWildcard(require("./algorand-OIHGJN5S.cjs")));
1370
+ mod = await Promise.resolve().then(() => _interopRequireWildcard(require("./algorand-EJ3S2V7E.cjs")));
1371
1371
  } catch (cause) {
1372
- throw new (0, _chunkFTKVCP6Kcjs.MissingDriverError)(
1372
+ throw new (0, _chunkPA6YD3HLcjs.MissingDriverError)(
1373
1373
  `Algorand selected, but its package isn't installed. Run: npm install algosdk`,
1374
1374
  { cause }
1375
1375
  );
@@ -1842,7 +1842,18 @@ function encodeBase642(str) {
1842
1842
 
1843
1843
  // src/policy.ts
1844
1844
  var ALLOW = { allowed: true };
1845
- 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
+ }
1846
1857
  function hostMatches(host, pattern) {
1847
1858
  if (pattern.startsWith("*.")) {
1848
1859
  const suffix = pattern.slice(1);
@@ -1858,18 +1869,26 @@ function chainMatches(intent, allowed) {
1858
1869
  const id = "id" in allowed ? allowed.id : void 0;
1859
1870
  return id !== void 0 && intent.network === `eip155:${id}`;
1860
1871
  }
1861
- function evaluatePolicy(intent, policy, spentForAssetBase) {
1872
+ function evaluatePolicy(intent, policy, spentForAssetBase, ctx) {
1862
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
+ }
1863
1880
  if (policy.chains && !policy.chains.some((c) => chainMatches(intent, c))) {
1864
1881
  return deny(
1882
+ "CHAIN",
1865
1883
  `chain ${intent.network} is not in the allowed set (policy.chains).`
1866
1884
  );
1867
1885
  }
1868
1886
  if (policy.hosts && !policy.hosts.some((h) => hostMatches(intent.host, h))) {
1869
- 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).`);
1870
1888
  }
1871
1889
  if (!intent.recognized && !policy.allowUnknownTokens) {
1872
1890
  return deny(
1891
+ "UNKNOWN_TOKEN",
1873
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.`
1874
1893
  );
1875
1894
  }
@@ -1882,34 +1901,52 @@ function evaluatePolicy(intent, policy, spentForAssetBase) {
1882
1901
  });
1883
1902
  if (!matches) {
1884
1903
  return deny(
1904
+ "TOKEN",
1885
1905
  `token ${_nullishCoalesce(intent.symbol, () => ( intent.asset))} is not in the allowed set (policy.tokens).`
1886
1906
  );
1887
1907
  }
1888
1908
  }
1889
1909
  if (policy.maxAmount !== void 0) {
1890
- const cap = _chunkFTKVCP6Kcjs.floorUnits.call(void 0, policy.maxAmount, intent.decimals);
1910
+ const cap = _chunkPA6YD3HLcjs.floorUnits.call(void 0, policy.maxAmount, intent.decimals);
1891
1911
  if (intent.amountBase > cap) {
1892
1912
  return deny(
1913
+ "MAX_AMOUNT",
1893
1914
  `payment of ${intent.amountBase} base units exceeds policy.maxAmount ` + `(${policy.maxAmount} ${_nullishCoalesce(intent.symbol, () => ( ""))}).`.trimEnd()
1894
1915
  );
1895
1916
  }
1896
1917
  }
1897
1918
  if (policy.maxTotal !== void 0) {
1898
- const cap = _chunkFTKVCP6Kcjs.floorUnits.call(void 0, policy.maxTotal, intent.decimals);
1919
+ const cap = _chunkPA6YD3HLcjs.floorUnits.call(void 0, policy.maxTotal, intent.decimals);
1899
1920
  if (spentForAssetBase + intent.amountBase > cap) {
1900
1921
  return deny(
1922
+ "MAX_TOTAL",
1901
1923
  `this payment would push spend on ${_nullishCoalesce(intent.symbol, () => ( intent.asset))} past policy.maxTotal (${policy.maxTotal}); already spent ${spentForAssetBase} base units.`
1902
1924
  );
1903
1925
  }
1904
1926
  }
1927
+ if (ctx && policy.windowTotal !== void 0 && policy.windowSeconds !== void 0) {
1928
+ const cap = _chunkPA6YD3HLcjs.floorUnits.call(void 0, 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 ${_nullishCoalesce(intent.symbol, () => ( intent.asset))}.`
1933
+ );
1934
+ }
1935
+ }
1905
1936
  return ALLOW;
1906
1937
  }
1907
1938
 
1908
1939
  // src/ledger.ts
1909
1940
  var keyFor = (network, asset) => `${network}|${asset}`;
1910
- var SpendLedger = (_class = class {constructor() { _class.prototype.__init.call(this);_class.prototype.__init2.call(this); }
1941
+ var SpendLedger = (_class = class {constructor() { _class.prototype.__init.call(this);_class.prototype.__init2.call(this);_class.prototype.__init3.call(this); }
1911
1942
  __init() {this.records = []}
1912
1943
  __init2() {this.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
+ __init3() {this.sessionStart = Date.now()}
1913
1950
  /** Record a settled payment. `decimals` is the TRUE token decimals (for the
1914
1951
  * per-asset running total used by maxTotal + the formatted summary). */
1915
1952
  record(r, decimals) {
@@ -1935,6 +1972,38 @@ var SpendLedger = (_class = class {constructor() { _class.prototype.__init.call(
1935
1972
  totalFor(network, asset) {
1936
1973
  return _nullishCoalesce(_optionalChain([this, 'access', _25 => _25.buckets, 'access', _26 => _26.get, 'call', _27 => _27(keyFor(network, asset)), 'optionalAccess', _28 => _28.total]), () => ( 0n));
1937
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
+ }
1938
2007
  /** An immutable snapshot of all spend so far. */
1939
2008
  summary() {
1940
2009
  return {
@@ -1945,7 +2014,7 @@ var SpendLedger = (_class = class {constructor() { _class.prototype.__init.call(
1945
2014
  symbol: b.symbol,
1946
2015
  decimals: b.decimals,
1947
2016
  totalBase: b.total.toString(),
1948
- totalFormatted: _chunkFTKVCP6Kcjs.formatUnits.call(void 0, b.total, b.decimals),
2017
+ totalFormatted: _chunkPA6YD3HLcjs.formatUnits.call(void 0, b.total, b.decimals),
1949
2018
  count: b.count
1950
2019
  })),
1951
2020
  records: [...this.records]
@@ -1968,15 +2037,46 @@ var PipRailClient = (_class2 = class {
1968
2037
 
1969
2038
  // Per-asset tally of everything this client has paid (powers spent() and the
1970
2039
  // policy's maxTotal cap).
1971
- __init3() {this.ledger = new SpendLedger()}
2040
+ __init4() {this.ledger = new SpendLedger()}
1972
2041
  // Resolved lazily on first request — this is what lets Solana (and future
1973
2042
  // families) auto-mount with no setup call.
1974
2043
 
1975
- constructor(opts) {;_class2.prototype.__init3.call(this);
2044
+ constructor(opts) {;_class2.prototype.__init4.call(this);
1976
2045
  this.opts = opts;
1977
2046
  this.maxRetries = Math.max(1, _nullishCoalesce(opts.maxPaymentRetries, () => ( 3)));
1978
2047
  this.retryTimeoutMs = _nullishCoalesce(opts.retryTimeoutMs, () => ( 3e4));
1979
2048
  this.onEvent = _nullishCoalesce(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
+ }
1980
2080
  }
1981
2081
  /** Emit an observability event, never letting a throwing handler break the
1982
2082
  * payment flow (mirrors the server gate's `onPaid` isolation). */
@@ -2074,6 +2174,64 @@ var PipRailClient = (_class2 = class {
2074
2174
  spent() {
2075
2175
  return this.ledger.summary();
2076
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: _optionalChain([view, 'optionalAccess', _32 => _32.expiresAt]) != null ? new Date(view.expiresAt).toISOString() : null,
2190
+ secondsRemaining: _nullishCoalesce(_optionalChain([view, 'optionalAccess', _33 => _33.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 = _optionalChain([this, 'access', _34 => _34.opts, 'access', _35 => _35.policy, 'optionalAccess', _36 => _36.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 = _chunkPA6YD3HLcjs.floorUnits.call(void 0, 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: _chunkPA6YD3HLcjs.formatUnits.call(void 0, 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
+ }
2077
2235
  /**
2078
2236
  * Plan a payment for a gated URL — WITHOUT paying. The read-only completion of
2079
2237
  * the `quote()` → `estimateCost()` → **`planPayment()`** trio: it surveys every
@@ -2094,11 +2252,11 @@ var PipRailClient = (_class2 = class {
2094
2252
  * the plan yourself. No funds move.
2095
2253
  */
2096
2254
  async planPayment(url, init) {
2097
- const res = await fetch(url, { ..._nullishCoalesce(init, () => ( {})), method: _nullishCoalesce(_optionalChain([init, 'optionalAccess', _32 => _32.method]), () => ( "GET")) });
2255
+ const res = await fetch(url, { ..._nullishCoalesce(init, () => ( {})), method: _nullishCoalesce(_optionalChain([init, 'optionalAccess', _37 => _37.method]), () => ( "GET")) });
2098
2256
  if (res.status !== 402) return null;
2099
2257
  const challenge = await parseChallenge(res);
2100
2258
  if (!challenge) {
2101
- throw new (0, _chunkFTKVCP6Kcjs.InvalidEnvelopeError)("402 response did not include a parseable x402 challenge.");
2259
+ throw new (0, _chunkPA6YD3HLcjs.InvalidEnvelopeError)("402 response did not include a parseable x402 challenge.");
2102
2260
  }
2103
2261
  const { net, wallet } = await this.ensure();
2104
2262
  return this.planFromChallenge(net, wallet, challenge, url, this.resolveSchemes());
@@ -2261,24 +2419,24 @@ var PipRailClient = (_class2 = class {
2261
2419
  * streams throw `NonReplayableBodyError`.
2262
2420
  */
2263
2421
  async fetch(url, init) {
2264
- const body = _optionalChain([init, 'optionalAccess', _33 => _33.body]);
2422
+ const body = _optionalChain([init, 'optionalAccess', _38 => _38.body]);
2265
2423
  if (body !== void 0 && body !== null && !isReplayableBodyInit(body)) {
2266
- throw new (0, _chunkFTKVCP6Kcjs.NonReplayableBodyError)(
2424
+ throw new (0, _chunkPA6YD3HLcjs.NonReplayableBodyError)(
2267
2425
  "fetch(): init.body is not replayable. Pass a string, FormData, URLSearchParams, ArrayBuffer, or Blob \u2014 not a ReadableStream."
2268
2426
  );
2269
2427
  }
2270
2428
  const firstResponse = await fetch(url, init);
2271
2429
  if (firstResponse.status !== 402) return firstResponse;
2272
- const schemes = this.resolveSchemes(_optionalChain([init, 'optionalAccess', _34 => _34.schemes]));
2430
+ const schemes = this.resolveSchemes(_optionalChain([init, 'optionalAccess', _39 => _39.schemes]));
2273
2431
  const resolved = await this.resolveChallenge(url, firstResponse, schemes);
2274
2432
  const { net, wallet, challenge } = resolved;
2275
2433
  let accept = resolved.accept;
2276
2434
  let quote = resolved.quote;
2277
- const autoRoute = _nullishCoalesce(_nullishCoalesce(_optionalChain([init, 'optionalAccess', _35 => _35.autoRoute]), () => ( this.opts.autoRoute)), () => ( false));
2435
+ const autoRoute = _nullishCoalesce(_nullishCoalesce(_optionalChain([init, 'optionalAccess', _40 => _40.autoRoute]), () => ( this.opts.autoRoute)), () => ( false));
2278
2436
  if (autoRoute) {
2279
2437
  const plan = await this.planFromChallenge(net, wallet, challenge, url, schemes);
2280
2438
  if (!plan.best) {
2281
- throw new (0, _chunkFTKVCP6Kcjs.PaymentDeclinedError)(_nullishCoalesce(plan.fundingHint, () => ( "No rail is settleable for this payment.")));
2439
+ throw new (0, _chunkPA6YD3HLcjs.PaymentDeclinedError)(_nullishCoalesce(plan.fundingHint, () => ( "No rail is settleable for this payment.")));
2282
2440
  }
2283
2441
  accept = plan.best.accept;
2284
2442
  quote = plan.best.quote;
@@ -2302,7 +2460,7 @@ var PipRailClient = (_class2 = class {
2302
2460
  async resolveChallenge(url, response, schemes) {
2303
2461
  const challenge = await parseChallenge(response);
2304
2462
  if (!challenge) {
2305
- throw new (0, _chunkFTKVCP6Kcjs.InvalidEnvelopeError)(
2463
+ throw new (0, _chunkPA6YD3HLcjs.InvalidEnvelopeError)(
2306
2464
  "402 response did not include a parseable x402 challenge."
2307
2465
  );
2308
2466
  }
@@ -2313,7 +2471,7 @@ var PipRailClient = (_class2 = class {
2313
2471
  (a) => a.scheme === "exact" && net.supports(a.network)
2314
2472
  );
2315
2473
  if (schemes.includes("exact") && exactOnNet && typeof net.payExact !== "function") {
2316
- throw new (0, _chunkFTKVCP6Kcjs.UnsupportedSchemeError)(
2474
+ throw new (0, _chunkPA6YD3HLcjs.UnsupportedSchemeError)(
2317
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.`
2318
2476
  );
2319
2477
  }
@@ -2322,13 +2480,13 @@ var PipRailClient = (_class2 = class {
2322
2480
  (a) => a.scheme === "exact" && net.supports(a.network) && net.describeAsset(a.asset) != null
2323
2481
  );
2324
2482
  if (payable) {
2325
- throw new (0, _chunkFTKVCP6Kcjs.NoCompatibleAcceptError)(
2483
+ throw new (0, _chunkPA6YD3HLcjs.NoCompatibleAcceptError)(
2326
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).`
2327
2485
  );
2328
2486
  }
2329
2487
  }
2330
2488
  const networks = [...new Set(challenge.accepts.map((a) => a.network))].join(", ");
2331
- throw new (0, _chunkFTKVCP6Kcjs.NoCompatibleAcceptError)(
2489
+ throw new (0, _chunkPA6YD3HLcjs.NoCompatibleAcceptError)(
2332
2490
  `No accepts[] entry payable by this client on ${net.network} (schemes: ${schemes.join(", ")}; challenge offered: ${networks || "none"}).`
2333
2491
  );
2334
2492
  }
@@ -2369,6 +2527,7 @@ var PipRailClient = (_class2 = class {
2369
2527
  * net/wallet. Shared by `planPayment` (read-only) and `fetch`'s autoRoute. */
2370
2528
  async planFromChallenge(net, wallet, challenge, url, schemes) {
2371
2529
  const chainLabel = typeof this.opts.chain === "string" ? this.opts.chain : net.network;
2530
+ const session = this.sessionView();
2372
2531
  const candidates = this.gatherCandidates(net, challenge, schemes);
2373
2532
  if (candidates.length === 0) {
2374
2533
  const offered = [...new Set(challenge.accepts.map((a) => a.network))].join(", ") || "none";
@@ -2379,7 +2538,8 @@ var PipRailClient = (_class2 = class {
2379
2538
  payable: false,
2380
2539
  best: null,
2381
2540
  options: [],
2382
- 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 } : {}
2383
2543
  };
2384
2544
  }
2385
2545
  const analysed = await Promise.all(
@@ -2397,7 +2557,8 @@ var PipRailClient = (_class2 = class {
2397
2557
  payable: best !== null,
2398
2558
  best,
2399
2559
  options,
2400
- fundingHint: best ? null : buildFundingHint(options, chainLabel)
2560
+ fundingHint: best ? null : buildFundingHint(options, chainLabel),
2561
+ ...session ? { session } : {}
2401
2562
  };
2402
2563
  }
2403
2564
  /** Analyse ONE rail against the wallet's holdings — quote (existing) + gas
@@ -2414,7 +2575,11 @@ var PipRailClient = (_class2 = class {
2414
2575
  const blockers = [];
2415
2576
  const warnings = [];
2416
2577
  const shortfall = {};
2417
- 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
+ }
2418
2583
  if (quote.symbolMismatch) warnings.push("SYMBOL_MISMATCH");
2419
2584
  if (!isExact && cost.basis === "heuristic") warnings.push("GAS_HEURISTIC");
2420
2585
  const tokenKnown = bal.token != null;
@@ -2423,21 +2588,21 @@ var PipRailClient = (_class2 = class {
2423
2588
  if (isExact) {
2424
2589
  if (tokenKnown && bal.token < amount) {
2425
2590
  blockers.push("INSUFFICIENT_TOKEN");
2426
- shortfall.token = _chunkFTKVCP6Kcjs.formatUnits.call(void 0, amount - bal.token, quote.decimals);
2591
+ shortfall.token = _chunkPA6YD3HLcjs.formatUnits.call(void 0, amount - bal.token, quote.decimals);
2427
2592
  }
2428
2593
  } else if (isNative) {
2429
2594
  if (nativeKnown && bal.native < amount + fee) {
2430
2595
  blockers.push("INSUFFICIENT_TOKEN");
2431
- shortfall.token = _chunkFTKVCP6Kcjs.formatUnits.call(void 0, amount + fee - bal.native, quote.decimals);
2596
+ shortfall.token = _chunkPA6YD3HLcjs.formatUnits.call(void 0, amount + fee - bal.native, quote.decimals);
2432
2597
  }
2433
2598
  } else {
2434
2599
  if (tokenKnown && bal.token < amount) {
2435
2600
  blockers.push("INSUFFICIENT_TOKEN");
2436
- shortfall.token = _chunkFTKVCP6Kcjs.formatUnits.call(void 0, amount - bal.token, quote.decimals);
2601
+ shortfall.token = _chunkPA6YD3HLcjs.formatUnits.call(void 0, amount - bal.token, quote.decimals);
2437
2602
  }
2438
2603
  if (nativeKnown && bal.native < fee) {
2439
2604
  blockers.push("INSUFFICIENT_GAS");
2440
- shortfall.native = _chunkFTKVCP6Kcjs.formatUnits.call(void 0, fee - bal.native, cost.feeDecimals);
2605
+ shortfall.native = _chunkPA6YD3HLcjs.formatUnits.call(void 0, fee - bal.native, cost.feeDecimals);
2441
2606
  } else if (nativeKnown && fee > 0n && bal.native < fee * 3n / 2n) {
2442
2607
  warnings.push("THIN_GAS_MARGIN");
2443
2608
  }
@@ -2462,8 +2627,8 @@ var PipRailClient = (_class2 = class {
2462
2627
  blockers,
2463
2628
  warnings,
2464
2629
  balance: {
2465
- token: bal.token != null ? _chunkFTKVCP6Kcjs.formatUnits.call(void 0, bal.token, quote.decimals) : null,
2466
- native: bal.native != null ? _chunkFTKVCP6Kcjs.formatUnits.call(void 0, bal.native, cost.feeDecimals) : null
2630
+ token: bal.token != null ? _chunkPA6YD3HLcjs.formatUnits.call(void 0, bal.token, quote.decimals) : null,
2631
+ native: bal.native != null ? _chunkPA6YD3HLcjs.formatUnits.call(void 0, bal.native, cost.feeDecimals) : null
2467
2632
  },
2468
2633
  need: { token: quote.amountFormatted, native: cost.feeFormatted },
2469
2634
  ...shortfall.token || shortfall.native ? { shortfall } : {},
@@ -2474,20 +2639,20 @@ var PipRailClient = (_class2 = class {
2474
2639
  * driver's describeAsset) + the policy verdict + a symbol-mismatch flag. */
2475
2640
  buildQuote(net, accept, url, description) {
2476
2641
  if (!/^\d+$/.test(accept.amount)) {
2477
- throw new (0, _chunkFTKVCP6Kcjs.InvalidEnvelopeError)(
2642
+ throw new (0, _chunkPA6YD3HLcjs.InvalidEnvelopeError)(
2478
2643
  `challenge amount "${accept.amount}" is not a base-unit integer.`
2479
2644
  );
2480
2645
  }
2481
2646
  const amountBase = BigInt(accept.amount);
2482
2647
  const described = net.describeAsset(accept.asset);
2483
- const decimals = _nullishCoalesce(_optionalChain([described, 'optionalAccess', _36 => _36.decimals]), () => ( accept.extra.decimals));
2648
+ const decimals = _nullishCoalesce(_optionalChain([described, 'optionalAccess', _41 => _41.decimals]), () => ( accept.extra.decimals));
2484
2649
  if (decimals === void 0) {
2485
- throw new (0, _chunkFTKVCP6Kcjs.InvalidEnvelopeError)(
2650
+ throw new (0, _chunkPA6YD3HLcjs.InvalidEnvelopeError)(
2486
2651
  `challenge for ${accept.asset} on ${accept.network} states no decimals and the SDK doesn't recognise the token \u2014 refusing to price it.`
2487
2652
  );
2488
2653
  }
2489
- const symbol = _nullishCoalesce(_optionalChain([described, 'optionalAccess', _37 => _37.symbol]), () => ( accept.extra.symbol));
2490
- const amountFormatted = _chunkFTKVCP6Kcjs.formatUnits.call(void 0, amountBase, decimals);
2654
+ const symbol = _nullishCoalesce(_optionalChain([described, 'optionalAccess', _42 => _42.symbol]), () => ( accept.extra.symbol));
2655
+ const amountFormatted = _chunkPA6YD3HLcjs.formatUnits.call(void 0, amountBase, decimals);
2491
2656
  const intent = {
2492
2657
  host: hostOf2(url),
2493
2658
  chain: this.opts.chain,
@@ -2498,10 +2663,25 @@ var PipRailClient = (_class2 = class {
2498
2663
  symbol,
2499
2664
  recognized: described != null
2500
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;
2501
2680
  const decision = evaluatePolicy(
2502
2681
  intent,
2503
2682
  this.opts.policy,
2504
- this.ledger.totalFor(accept.network, accept.asset)
2683
+ this.ledger.totalFor(accept.network, accept.asset),
2684
+ ctx
2505
2685
  );
2506
2686
  const serverSymbol = accept.extra.symbol;
2507
2687
  const symbolMismatch = intent.recognized && !!serverSymbol && !!symbol && serverSymbol.toUpperCase() !== symbol.toUpperCase();
@@ -2520,15 +2700,19 @@ var PipRailClient = (_class2 = class {
2520
2700
  recognized: intent.recognized,
2521
2701
  symbolMismatch,
2522
2702
  withinPolicy: decision.allowed,
2523
- ...decision.reason ? { policyReason: decision.reason } : {}
2703
+ ...decision.reason ? { policyReason: decision.reason } : {},
2704
+ ...decision.code ? { policyCode: decision.code } : {}
2524
2705
  };
2525
2706
  }
2526
2707
  /** Enforce the spend policy and the onBeforePay hook — both refuse by
2527
- * 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. */
2528
2711
  async authorize(quote) {
2529
2712
  if (!quote.withinPolicy) {
2530
- throw new (0, _chunkFTKVCP6Kcjs.PaymentDeclinedError)(
2531
- `Payment refused by policy: ${_nullishCoalesce(quote.policyReason, () => ( "not allowed"))}`
2713
+ throw new (0, _chunkPA6YD3HLcjs.PaymentDeclinedError)(
2714
+ `Payment refused by policy: ${_nullishCoalesce(quote.policyReason, () => ( "not allowed"))}`,
2715
+ { reasonCode: reasonCodeForPolicy(quote.policyCode) }
2532
2716
  );
2533
2717
  }
2534
2718
  const hook = this.opts.onBeforePay;
@@ -2537,13 +2721,15 @@ var PipRailClient = (_class2 = class {
2537
2721
  try {
2538
2722
  approved = await hook(quote);
2539
2723
  } catch (err) {
2540
- throw new (0, _chunkFTKVCP6Kcjs.PaymentDeclinedError)("onBeforePay threw \u2014 refusing to pay.", {
2541
- cause: err
2724
+ throw new (0, _chunkPA6YD3HLcjs.PaymentDeclinedError)("onBeforePay threw \u2014 refusing to pay.", {
2725
+ cause: err,
2726
+ reasonCode: "APPROVAL"
2542
2727
  });
2543
2728
  }
2544
2729
  if (!approved) {
2545
- throw new (0, _chunkFTKVCP6Kcjs.PaymentDeclinedError)(
2546
- `onBeforePay declined ${quote.amountFormatted} ${_nullishCoalesce(quote.symbol, () => ( ""))}`.trimEnd() + ` on ${quote.network}.`
2730
+ throw new (0, _chunkPA6YD3HLcjs.PaymentDeclinedError)(
2731
+ `onBeforePay declined ${quote.amountFormatted} ${_nullishCoalesce(quote.symbol, () => ( ""))}`.trimEnd() + ` on ${quote.network}.`,
2732
+ { reasonCode: "APPROVAL" }
2547
2733
  );
2548
2734
  }
2549
2735
  }
@@ -2566,7 +2752,7 @@ var PipRailClient = (_class2 = class {
2566
2752
  }
2567
2753
  async payAndConfirm(net, wallet, accept) {
2568
2754
  if (!net.supports(accept.network)) {
2569
- throw new (0, _chunkFTKVCP6Kcjs.WrongChainError)(
2755
+ throw new (0, _chunkPA6YD3HLcjs.WrongChainError)(
2570
2756
  `Challenge expects ${accept.network} but client is on ${net.network}.`
2571
2757
  );
2572
2758
  }
@@ -2595,7 +2781,7 @@ var PipRailClient = (_class2 = class {
2595
2781
  accepted: accept,
2596
2782
  payload: { nonce: accept.extra.nonce, txHash: ref }
2597
2783
  };
2598
- const headers = new Headers(_optionalChain([originalInit, 'optionalAccess', _38 => _38.headers]));
2784
+ const headers = new Headers(_optionalChain([originalInit, 'optionalAccess', _43 => _43.headers]));
2599
2785
  headers.set(HEADER_SIGNATURE, buildSignatureHeader(signature));
2600
2786
  let lastResponse = null;
2601
2787
  let lastReason = null;
@@ -2610,7 +2796,7 @@ var PipRailClient = (_class2 = class {
2610
2796
  () => timeoutController.abort(),
2611
2797
  this.retryTimeoutMs
2612
2798
  );
2613
- const signal = _optionalChain([originalInit, 'optionalAccess', _39 => _39.signal]) && typeof AbortSignal.any === "function" ? AbortSignal.any([timeoutController.signal, originalInit.signal]) : timeoutController.signal;
2799
+ const signal = _optionalChain([originalInit, 'optionalAccess', _44 => _44.signal]) && typeof AbortSignal.any === "function" ? AbortSignal.any([timeoutController.signal, originalInit.signal]) : timeoutController.signal;
2614
2800
  try {
2615
2801
  lastResponse = await fetch(url, {
2616
2802
  ..._nullishCoalesce(originalInit, () => ( {})),
@@ -2619,7 +2805,7 @@ var PipRailClient = (_class2 = class {
2619
2805
  });
2620
2806
  } catch (err) {
2621
2807
  if (timeoutController.signal.aborted) {
2622
- throw new (0, _chunkFTKVCP6Kcjs.PaymentTimeoutError)(
2808
+ throw new (0, _chunkPA6YD3HLcjs.PaymentTimeoutError)(
2623
2809
  `Server did not respond within ${this.retryTimeoutMs}ms after broadcasting payment ${ref}. Re-verify or re-submit ref=${ref} \u2014 do NOT re-pay.`,
2624
2810
  { cause: err, ref }
2625
2811
  );
@@ -2641,7 +2827,7 @@ var PipRailClient = (_class2 = class {
2641
2827
  kind: "payment-failed",
2642
2828
  reason: `server returned 402 after broadcasting payment ${ref}${unconfirmedNote} (${why})`
2643
2829
  });
2644
- throw new (0, _chunkFTKVCP6Kcjs.MaxRetriesExceededError)(
2830
+ throw new (0, _chunkPA6YD3HLcjs.MaxRetriesExceededError)(
2645
2831
  `Server still returned 402 after ${attempts} attempt(s) with on-chain proof ref=${ref}${unconfirmedNote}. Last server rejection: ${why}. Re-verify or re-submit ref=${ref} before retrying \u2014 never re-pay (it would double-spend).`,
2646
2832
  { ref }
2647
2833
  );
@@ -2665,17 +2851,17 @@ var PipRailClient = (_class2 = class {
2665
2851
  */
2666
2852
  async payExactRail(net, wallet, accept, url, init, quote) {
2667
2853
  if (!net.payExact) {
2668
- throw new (0, _chunkFTKVCP6Kcjs.UnsupportedSchemeError)(
2854
+ throw new (0, _chunkPA6YD3HLcjs.UnsupportedSchemeError)(
2669
2855
  `the ${net.family} family can't pay a standard 'exact' rail (EVM + EIP-3009 only).`
2670
2856
  );
2671
2857
  }
2672
- throwIfAborted(_optionalChain([init, 'optionalAccess', _40 => _40.signal]));
2858
+ throwIfAborted(_optionalChain([init, 'optionalAccess', _45 => _45.signal]));
2673
2859
  const { payload, accepted, payerFrom, nonce } = await net.payExact(wallet, accept);
2674
- const headers = new Headers(_optionalChain([init, 'optionalAccess', _41 => _41.headers]));
2860
+ const headers = new Headers(_optionalChain([init, 'optionalAccess', _46 => _46.headers]));
2675
2861
  headers.set(HEADER_SIGNATURE, buildExactSignatureHeader({ accepted, payload }));
2676
2862
  const rejectDefinitive = (why2) => {
2677
2863
  this.safeEmit({ kind: "payment-failed", reason: `exact: facilitator rejected nonce=${nonce} (${why2})` });
2678
- throw new (0, _chunkFTKVCP6Kcjs.MaxRetriesExceededError)(
2864
+ throw new (0, _chunkPA6YD3HLcjs.MaxRetriesExceededError)(
2679
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}.`,
2680
2866
  { ref: nonce }
2681
2867
  );
@@ -2688,17 +2874,17 @@ var PipRailClient = (_class2 = class {
2688
2874
  if (Date.now() >= deadline) break;
2689
2875
  await new Promise((r) => setTimeout(r, Math.min(2e3, 400 * 2 ** (attempt - 1))));
2690
2876
  }
2691
- throwIfAborted(_optionalChain([init, 'optionalAccess', _42 => _42.signal]));
2877
+ throwIfAborted(_optionalChain([init, 'optionalAccess', _47 => _47.signal]));
2692
2878
  const budget = Math.min(this.retryTimeoutMs, deadline - Date.now());
2693
2879
  if (budget <= 0) break;
2694
2880
  const timeoutController = new AbortController();
2695
2881
  const timeoutId = setTimeout(() => timeoutController.abort(), budget);
2696
- const signal = _optionalChain([init, 'optionalAccess', _43 => _43.signal]) && typeof AbortSignal.any === "function" ? AbortSignal.any([timeoutController.signal, init.signal]) : timeoutController.signal;
2882
+ const signal = _optionalChain([init, 'optionalAccess', _48 => _48.signal]) && typeof AbortSignal.any === "function" ? AbortSignal.any([timeoutController.signal, init.signal]) : timeoutController.signal;
2697
2883
  let response;
2698
2884
  try {
2699
2885
  response = await fetch(url, { ..._nullishCoalesce(init, () => ( {})), headers, signal });
2700
2886
  } catch (err) {
2701
- throw new (0, _chunkFTKVCP6Kcjs.PaymentTimeoutError)(
2887
+ throw new (0, _chunkPA6YD3HLcjs.PaymentTimeoutError)(
2702
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.`,
2703
2889
  { cause: err, ref: nonce }
2704
2890
  );
@@ -2714,7 +2900,7 @@ var PipRailClient = (_class2 = class {
2714
2900
  if (response.ok && !(settle && settle.success === false)) {
2715
2901
  const receipt = parseReceipt(response);
2716
2902
  this.safeEmit({ kind: "payment-settled", receipt, ...settle ? { settle } : {} });
2717
- const ref = _optionalChain([settle, 'optionalAccess', _44 => _44.transaction]) || _optionalChain([receipt, 'optionalAccess', _45 => _45.transaction]) || `eip3009-nonce:${nonce}`;
2903
+ const ref = _optionalChain([settle, 'optionalAccess', _49 => _49.transaction]) || _optionalChain([receipt, 'optionalAccess', _50 => _50.transaction]) || `eip3009-nonce:${nonce}`;
2718
2904
  this.recordSpend(quote, ref);
2719
2905
  return response;
2720
2906
  }
@@ -2731,14 +2917,14 @@ var PipRailClient = (_class2 = class {
2731
2917
  kind: "payment-failed",
2732
2918
  reason: `exact: 402 after submitting authorization nonce=${nonce} (${why})`
2733
2919
  });
2734
- throw new (0, _chunkFTKVCP6Kcjs.MaxRetriesExceededError)(
2920
+ throw new (0, _chunkPA6YD3HLcjs.MaxRetriesExceededError)(
2735
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}.`,
2736
2922
  { ref: nonce }
2737
2923
  );
2738
2924
  }
2739
2925
  }, _class2);
2740
2926
  function throwIfAborted(signal) {
2741
- if (_optionalChain([signal, 'optionalAccess', _46 => _46.aborted])) {
2927
+ if (_optionalChain([signal, 'optionalAccess', _51 => _51.aborted])) {
2742
2928
  throw _nullishCoalesce(signal.reason, () => ( new DOMException("This operation was aborted.", "AbortError")));
2743
2929
  }
2744
2930
  }
@@ -2771,6 +2957,9 @@ function buildFundingHint(options, chainLabel) {
2771
2957
  if (target.blockers.includes("RECIPIENT_NOT_READY")) {
2772
2958
  return `Recipient ${shortAddr(target.accept.payTo)} can't receive on ${chainLabel} yet \u2014 ${_nullishCoalesce(target.recipient.fix, () => ( "recipient not ready"))}.`;
2773
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
+ }
2774
2963
  if (target.blockers.includes("OUTSIDE_POLICY")) {
2775
2964
  return `Refused by spend policy: ${_nullishCoalesce(target.quote.policyReason, () => ( "not allowed"))}.`;
2776
2965
  }
@@ -2778,10 +2967,10 @@ function buildFundingHint(options, chainLabel) {
2778
2967
  return `Couldn't fully read your wallet on ${chainLabel} (RPC throttled) \u2014 retry; you may already be able to pay ${target.quote.amountFormatted} ${sym}.`;
2779
2968
  }
2780
2969
  const parts = [];
2781
- if (target.blockers.includes("INSUFFICIENT_TOKEN") && _optionalChain([target, 'access', _47 => _47.shortfall, 'optionalAccess', _48 => _48.token])) {
2970
+ if (target.blockers.includes("INSUFFICIENT_TOKEN") && _optionalChain([target, 'access', _52 => _52.shortfall, 'optionalAccess', _53 => _53.token])) {
2782
2971
  parts.push(`top up ${target.shortfall.token} ${sym}`);
2783
2972
  }
2784
- if (target.blockers.includes("INSUFFICIENT_GAS") && _optionalChain([target, 'access', _49 => _49.shortfall, 'optionalAccess', _50 => _50.native])) {
2973
+ if (target.blockers.includes("INSUFFICIENT_GAS") && _optionalChain([target, 'access', _54 => _54.shortfall, 'optionalAccess', _55 => _55.native])) {
2785
2974
  parts.push(`add ~${target.shortfall.native} ${target.cost.feeSymbol} for gas`);
2786
2975
  }
2787
2976
  return parts.length ? `Can't settle on ${chainLabel}: ${parts.join(" and ")} (to pay ${target.quote.amountFormatted} ${sym}).` : `Can't settle on ${chainLabel} for ${target.quote.amountFormatted} ${sym}.`;
@@ -2795,7 +2984,7 @@ async function planAcross(clients, url, init) {
2795
2984
  const status = best ? "ready" : options.some((o) => o.state === "unknown") ? "unknown" : "blocked";
2796
2985
  return {
2797
2986
  url,
2798
- network: _nullishCoalesce(_optionalChain([best, 'optionalAccess', _51 => _51.accept, 'access', _52 => _52.network]), () => ( live[0].network)),
2987
+ network: _nullishCoalesce(_optionalChain([best, 'optionalAccess', _56 => _56.accept, 'access', _57 => _57.network]), () => ( live[0].network)),
2799
2988
  status,
2800
2989
  payable: best !== null,
2801
2990
  best,
@@ -2808,6 +2997,20 @@ function railOnNetwork(rail, matches) {
2808
2997
  const n = normalizeNetwork(rail.network);
2809
2998
  return !n.includes(":") || matches(n);
2810
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
+ }
2811
3014
  function hostOf2(url) {
2812
3015
  try {
2813
3016
  return new URL(url).hostname;
@@ -2828,8 +3031,8 @@ function isReplayableBodyInit(value) {
2828
3031
  async function readInvalidReason(response) {
2829
3032
  try {
2830
3033
  const body = await response.clone().json();
2831
- const ext = _optionalChain([body, 'optionalAccess', _53 => _53.extensions]);
2832
- const piprail = _optionalChain([ext, 'optionalAccess', _54 => _54.piprail]);
3034
+ const ext = _optionalChain([body, 'optionalAccess', _58 => _58.extensions]);
3035
+ const piprail = _optionalChain([ext, 'optionalAccess', _59 => _59.piprail]);
2833
3036
  if (piprail && typeof piprail.code === "string") {
2834
3037
  return {
2835
3038
  error: piprail.code,
@@ -2851,11 +3054,115 @@ async function readInvalidReason(response) {
2851
3054
  } catch (e24) {
2852
3055
  }
2853
3056
  const settle = parseSettleResponse(response);
2854
- if (_optionalChain([settle, 'optionalAccess', _55 => _55.errorReason])) return { error: settle.errorReason, detail: "" };
3057
+ if (_optionalChain([settle, 'optionalAccess', _60 => _60.errorReason])) return { error: settle.errorReason, detail: "" };
2855
3058
  return null;
2856
3059
  }
2857
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} ${_nullishCoalesce(q.symbol, () => ( q.asset))} on ${plan.best.accept.network} (gas ~${c.feeFormatted} ${c.feeSymbol}).${note}`;
3070
+ }
3071
+ return `NOT payable: ${_nullishCoalesce(plan.fundingHint, () => ( `no settleable rail on ${plan.network}`))}`;
3072
+ }
3073
+ function explainDecline(err) {
3074
+ if (!(err instanceof _chunkPA6YD3HLcjs.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} ${_nullishCoalesce(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
+
2858
3164
  // src/agent.ts
3165
+ var OPEN_OBJECT = { type: "object", additionalProperties: true };
2859
3166
  async function readBody(res) {
2860
3167
  const text = await res.text();
2861
3168
  if (!text) return null;
@@ -2928,6 +3235,7 @@ function paymentTools(client) {
2928
3235
  required: ["url"],
2929
3236
  additionalProperties: false
2930
3237
  },
3238
+ outputSchema: OPEN_OBJECT,
2931
3239
  invoke: async (args) => {
2932
3240
  const quote = await client.quote(String(args.url));
2933
3241
  return quote ? { gated: true, ...quote } : { gated: false, url: String(args.url) };
@@ -2951,6 +3259,7 @@ function paymentTools(client) {
2951
3259
  required: ["url"],
2952
3260
  additionalProperties: false
2953
3261
  },
3262
+ outputSchema: OPEN_OBJECT,
2954
3263
  invoke: async (args) => {
2955
3264
  const plan = await client.planPayment(String(args.url));
2956
3265
  if (plan == null) return { gated: false, url: String(args.url) };
@@ -2959,6 +3268,8 @@ function paymentTools(client) {
2959
3268
  payable: plan.payable,
2960
3269
  status: plan.status,
2961
3270
  fundingHint: plan.fundingHint,
3271
+ // One model-readable line distilling the whole plan.
3272
+ summary: summarizePlan(plan),
2962
3273
  best: plan.best ? {
2963
3274
  network: plan.best.accept.network,
2964
3275
  symbol: plan.best.quote.symbol,
@@ -2974,7 +3285,9 @@ function paymentTools(client) {
2974
3285
  blockers: o.blockers,
2975
3286
  warnings: o.warnings,
2976
3287
  recipientReady: o.recipient.ready
2977
- }))
3288
+ })),
3289
+ // The session's time leash, present only when a time policy is configured.
3290
+ ...plan.session ? { session: plan.session } : {}
2978
3291
  };
2979
3292
  }
2980
3293
  },
@@ -3032,8 +3345,20 @@ function paymentTools(client) {
3032
3345
  receipt: parseReceipt(res)
3033
3346
  };
3034
3347
  } catch (err) {
3035
- if (err instanceof _chunkFTKVCP6Kcjs.PaymentDeclinedError) {
3036
- return { declined: true, reason: err.message };
3348
+ if (err instanceof _chunkPA6YD3HLcjs.PipRailError) {
3349
+ const out = {
3350
+ ok: false,
3351
+ code: err.code,
3352
+ reason: err.message,
3353
+ explain: explainDecline(err)
3354
+ };
3355
+ if (err instanceof _chunkPA6YD3HLcjs.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;
3037
3362
  }
3038
3363
  throw err;
3039
3364
  }
@@ -3071,10 +3396,57 @@ function paymentTools(client) {
3071
3396
  const outcomes = await client.register(String(args.url), opts);
3072
3397
  return { outcomes };
3073
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 })
3074
3433
  }
3075
3434
  ];
3076
3435
  }
3077
3436
 
3437
+ // src/classify.ts
3438
+ function classifyChallenge(challenge, opts) {
3439
+ const accepts = _nullishCoalesce(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
+
3078
3450
  // src/discovery.ts
3079
3451
  var GENERATOR = "@piprail/sdk \xB7 https://piprail.com";
3080
3452
  function buildBazaarExtension(descriptor = {}) {
@@ -3196,13 +3568,13 @@ async function settleViaFacilitator(input) {
3196
3568
  try {
3197
3569
  verify = await post(`${base2}/verify`, body, auth);
3198
3570
  } catch (err) {
3199
- throw new (0, _chunkFTKVCP6Kcjs.SettlementError)(
3571
+ throw new (0, _chunkPA6YD3HLcjs.SettlementError)(
3200
3572
  `exact settle (facilitator ${base2}): /verify request failed (${err instanceof Error ? err.message : String(err)}).`,
3201
3573
  { cause: err }
3202
3574
  );
3203
3575
  }
3204
3576
  if (verify.status !== 200) {
3205
- throw new (0, _chunkFTKVCP6Kcjs.SettlementError)(
3577
+ throw new (0, _chunkPA6YD3HLcjs.SettlementError)(
3206
3578
  `exact settle (facilitator ${base2}): /verify returned HTTP ${verify.status} (transport/auth error).`
3207
3579
  );
3208
3580
  }
@@ -3218,13 +3590,13 @@ async function settleViaFacilitator(input) {
3218
3590
  try {
3219
3591
  settle = await post(`${base2}/settle`, body, auth);
3220
3592
  } catch (err) {
3221
- throw new (0, _chunkFTKVCP6Kcjs.SettlementError)(
3593
+ throw new (0, _chunkPA6YD3HLcjs.SettlementError)(
3222
3594
  `exact settle (facilitator ${base2}): /settle request failed (${err instanceof Error ? err.message : String(err)}).`,
3223
3595
  { cause: err }
3224
3596
  );
3225
3597
  }
3226
3598
  if (settle.status !== 200) {
3227
- throw new (0, _chunkFTKVCP6Kcjs.SettlementError)(
3599
+ throw new (0, _chunkPA6YD3HLcjs.SettlementError)(
3228
3600
  `exact settle (facilitator ${base2}): /settle returned HTTP ${settle.status} (transport/auth error).`
3229
3601
  );
3230
3602
  }
@@ -3290,7 +3662,7 @@ function createPaymentGate(options) {
3290
3662
  }
3291
3663
  net.assertValidPayTo(payTo);
3292
3664
  const { asset, decimals, symbol } = net.resolveToken(a.token);
3293
- const amountBase = _chunkFTKVCP6Kcjs.parseUnits.call(void 0, a.amount, decimals);
3665
+ const amountBase = _chunkPA6YD3HLcjs.parseUnits.call(void 0, a.amount, decimals);
3294
3666
  const spec = { net, asset, decimals, symbol, amountBase, amountFormatted: a.amount, payTo };
3295
3667
  if (options.exact) spec.exact = await resolveExactRail(net, asset);
3296
3668
  return spec;
@@ -3405,7 +3777,7 @@ function createPaymentGate(options) {
3405
3777
  const specs = await ready();
3406
3778
  const nonce = genNonce();
3407
3779
  const bazaar = options.discovery ? { bazaar: buildBazaarExtension(options.discovery === true ? {} : options.discovery) } : void 0;
3408
- const extensions = { ...bazaar, ..._optionalChain([opts, 'optionalAccess', _56 => _56.extensions]) };
3780
+ const extensions = { ...bazaar, ..._optionalChain([opts, 'optionalAccess', _61 => _61.extensions]) };
3409
3781
  const challenge2 = {
3410
3782
  x402Version: 2,
3411
3783
  resource: {
@@ -3413,7 +3785,7 @@ function createPaymentGate(options) {
3413
3785
  ...options.description ? { description: options.description } : {}
3414
3786
  },
3415
3787
  accepts: buildAccepts(specs, nonce),
3416
- ..._optionalChain([opts, 'optionalAccess', _57 => _57.error]) ? { error: opts.error } : {},
3788
+ ..._optionalChain([opts, 'optionalAccess', _62 => _62.error]) ? { error: opts.error } : {},
3417
3789
  ...Object.keys(extensions).length > 0 ? { extensions } : {}
3418
3790
  };
3419
3791
  return { challenge: challenge2, requiredHeader: buildChallengeHeader(challenge2) };
@@ -3572,7 +3944,7 @@ function requirePayment(options) {
3572
3944
  try {
3573
3945
  result = await gate.verify(_nullishCoalesce(req.headers[HEADER_SIGNATURE], () => ( req.headers[HEADER_SIGNATURE_V1])));
3574
3946
  } catch (err) {
3575
- if (err instanceof _chunkFTKVCP6Kcjs.SettlementError) {
3947
+ if (err instanceof _chunkPA6YD3HLcjs.SettlementError) {
3576
3948
  res.status(502);
3577
3949
  res.json({ x402Version: 2, error: "settlement_failed", detail: err.message });
3578
3950
  return;
@@ -3669,4 +4041,10 @@ function normaliseHeader(value) {
3669
4041
 
3670
4042
 
3671
4043
 
3672
- exports.CHAINS = CHAINS; exports.ConfirmationTimeoutError = _chunkFTKVCP6Kcjs.ConfirmationTimeoutError; exports.DIRECTORY_INFO = DIRECTORY_INFO; exports.EIP3009_TYPES = EIP3009_TYPES; exports.EXACT_NETWORK_SLUGS = EXACT_NETWORK_SLUGS; exports.GENERATOR = GENERATOR; exports.HEADER_REQUIRED = HEADER_REQUIRED; exports.HEADER_RESPONSE = HEADER_RESPONSE; exports.HEADER_RESPONSE_V1 = HEADER_RESPONSE_V1; exports.HEADER_SIGNATURE = HEADER_SIGNATURE; exports.HEADER_SIGNATURE_V1 = HEADER_SIGNATURE_V1; exports.InsufficientFundsError = _chunkFTKVCP6Kcjs.InsufficientFundsError; exports.InvalidEnvelopeError = _chunkFTKVCP6Kcjs.InvalidEnvelopeError; exports.MaxRetriesExceededError = _chunkFTKVCP6Kcjs.MaxRetriesExceededError; exports.MissingDriverError = _chunkFTKVCP6Kcjs.MissingDriverError; exports.NoCompatibleAcceptError = _chunkFTKVCP6Kcjs.NoCompatibleAcceptError; exports.NonReplayableBodyError = _chunkFTKVCP6Kcjs.NonReplayableBodyError; exports.PaymentDeclinedError = _chunkFTKVCP6Kcjs.PaymentDeclinedError; exports.PaymentTimeoutError = _chunkFTKVCP6Kcjs.PaymentTimeoutError; exports.PipRailClient = PipRailClient; exports.PipRailError = _chunkFTKVCP6Kcjs.PipRailError; exports.RecipientNotReadyError = _chunkFTKVCP6Kcjs.RecipientNotReadyError; exports.SettlementError = _chunkFTKVCP6Kcjs.SettlementError; exports.UnknownTokenError = _chunkFTKVCP6Kcjs.UnknownTokenError; exports.UnsupportedNetworkError = _chunkFTKVCP6Kcjs.UnsupportedNetworkError; exports.UnsupportedSchemeError = _chunkFTKVCP6Kcjs.UnsupportedSchemeError; exports.WrongChainError = _chunkFTKVCP6Kcjs.WrongChainError; exports.WrongFamilyError = _chunkFTKVCP6Kcjs.WrongFamilyError; exports.buildBazaarExtension = buildBazaarExtension; exports.buildChallengeHeader = buildChallengeHeader; exports.buildExactAuthorization = buildExactAuthorization; exports.buildExactSignatureHeader = buildExactSignatureHeader; exports.buildOpenApi = buildOpenApi; exports.buildReceiptHeader = buildReceiptHeader; exports.buildSignatureHeader = buildSignatureHeader; exports.buildWellKnownX402 = buildWellKnownX402; exports.buildX402DnsTxt = buildX402DnsTxt; exports.chainIdForExactNetwork = chainIdForExactNetwork; exports.claim402IndexDomain = claim402IndexDomain; exports.createPaymentGate = createPaymentGate; exports.decorateOutcome = decorateOutcome; exports.eip3009Abi = eip3009Abi; exports.encodeXPaymentHeader = encodeXPaymentHeader; exports.evaluatePolicy = evaluatePolicy; exports.getDirectoryInfo = getDirectoryInfo; exports.normalizeNetwork = normalizeNetwork; exports.parseChallenge = parseChallenge; exports.parseExactPaymentHeader = parseExactPaymentHeader; exports.parseExactRequirements = parseExactRequirements; exports.parseReceipt = parseReceipt; exports.parseSettleResponse = parseSettleResponse; exports.parseSignatureHeader = parseSignatureHeader; exports.paymentTools = paymentTools; exports.pickAccept = pickAccept; exports.planAcross = planAcross; exports.readExactDomain = readExactDomain; exports.register402Index = register402Index; exports.registerDriver = registerDriver; exports.registerX402Scan = registerX402Scan; exports.requirePayment = requirePayment; exports.resolveChain = resolveChain; exports.searchOpenIndexes = searchOpenIndexes; exports.settleViaFacilitator = settleViaFacilitator; exports.toInsufficientFundsError = _chunkFTKVCP6Kcjs.toInsufficientFundsError; exports.toInvalidBody = toInvalidBody; exports.verify402IndexDomain = verify402IndexDomain;
4044
+
4045
+
4046
+
4047
+
4048
+
4049
+
4050
+ exports.CHAINS = CHAINS; exports.ConfirmationTimeoutError = _chunkPA6YD3HLcjs.ConfirmationTimeoutError; exports.DIRECTORY_INFO = DIRECTORY_INFO; exports.EIP3009_TYPES = EIP3009_TYPES; exports.EXACT_NETWORK_SLUGS = EXACT_NETWORK_SLUGS; exports.GENERATOR = GENERATOR; exports.HEADER_REQUIRED = HEADER_REQUIRED; exports.HEADER_RESPONSE = HEADER_RESPONSE; exports.HEADER_RESPONSE_V1 = HEADER_RESPONSE_V1; exports.HEADER_SIGNATURE = HEADER_SIGNATURE; exports.HEADER_SIGNATURE_V1 = HEADER_SIGNATURE_V1; exports.InsufficientFundsError = _chunkPA6YD3HLcjs.InsufficientFundsError; exports.InvalidEnvelopeError = _chunkPA6YD3HLcjs.InvalidEnvelopeError; exports.MaxRetriesExceededError = _chunkPA6YD3HLcjs.MaxRetriesExceededError; exports.MissingDriverError = _chunkPA6YD3HLcjs.MissingDriverError; exports.NoCompatibleAcceptError = _chunkPA6YD3HLcjs.NoCompatibleAcceptError; exports.NonReplayableBodyError = _chunkPA6YD3HLcjs.NonReplayableBodyError; exports.PIPRAIL_AGENT_GUIDE = PIPRAIL_AGENT_GUIDE; exports.PaymentDeclinedError = _chunkPA6YD3HLcjs.PaymentDeclinedError; exports.PaymentTimeoutError = _chunkPA6YD3HLcjs.PaymentTimeoutError; exports.PipRailClient = PipRailClient; exports.PipRailError = _chunkPA6YD3HLcjs.PipRailError; exports.RecipientNotReadyError = _chunkPA6YD3HLcjs.RecipientNotReadyError; exports.SettlementError = _chunkPA6YD3HLcjs.SettlementError; exports.UnknownTokenError = _chunkPA6YD3HLcjs.UnknownTokenError; exports.UnsupportedNetworkError = _chunkPA6YD3HLcjs.UnsupportedNetworkError; exports.UnsupportedSchemeError = _chunkPA6YD3HLcjs.UnsupportedSchemeError; exports.WrongChainError = _chunkPA6YD3HLcjs.WrongChainError; exports.WrongFamilyError = _chunkPA6YD3HLcjs.WrongFamilyError; exports.agentGuide = agentGuide; exports.buildBazaarExtension = buildBazaarExtension; exports.buildChallengeHeader = buildChallengeHeader; exports.buildExactAuthorization = buildExactAuthorization; exports.buildExactSignatureHeader = buildExactSignatureHeader; exports.buildOpenApi = buildOpenApi; exports.buildReceiptHeader = buildReceiptHeader; exports.buildSignatureHeader = buildSignatureHeader; exports.buildWellKnownX402 = buildWellKnownX402; exports.buildX402DnsTxt = buildX402DnsTxt; exports.chainIdForExactNetwork = chainIdForExactNetwork; exports.claim402IndexDomain = claim402IndexDomain; exports.classifyChallenge = classifyChallenge; exports.createPaymentGate = createPaymentGate; exports.decorateOutcome = decorateOutcome; exports.eip3009Abi = eip3009Abi; exports.encodeXPaymentHeader = encodeXPaymentHeader; exports.evaluatePolicy = evaluatePolicy; exports.explainDecline = explainDecline; exports.formatSpendReport = formatSpendReport; exports.getDirectoryInfo = getDirectoryInfo; exports.normalizeNetwork = normalizeNetwork; exports.parseChallenge = parseChallenge; exports.parseExactPaymentHeader = parseExactPaymentHeader; exports.parseExactRequirements = parseExactRequirements; exports.parseReceipt = parseReceipt; exports.parseSettleResponse = parseSettleResponse; exports.parseSignatureHeader = parseSignatureHeader; exports.paymentTools = paymentTools; exports.pickAccept = pickAccept; exports.planAcross = planAcross; exports.readExactDomain = readExactDomain; exports.register402Index = register402Index; exports.registerDriver = registerDriver; exports.registerX402Scan = registerX402Scan; exports.requirePayment = requirePayment; exports.resolveChain = resolveChain; exports.searchOpenIndexes = searchOpenIndexes; exports.settleViaFacilitator = settleViaFacilitator; exports.summarizePlan = summarizePlan; exports.toInsufficientFundsError = _chunkPA6YD3HLcjs.toInsufficientFundsError; exports.toInvalidBody = toInvalidBody; exports.verify402IndexDomain = verify402IndexDomain;