@aifinpay/agent 2.3.0 → 2.5.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 (54) hide show
  1. package/PAYMENT_RECEIPTS.md +194 -19
  2. package/README.md +58 -27
  3. package/dist/agentHistory.d.ts +9 -0
  4. package/dist/agentHistory.d.ts.map +1 -1
  5. package/dist/agentHistory.js +110 -16
  6. package/dist/agentHistory.js.map +1 -1
  7. package/dist/aifp1.d.ts +124 -17
  8. package/dist/aifp1.d.ts.map +1 -1
  9. package/dist/aifp1.js +580 -82
  10. package/dist/aifp1.js.map +1 -1
  11. package/dist/deploymentErrors.d.ts +5 -0
  12. package/dist/deploymentErrors.d.ts.map +1 -0
  13. package/dist/deploymentErrors.js +8 -0
  14. package/dist/deploymentErrors.js.map +1 -0
  15. package/dist/deploymentResolver.d.ts +2 -3
  16. package/dist/deploymentResolver.d.ts.map +1 -1
  17. package/dist/deploymentResolver.js +2 -6
  18. package/dist/deploymentResolver.js.map +1 -1
  19. package/dist/facilitators/index.d.ts +1 -1
  20. package/dist/facilitators/index.d.ts.map +1 -1
  21. package/dist/generated/solanaV14Deployments.generated.d.ts +1 -0
  22. package/dist/generated/solanaV14Deployments.generated.d.ts.map +1 -1
  23. package/dist/generated/solanaV14Deployments.generated.js +4 -2
  24. package/dist/generated/solanaV14Deployments.generated.js.map +1 -1
  25. package/dist/index.d.ts +10 -4
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +12 -3
  28. package/dist/index.js.map +1 -1
  29. package/dist/paymentChains.d.ts +95 -0
  30. package/dist/paymentChains.d.ts.map +1 -0
  31. package/dist/paymentChains.js +116 -0
  32. package/dist/paymentChains.js.map +1 -0
  33. package/dist/settlement.d.ts +5 -5
  34. package/dist/settlementSolanaV14.d.ts +135 -0
  35. package/dist/settlementSolanaV14.d.ts.map +1 -0
  36. package/dist/settlementSolanaV14.js +733 -0
  37. package/dist/settlementSolanaV14.js.map +1 -0
  38. package/dist/settlementV14.d.ts +9 -2
  39. package/dist/settlementV14.d.ts.map +1 -1
  40. package/dist/settlementV14.js +81 -14
  41. package/dist/settlementV14.js.map +1 -1
  42. package/dist/solanaDeploymentResolver.d.ts +2 -1
  43. package/dist/solanaDeploymentResolver.d.ts.map +1 -1
  44. package/dist/solanaDeploymentResolver.js +1 -32
  45. package/dist/solanaDeploymentResolver.js.map +1 -1
  46. package/dist/spendLedger.d.ts +74 -33
  47. package/dist/spendLedger.d.ts.map +1 -1
  48. package/dist/spendLedger.js +523 -130
  49. package/dist/spendLedger.js.map +1 -1
  50. package/dist/unifiedAgent.d.ts +17 -8
  51. package/dist/unifiedAgent.d.ts.map +1 -1
  52. package/dist/unifiedAgent.js +227 -42
  53. package/dist/unifiedAgent.js.map +1 -1
  54. package/package.json +3 -3
@@ -15,18 +15,21 @@ import bs58 from "bs58";
15
15
  import { createHash, randomBytes } from "node:crypto";
16
16
  import { createPublicClient, createWalletClient, http, keccak256, toHex, toFunctionSelector, formatEther, } from "viem";
17
17
  import { privateKeyToAccount } from "viem/accounts";
18
- import { polygon, base, arbitrum, optimism, bsc, mainnet, unichain } from "viem/chains";
18
+ import { polygon, base, arbitrum, optimism, bsc, mainnet, unichain, avalanche } from "viem/chains";
19
+ import { paymentChain } from "./paymentChains.js";
19
20
  import { botchain, xrplevm, robinhood } from "./chains.js";
20
21
  import { V14_DEPLOYMENTS } from "./generated/v14Deployments.generated.js";
21
22
  import { SPLITTER_ROUTES } from "./generated/splitterRoutes.generated.js";
23
+ import { Keypair, } from "@solana/web3.js";
22
24
  import { AiFinPayError, X402Error } from "./errors.js";
23
25
  import { Agent } from "./agent.js";
24
26
  import { loadLocalWalletIdentity } from "./localIdentity.js";
25
27
  import { SettlementClient, executeSettlementInvoice, } from "./settlement.js";
26
- import { executeV14Settlement } from "./settlementV14.js";
27
- import { getQuota } from "./agentHistory.js";
28
+ import { executeV14Settlement, assertPreparedV14Recovery } from "./settlementV14.js";
29
+ import { solanaV14Rpc, authorizedSolanaV14Inventory, prepareSolanaV14Settlement, executeSolanaV14Settlement, verifyHistoricalSolanaV14Quote, solanaStableMint, SOLANA_GENESIS, } from "./settlementSolanaV14.js";
30
+ import { getAgentHistory, getQuota } from "./agentHistory.js";
28
31
  import { MemorySpendLedger, FileSpendLedger } from "./spendLedger.js";
29
- import { aifp1Fetch, recoverAifp1Payment, Aifp1ReceiptCache, } from "./aifp1.js";
32
+ import { aifp1Fetch, recoverAifp1Payment, Aifp1ReceiptCache, Aifp1QuoteError, } from "./aifp1.js";
30
33
  import { bridgeQuote as crossChainQuote, bridgeExecute as crossChainExecute, bridgeWaitForArrival, USDC_NATIVE, } from "./crossChain.js";
31
34
  /**
32
35
  * The native-token payment block of a bridge 402, whichever key it arrived
@@ -109,10 +112,26 @@ const CHAIN_OBJECTS = {
109
112
  // Legacy v1.2 splitter addresses, verified on-chain 2026-08-01. These are the
110
113
  // pre-v1.3 contracts; v1.3+ addresses live in the generated registry files.
111
114
  const LEGACY_SPLITTER = {
112
- polygon: { splitter: "0xbD1fa5453f212F096c0213788a645eC597FB4DDe", version: "1.2", usdc: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359" },
113
- base: { splitter: "0x8Ad9830D16b1f10333866a3f38C949CbB19f4BAD", version: "1.1", usdc: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" },
114
- optimism: { splitter: "0xF03B3387415D557b6ab709D06E8aF0b4ABD6Eb74", version: "1.2", usdc: "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85" },
115
- unichain: { splitter: "0xeE92807decAa3A02F1e165dd7Efcd92ab9aA83CB", version: "1.1", usdc: "0x078D782b760474a361dDA0AF3839290b0EF57AD6" },
115
+ polygon: {
116
+ splitter: "0xbD1fa5453f212F096c0213788a645eC597FB4DDe",
117
+ version: "1.2",
118
+ usdc: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
119
+ },
120
+ base: {
121
+ splitter: "0x8Ad9830D16b1f10333866a3f38C949CbB19f4BAD",
122
+ version: "1.1",
123
+ usdc: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
124
+ },
125
+ optimism: {
126
+ splitter: "0xF03B3387415D557b6ab709D06E8aF0b4ABD6Eb74",
127
+ version: "1.2",
128
+ usdc: "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85",
129
+ },
130
+ unichain: {
131
+ splitter: "0xeE92807decAa3A02F1e165dd7Efcd92ab9aA83CB",
132
+ version: "1.1",
133
+ usdc: "0x078D782b760474a361dDA0AF3839290b0EF57AD6",
134
+ },
116
135
  robinhood: { splitter: "0x78bed24B8D3A5eB2cf8D9A0D6A9Da6Bc5d7f32eB", version: "1.2" },
117
136
  xrplevm: { splitter: "0x147d8fF8c027E24303b5B99CbC8843e1D3dF94cC", version: "1.2" },
118
137
  botchain: { splitter: "0x147d8fF8c027E24303b5B99CbC8843e1D3dF94cC", version: "1.2" },
@@ -141,7 +160,10 @@ const NATIVE_USD_DEFAULT = {
141
160
  const TRANSPORT_OVERRIDE = {
142
161
  polygon: { defaultRpc: "https://polygon.drpc.org" },
143
162
  botchain: { defaultRpc: "https://rpc.botchain.ai", explorer: "https://scan.botchain.ai" },
144
- robinhood: { defaultRpc: "https://rpc.mainnet.chain.robinhood.com", explorer: "https://robinhoodchain.blockscout.com" },
163
+ robinhood: {
164
+ defaultRpc: "https://rpc.mainnet.chain.robinhood.com",
165
+ explorer: "https://robinhoodchain.blockscout.com",
166
+ },
145
167
  };
146
168
  function buildSplitterDeployments() {
147
169
  const result = {};
@@ -156,7 +178,7 @@ function buildSplitterDeployments() {
156
178
  version: legacy.version,
157
179
  chainId: route?.chainId ?? chainObj.id,
158
180
  chain: chainObj,
159
- defaultRpc: override?.defaultRpc ?? route?.defaultRpc ?? (chainObj.rpcUrls?.default?.http?.[0] ?? ""),
181
+ defaultRpc: override?.defaultRpc ?? route?.defaultRpc ?? chainObj.rpcUrls?.default?.http?.[0] ?? "",
160
182
  splitter: legacy.splitter,
161
183
  usdc: legacy.usdc,
162
184
  explorer: override?.explorer ?? route?.explorer ?? "",
@@ -178,7 +200,11 @@ const EVM_CHAIN_OBJECTS = {
178
200
  robinhood,
179
201
  };
180
202
  function evmChainObject(name) {
181
- return (EVM_CHAIN_OBJECTS[name] ??
203
+ return ((name === "bnb"
204
+ ? bsc
205
+ : name === "avalanche"
206
+ ? avalanche
207
+ : EVM_CHAIN_OBJECTS[name]) ??
182
208
  SPLITTER_DEPLOYMENTS[name]?.chain);
183
209
  }
184
210
  // Mainnet USDC SPL mint on Solana (Circle native, not Wormhole-wrapped USDCet)
@@ -323,12 +349,12 @@ export class AiFinPayAgent {
323
349
  if (opts.spendLedger)
324
350
  this.ledgerOverride = opts.spendLedger;
325
351
  this.telemetry = opts.telemetry !== false;
326
- this.polygonRpc = opts.polygonRpc ?? "https://polygon.drpc.org";
352
+ this.polygonRpc = opts.polygonRpc ?? opts.evmRpcUrls?.polygon ?? "https://polygon.drpc.org";
327
353
  this.solanaRpc = opts.solanaRpc ?? process.env.AIFINPAY_SOLANA_RPC ?? "https://api.mainnet-beta.solana.com";
328
354
  // Optional RPC overrides for non-Polygon EVM chains used in bridge flows.
329
355
  // Falls back to viem's chain.rpcUrls.default if not provided.
330
356
  if (opts.evmRpcUrls)
331
- this.evmRpcUrls = opts.evmRpcUrls;
357
+ this.evmRpcUrls = { ...opts.evmRpcUrls };
332
358
  if (this.polygonRpc)
333
359
  this.evmRpcUrls.polygon = this.polygonRpc;
334
360
  }
@@ -365,7 +391,9 @@ export class AiFinPayAgent {
365
391
  if (!chain) {
366
392
  throw new AiFinPayError(`evmClients: unsupported EVM chain "${name}"`);
367
393
  }
368
- const rpcUrl = this.evmRpcUrls[name] ?? SPLITTER_DEPLOYMENTS[name]?.defaultRpc;
394
+ const rpcUrl = this.evmRpcUrls[name] ??
395
+ paymentChain(name)?.defaultRpc ??
396
+ SPLITTER_DEPLOYMENTS[name]?.defaultRpc;
369
397
  const transport = rpcUrl ? http(rpcUrl) : http();
370
398
  const publicClient = createPublicClient({ chain, transport });
371
399
  const walletClient = createWalletClient({ chain, transport, account: this.evmAccount });
@@ -518,7 +546,7 @@ export class AiFinPayAgent {
518
546
  for (const url of this.registryCandidates) {
519
547
  let r;
520
548
  try {
521
- r = await fetch(url);
549
+ r = await this.inner.fetchImpl(url);
522
550
  }
523
551
  catch (err) {
524
552
  attempts.push(`${url} → ${err.message}`);
@@ -591,7 +619,7 @@ export class AiFinPayAgent {
591
619
  const nonce = await this.networkNonce();
592
620
  const message = `AiFinPay-network-publish:polygon:${addr}:${nonce}`;
593
621
  const signature = await this.evmAccount.signMessage({ message });
594
- const r = await fetch(`${base}/api/network/agents/${addr}/publish`, {
622
+ const r = await this.inner.fetchImpl(`${base}/api/network/agents/${addr}/publish`, {
595
623
  method: "POST",
596
624
  headers: { "content-type": "application/json" },
597
625
  body: JSON.stringify({
@@ -617,7 +645,7 @@ export class AiFinPayAgent {
617
645
  const nonce = await this.networkNonce();
618
646
  const message = `AiFinPay-network-unpublish:polygon:${addr}:${nonce}`;
619
647
  const signature = await this.evmAccount.signMessage({ message });
620
- const r = await fetch(`${base}/api/network/agents/${addr}/unpublish`, {
648
+ const r = await this.inner.fetchImpl(`${base}/api/network/agents/${addr}/unpublish`, {
621
649
  method: "POST",
622
650
  headers: { "content-type": "application/json" },
623
651
  body: JSON.stringify({ nonce, signature }),
@@ -647,14 +675,14 @@ export class AiFinPayAgent {
647
675
  if (query.limit)
648
676
  params.set("limit", String(query.limit));
649
677
  }
650
- const r = await fetch(`${base}/api/network/agents?${params.toString()}`);
678
+ const r = await this.inner.fetchImpl(`${base}/api/network/agents?${params.toString()}`);
651
679
  if (!r.ok)
652
680
  throw new AiFinPayError(`network search ${base} → ${r.status}`);
653
681
  const j = (await r.json());
654
682
  return j.agents ?? [];
655
683
  }
656
684
  async networkNonce() {
657
- const r = await fetch(`${this.inner.baseUrl}/api/network/nonce`);
685
+ const r = await this.inner.fetchImpl(`${this.inner.baseUrl}/api/network/nonce`);
658
686
  if (!r.ok)
659
687
  throw new AiFinPayError(`network nonce → ${r.status}`);
660
688
  const { nonce } = (await r.json());
@@ -698,11 +726,11 @@ export class AiFinPayAgent {
698
726
  * total and then reserved would have rebuilt the race this replaces, where
699
727
  * two concurrent calls both read the same total, both passed, and both paid.
700
728
  */
701
- async reserveDaily(costUsd) {
729
+ async reserveDaily(costUsd, binding) {
702
730
  const cap = this.budgetCaps.daily_usd;
703
731
  if (cap === undefined)
704
732
  return null; // no daily cap to enforce
705
- const id = await this.ledger.reserve(costUsd, cap, 24 * 3600 * 1000);
733
+ const id = await this.ledger.reserve(costUsd, cap, 24 * 3600 * 1000, binding);
706
734
  if (id)
707
735
  return id;
708
736
  const err = new BudgetCapExceededError("daily", `this call would take daily spend past the $${cap} cap`);
@@ -1150,7 +1178,7 @@ export class AiFinPayAgent {
1150
1178
  if (p.settlementCall?.splitter_version !== "1.4")
1151
1179
  throw new AiFinPayError("expected signed v1.4 settlement call");
1152
1180
  const chain = opts.v14.chain ?? "polygon";
1153
- const { publicClient, walletClient } = this.splitterClients(chain);
1181
+ const { publicClient, walletClient } = chain === "polygon" ? this.polygonClients() : this.evmClients(chain);
1154
1182
  const result = await executeV14Settlement(p.settlementCall, {
1155
1183
  publicClient,
1156
1184
  walletClient,
@@ -1202,27 +1230,97 @@ export class AiFinPayAgent {
1202
1230
  * `budgetCaps.on_limit_exceeded` is "skip".
1203
1231
  */
1204
1232
  async fetchPaid(url, init = {}, opts = {}) {
1233
+ // Quote admission is durable even when the owner did not configure a daily cap.
1234
+ const ledger = opts.solanaV14 && !this.ledgerOverride ? (this._ledger = FileSpendLedger.forAgent(this.evmAddress)) : this.ledger;
1235
+ if (opts.solanaV14 && (!ledger.beginQuoteAdmission || !ledger.adoptQuoteAdmission || !ledger.closeQuoteAdmission))
1236
+ throw new Aifp1QuoteError("Solana custom ledger requires durable quote-admission hooks before POST");
1237
+ if ((opts.v14 || opts.solanaV14) &&
1238
+ this.budgetCaps.daily_usd !== undefined &&
1239
+ (!this.ledger.prepare || !this.ledger.assertRecovery || !this.ledger.complete)) {
1240
+ throw new Aifp1QuoteError("Custom capped v1.4 ledger requires prepare/assertRecovery/complete hooks before payment");
1241
+ }
1205
1242
  const deps = {
1243
+ walletIdentity: this.evmAddress.toLowerCase(),
1244
+ quoteOwnerContext: JSON.stringify([
1245
+ this.solanaRpc,
1246
+ this.budgetCaps.daily_usd ?? null,
1247
+ this.budgetCaps.per_call_usd ?? null,
1248
+ this.budgetCaps.on_limit_exceeded ?? "throw",
1249
+ ]),
1250
+ beginQuoteAdmission: (binding, context, create) => ledger.beginQuoteAdmission(binding, context, create),
1251
+ adoptQuoteAdmission: (id, binding, context, quoteJson) => ledger.adoptQuoteAdmission(id, binding, context, quoteJson),
1252
+ closeQuoteAdmission: (id, binding, context, terminal, quoteJson) => ledger.closeQuoteAdmission(id, binding, context, terminal, quoteJson),
1253
+ verifyHistoricalSolanaQuote: (call, auth, orderId) => verifyHistoricalSolanaV14Quote(call, {
1254
+ rpc: solanaV14Rpc(this.solanaRpc, this.inner.fetchImpl),
1255
+ deployment: authorizedSolanaV14Inventory(auth.environment, auth.network),
1256
+ payer: this.solanaAddress,
1257
+ orderId,
1258
+ }),
1206
1259
  fetchImpl: this.inner.fetchImpl,
1207
1260
  cache: this._aifp1Cache,
1208
1261
  // A 0x address, because AIFP agent policies are address-keyed
1209
1262
  // (backend/aifp/agent-policy.js normalizeAddress) — a Solana pubkey here
1210
1263
  // would silently opt the agent out of its owner's own limits.
1211
- agentId: opts.agentId ?? this.evmAddress,
1212
- payerAddress: this.evmAddress,
1213
- signPaymentAuthorization: (message) => this.evmAccount.signMessage({ message }),
1264
+ agentId: opts.agentId ?? (opts.solanaV14 ? this.solanaAddress : this.evmAddress),
1265
+ payerAddress: opts.solanaV14 ? this.solanaAddress : this.evmAddress,
1266
+ signPaymentAuthorization: (message) => opts.solanaV14
1267
+ ? Promise.resolve(bs58.encode(nacl.sign.detached(Buffer.from(message), this.inner.secretKey)))
1268
+ : this.evmAccount.signMessage({ message }),
1269
+ prepareSolana: (call, auth, orderId) => prepareSolanaV14Settlement(call, {
1270
+ rpc: solanaV14Rpc(this.solanaRpc, this.inner.fetchImpl),
1271
+ deployment: authorizedSolanaV14Inventory(auth.environment, auth.network),
1272
+ payer: this.solanaAddress,
1273
+ orderId,
1274
+ maxFeeLamports: auth.maxFeeLamports,
1275
+ }),
1276
+ settleSolana: (plan, onPrepared) => executeSolanaV14Settlement(plan, {
1277
+ rpc: solanaV14Rpc(this.solanaRpc, this.inner.fetchImpl),
1278
+ keypair: Keypair.fromSecretKey(this.inner.secretKey),
1279
+ onPrepared,
1280
+ }),
1214
1281
  settle: (p) => this.settleAifp1NativeV13(p, opts),
1215
1282
  checkPerCall: (usd) => this.checkPerCall(usd),
1216
- reserveDaily: (usd) => this.reserveDaily(usd),
1283
+ reserveDaily: async (usd, binding, admissionId) => {
1284
+ if (!admissionId)
1285
+ return this.reserveDaily(usd, binding);
1286
+ const cap = this.budgetCaps.daily_usd ?? Number.MAX_VALUE;
1287
+ const id = await ledger.reserve(usd, cap, 24 * 3600 * 1000, binding, admissionId);
1288
+ if (id)
1289
+ return id;
1290
+ if (this.budgetCaps.on_limit_exceeded === "skip")
1291
+ return "skip";
1292
+ throw new BudgetCapExceededError("daily", `this call would take daily spend past the $${cap} cap`);
1293
+ },
1217
1294
  commit: (id, usd) => this.ledger.commit(id, usd),
1218
1295
  release: (id) => this.ledger.release(id),
1296
+ prepareReservation: async (id, tx, binding) => {
1297
+ if (!this.ledger.prepare || !this.ledger.assertRecovery || !this.ledger.complete) {
1298
+ throw new Aifp1QuoteError("Custom capped v1.4 ledger requires prepare/assertRecovery/complete hooks");
1299
+ }
1300
+ await this.ledger.prepare(id, tx, binding);
1301
+ },
1302
+ assertReservation: async (id, tx, binding) => {
1303
+ if (!this.ledger.assertRecovery)
1304
+ throw new Aifp1QuoteError("Custom ledger cannot verify the original budget reservation");
1305
+ await this.ledger.assertRecovery(id, tx, binding);
1306
+ },
1307
+ completeReservation: async (id, tx, binding) => {
1308
+ if (!this.ledger.complete)
1309
+ throw new Aifp1QuoteError("Custom ledger cannot reconcile the prepared payment");
1310
+ await this.ledger.complete(id, tx, binding);
1311
+ },
1312
+ finalizeFailedReservation: async (id, tx, binding, feeUsd) => {
1313
+ if (!this.ledger.finalizeFailure)
1314
+ throw new Aifp1QuoteError("Custom ledger cannot reconcile proven finalized failure; retain the reservation");
1315
+ await this.ledger.finalizeFailure(id, tx, binding, feeUsd);
1316
+ },
1219
1317
  onPaid: ({ merchantId, amountUsd, txRef }) => {
1220
1318
  this.spend24h.add(amountUsd);
1221
1319
  if (this.telemetry) {
1222
1320
  this.reportTelemetry({
1223
1321
  kind: "aifp1",
1224
1322
  merchant: merchantId,
1225
- chain: opts.v14?.chain ?? "polygon",
1323
+ chain: opts.solanaV14 ? "solana" : (opts.v14?.chain ?? "polygon"),
1226
1324
  cost: amountUsd,
1227
1325
  tx: txRef,
1228
1326
  });
@@ -1233,10 +1331,37 @@ export class AiFinPayAgent {
1233
1331
  }
1234
1332
  /** Recover receipt issuance for an existing payment; never sends a transaction. */
1235
1333
  async recoverPaidPayment(recovery, opts = {}) {
1334
+ if (recovery.family !== "solana" && recovery.budgetReservationId !== undefined) {
1335
+ const chain = recovery.chain ?? "polygon";
1336
+ await assertPreparedV14Recovery(recovery.quote.settlement_call, { hash: recovery.txRef, serializedTransaction: recovery.serializedTransaction }, chain, this.evmAddress, recovery.quote.quote_id);
1337
+ }
1236
1338
  return recoverAifp1Payment(recovery, {
1237
- payerAddress: this.evmAddress,
1238
- signPaymentAuthorization: (message) => this.evmAccount.signMessage({ message }),
1339
+ walletIdentity: this.evmAddress.toLowerCase(),
1340
+ payerAddress: recovery.family === "solana" ? this.solanaAddress : this.evmAddress,
1341
+ ...(recovery.family === "solana"
1342
+ ? {
1343
+ solanaRpc: solanaV14Rpc(this.solanaRpc, this.inner.fetchImpl),
1344
+ finalizeFailedReservation: async (id, tx, binding, feeUsd) => {
1345
+ if (!this.ledger.finalizeFailure)
1346
+ throw new Aifp1QuoteError("Custom ledger cannot reconcile canonical failure; retain reservation");
1347
+ await this.ledger.finalizeFailure(id, tx, binding, feeUsd);
1348
+ },
1349
+ }
1350
+ : {}),
1351
+ signPaymentAuthorization: (message) => recovery.family === "solana"
1352
+ ? Promise.resolve(bs58.encode(nacl.sign.detached(Buffer.from(message), this.inner.secretKey)))
1353
+ : this.evmAccount.signMessage({ message }),
1239
1354
  fetchImpl: this.inner.fetchImpl,
1355
+ assertReservation: async (id, tx, binding) => {
1356
+ if (!this.ledger.assertRecovery)
1357
+ throw new Aifp1QuoteError("Custom ledger cannot verify the original budget reservation");
1358
+ await this.ledger.assertRecovery(id, tx, binding);
1359
+ },
1360
+ completeReservation: async (id, tx, binding) => {
1361
+ if (!this.ledger.complete)
1362
+ throw new Aifp1QuoteError("Custom ledger cannot reconcile the prepared payment");
1363
+ await this.ledger.complete(id, tx, binding);
1364
+ },
1240
1365
  }, opts);
1241
1366
  }
1242
1367
  /**
@@ -1266,9 +1391,18 @@ export class AiFinPayAgent {
1266
1391
  * Prepaid quota batches for this agent's address, most room first with a
1267
1392
  * per-merchant rollup. Non-signing read of retained receipts metadata.
1268
1393
  */
1394
+ async getPaymentHistory(opts = {}) {
1395
+ return getAgentHistory({
1396
+ ...opts,
1397
+ address: opts.network === "solana" ? this.solanaAddress : this.evmAddress,
1398
+ fetchImpl: this.inner.fetchImpl,
1399
+ });
1400
+ }
1269
1401
  async getQuota(opts = {}) {
1270
1402
  return getQuota({
1271
- address: this.evmAddress,
1403
+ address: opts.network === "solana" ? this.solanaAddress : this.evmAddress,
1404
+ network: opts.network,
1405
+ solanaNetwork: opts.solanaNetwork,
1272
1406
  merchantId: opts.merchantId,
1273
1407
  includeExhausted: opts.includeExhausted,
1274
1408
  baseUrl: opts.baseUrl,
@@ -1292,29 +1426,53 @@ export class AiFinPayAgent {
1292
1426
  * with no known price is excluded from the total and listed in
1293
1427
  * `unknown_legs` instead of being fabricated.
1294
1428
  */
1295
- async balance() {
1429
+ async balance(options = {}) {
1430
+ const cluster = options.solanaNetwork;
1431
+ if (cluster !== undefined) {
1432
+ if (cluster !== "mainnet" && cluster !== "devnet")
1433
+ throw new AiFinPayError("Explicit Solana balance cluster is invalid");
1434
+ if ((await solanaV14Rpc(this.solanaRpc, this.inner.fetchImpl).request("getGenesisHash", [])) !==
1435
+ SOLANA_GENESIS[cluster])
1436
+ throw new AiFinPayError("Solana balance RPC does not match the owner-selected cluster");
1437
+ }
1438
+ const mint = cluster ? solanaStableMint(cluster, "USDC") : USDC_SOLANA_MINT;
1296
1439
  const [pol, sol] = await Promise.all([
1297
1440
  this.tokenUsd("POL", SPLITTER_DEPLOYMENTS.polygon.nativeUsdEnv),
1298
- this.tokenUsd("SOL", "AIFINPAY_SOL_USD"),
1441
+ cluster === "devnet"
1442
+ ? Promise.resolve({ usd: null, source: "test-assets-not-valued" })
1443
+ : this.tokenUsd("SOL", "AIFINPAY_SOL_USD"),
1299
1444
  ]);
1300
1445
  const polygon = await this.fetchPolygonNative().catch(() => 0);
1301
- const solana = await this.fetchSolanaNative().catch(() => 0);
1302
- const solana_usdc = await this.fetchSolanaUsdc().catch(() => 0);
1446
+ const solana = cluster ? await this.fetchSolanaNative(true) : await this.fetchSolanaNative().catch(() => 0);
1447
+ const solana_usdc = cluster
1448
+ ? await this.fetchSolanaUsdc(mint, true)
1449
+ : await this.fetchSolanaUsdc(mint).catch(() => 0);
1303
1450
  const polygon_usdc = await this.fetchPolygonUsdc().catch(() => 0);
1304
1451
  const unknown_legs = [];
1305
- let agent_balance_usd = solana_usdc + polygon_usdc; // USDC ≈ $1
1452
+ let agent_balance_usd = (cluster === "devnet" ? 0 : solana_usdc) + polygon_usdc; // Devnet tokens have no fiat value.
1306
1453
  if (Number.isFinite(pol.usd))
1307
1454
  agent_balance_usd += polygon * pol.usd;
1308
1455
  else
1309
1456
  unknown_legs.push("pol");
1310
- if (Number.isFinite(sol.usd))
1457
+ if (typeof sol.usd === "number" && Number.isFinite(sol.usd))
1311
1458
  agent_balance_usd += solana * sol.usd;
1312
1459
  else
1313
1460
  unknown_legs.push("sol");
1314
1461
  return {
1315
1462
  agent_balance_usd,
1316
1463
  chains: {
1317
- solana: { sol: solana, usdc: solana_usdc, msecco_balance: 0 },
1464
+ solana: {
1465
+ sol: solana,
1466
+ usdc: solana_usdc,
1467
+ msecco_balance: 0,
1468
+ network: cluster ?? null,
1469
+ usdc_mint: mint,
1470
+ valuation: cluster === "devnet"
1471
+ ? "test-assets-not-valued"
1472
+ : cluster === "mainnet"
1473
+ ? "spot-estimate"
1474
+ : "unverified-owner-rpc/mainnet-mint",
1475
+ },
1318
1476
  polygon: { matic: polygon, usdc: polygon_usdc, msecco_balance: 0 },
1319
1477
  },
1320
1478
  spend_24h_usd: this.spend24h.total24h(),
@@ -1331,7 +1489,15 @@ export class AiFinPayAgent {
1331
1489
  });
1332
1490
  return Number(wei) / 1e18;
1333
1491
  }
1334
- async fetchSolanaNative() {
1492
+ async fetchSolanaNative(strict = false) {
1493
+ if (strict) {
1494
+ const result = (await solanaV14Rpc(this.solanaRpc, this.inner.fetchImpl).request("getBalance", [
1495
+ this.solanaAddress,
1496
+ ]));
1497
+ if (!result || !Number.isSafeInteger(result.value) || result.value < 0)
1498
+ throw new AiFinPayError("Solana balance RPC returned an invalid native balance");
1499
+ return result.value / 1e9;
1500
+ }
1335
1501
  // Raw JSON-RPC; honours the constructor's solanaRpc option (previously
1336
1502
  // re-read env here with a different default endpoint).
1337
1503
  const rpc = this.solanaRpc;
@@ -1355,10 +1521,29 @@ export class AiFinPayAgent {
1355
1521
  * Sum SPL USDC balances across all token accounts owned by the agent's
1356
1522
  * Solana pubkey. Uses raw JSON-RPC `getTokenAccountsByOwner` so we don't
1357
1523
  * pull `@solana/spl-token` as a dep. Returns 0 (never throws) when the
1358
- * RPC is unreachable or returns nothing — `balance()` must remain a
1359
- * non-blocking introspection call.
1524
+ * RPC is unreachable or returns nothing in the legacy, unverified context.
1525
+ * An explicit cluster requires a successful, well-formed balance read.
1360
1526
  */
1361
- async fetchSolanaUsdc() {
1527
+ async fetchSolanaUsdc(mint = USDC_SOLANA_MINT, strict = false) {
1528
+ if (strict) {
1529
+ const result = (await solanaV14Rpc(this.solanaRpc, this.inner.fetchImpl).request("getTokenAccountsByOwner", [
1530
+ this.solanaAddress,
1531
+ { mint },
1532
+ { encoding: "jsonParsed" },
1533
+ ]));
1534
+ if (!result || !Array.isArray(result.value))
1535
+ throw new AiFinPayError("Solana balance RPC returned invalid token accounts");
1536
+ let total = 0;
1537
+ for (const account of result.value) {
1538
+ const ui = account?.account?.data?.parsed?.info?.tokenAmount?.uiAmount;
1539
+ if (typeof ui !== "number" || !Number.isFinite(ui) || ui < 0)
1540
+ throw new AiFinPayError("Solana balance RPC returned an invalid token balance");
1541
+ total += ui;
1542
+ if (!Number.isFinite(total))
1543
+ throw new AiFinPayError("Solana token balance overflow");
1544
+ }
1545
+ return total;
1546
+ }
1362
1547
  const rpc = this.solanaRpc;
1363
1548
  try {
1364
1549
  const r = await this.inner.fetchImpl(rpc, {
@@ -1368,7 +1553,7 @@ export class AiFinPayAgent {
1368
1553
  jsonrpc: "2.0",
1369
1554
  id: 1,
1370
1555
  method: "getTokenAccountsByOwner",
1371
- params: [this.solanaAddress, { mint: USDC_SOLANA_MINT }, { encoding: "jsonParsed" }],
1556
+ params: [this.solanaAddress, { mint }, { encoding: "jsonParsed" }],
1372
1557
  }),
1373
1558
  });
1374
1559
  if (!r.ok)