@aifinpay/agent 1.8.2 → 2.0.0-rc.12

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 (97) hide show
  1. package/PAYMENT_RECEIPTS.md +110 -0
  2. package/README.md +93 -35
  3. package/dist/agent.d.ts +6 -1
  4. package/dist/agent.d.ts.map +1 -1
  5. package/dist/agent.js +50 -12
  6. package/dist/agent.js.map +1 -1
  7. package/dist/agentHistory.d.ts +15 -0
  8. package/dist/agentHistory.d.ts.map +1 -0
  9. package/dist/agentHistory.js +47 -0
  10. package/dist/agentHistory.js.map +1 -0
  11. package/dist/agentPassport.d.ts +95 -0
  12. package/dist/agentPassport.d.ts.map +1 -0
  13. package/dist/agentPassport.js +177 -0
  14. package/dist/agentPassport.js.map +1 -0
  15. package/dist/aifp1.d.ts +135 -8
  16. package/dist/aifp1.d.ts.map +1 -1
  17. package/dist/aifp1.js +317 -50
  18. package/dist/aifp1.js.map +1 -1
  19. package/dist/apiUrl.d.ts +3 -0
  20. package/dist/apiUrl.d.ts.map +1 -0
  21. package/dist/apiUrl.js +8 -0
  22. package/dist/apiUrl.js.map +1 -0
  23. package/dist/chains.d.ts +4 -0
  24. package/dist/chains.d.ts.map +1 -0
  25. package/dist/chains.js +24 -0
  26. package/dist/chains.js.map +1 -0
  27. package/dist/deploymentResolver.d.ts +87 -0
  28. package/dist/deploymentResolver.d.ts.map +1 -0
  29. package/dist/deploymentResolver.js +145 -0
  30. package/dist/deploymentResolver.js.map +1 -0
  31. package/dist/facilitators/aifinpay.d.ts +4 -5
  32. package/dist/facilitators/aifinpay.d.ts.map +1 -1
  33. package/dist/facilitators/aifinpay.js +49 -19
  34. package/dist/facilitators/aifinpay.js.map +1 -1
  35. package/dist/facilitators/base.d.ts +12 -1
  36. package/dist/facilitators/base.d.ts.map +1 -1
  37. package/dist/facilitators/detect.d.ts +3 -3
  38. package/dist/facilitators/detect.d.ts.map +1 -1
  39. package/dist/facilitators/detect.js +3 -22
  40. package/dist/facilitators/detect.js.map +1 -1
  41. package/dist/facilitators/standard-x402.d.ts +4 -8
  42. package/dist/facilitators/standard-x402.d.ts.map +1 -1
  43. package/dist/facilitators/standard-x402.js +218 -92
  44. package/dist/facilitators/standard-x402.js.map +1 -1
  45. package/dist/index.d.ts +26 -18
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +23 -31
  48. package/dist/index.js.map +1 -1
  49. package/dist/localIdentity.d.ts +17 -0
  50. package/dist/localIdentity.d.ts.map +1 -0
  51. package/dist/localIdentity.js +81 -0
  52. package/dist/localIdentity.js.map +1 -0
  53. package/dist/settlement.d.ts +198 -0
  54. package/dist/settlement.d.ts.map +1 -0
  55. package/dist/settlement.js +429 -0
  56. package/dist/settlement.js.map +1 -0
  57. package/dist/settlementHttp.d.ts +10 -0
  58. package/dist/settlementHttp.d.ts.map +1 -0
  59. package/dist/settlementHttp.js +70 -0
  60. package/dist/settlementHttp.js.map +1 -0
  61. package/dist/settlementV14.d.ts +136 -0
  62. package/dist/settlementV14.d.ts.map +1 -0
  63. package/dist/settlementV14.js +170 -0
  64. package/dist/settlementV14.js.map +1 -0
  65. package/dist/solanaDeploymentResolver.d.ts +90 -0
  66. package/dist/solanaDeploymentResolver.d.ts.map +1 -0
  67. package/dist/solanaDeploymentResolver.js +143 -0
  68. package/dist/solanaDeploymentResolver.js.map +1 -0
  69. package/dist/solanaV14Deployments.generated.d.ts +35 -0
  70. package/dist/solanaV14Deployments.generated.d.ts.map +1 -0
  71. package/dist/solanaV14Deployments.generated.js +61 -0
  72. package/dist/solanaV14Deployments.generated.js.map +1 -0
  73. package/dist/splitterRoutes.d.ts +120 -0
  74. package/dist/splitterRoutes.d.ts.map +1 -0
  75. package/dist/splitterRoutes.generated.d.ts +22 -0
  76. package/dist/splitterRoutes.generated.d.ts.map +1 -0
  77. package/dist/splitterRoutes.generated.js +459 -0
  78. package/dist/splitterRoutes.generated.js.map +1 -0
  79. package/dist/splitterRoutes.js +73 -0
  80. package/dist/splitterRoutes.js.map +1 -0
  81. package/dist/unifiedAgent.d.ts +96 -18
  82. package/dist/unifiedAgent.d.ts.map +1 -1
  83. package/dist/unifiedAgent.js +100 -325
  84. package/dist/unifiedAgent.js.map +1 -1
  85. package/dist/v14Deployments.generated.d.ts +53 -0
  86. package/dist/v14Deployments.generated.d.ts.map +1 -0
  87. package/dist/v14Deployments.generated.js +89 -0
  88. package/dist/v14Deployments.generated.js.map +1 -0
  89. package/dist/wallet.d.ts +28 -0
  90. package/dist/wallet.d.ts.map +1 -0
  91. package/dist/wallet.js +97 -0
  92. package/dist/wallet.js.map +1 -0
  93. package/package.json +42 -9
  94. package/dist/cli.d.ts +0 -3
  95. package/dist/cli.d.ts.map +0 -1
  96. package/dist/cli.js +0 -127
  97. package/dist/cli.js.map +0 -1
@@ -15,14 +15,30 @@ 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 { defineChain } from "viem";
19
18
  import { polygon, base, arbitrum, optimism, bsc, mainnet, unichain } from "viem/chains";
20
- import { Connection, Keypair, PublicKey, SystemProgram, Transaction, TransactionInstruction, sendAndConfirmTransaction, } from "@solana/web3.js";
19
+ import { botchain, xrplevm } from "./chains.js";
21
20
  import { AiFinPayError, X402Error, } from "./errors.js";
22
21
  import { Agent } from "./agent.js";
22
+ import { loadLocalWalletIdentity } from "./localIdentity.js";
23
+ import { SettlementClient, executeSettlementInvoice } from "./settlement.js";
23
24
  import { MemorySpendLedger, FileSpendLedger } from "./spendLedger.js";
24
25
  import { aifp1Fetch, Aifp1ReceiptCache, } from "./aifp1.js";
25
26
  import { bridgeQuote as crossChainQuote, bridgeExecute as crossChainExecute, bridgeWaitForArrival, USDC_NATIVE, } from "./crossChain.js";
27
+ /**
28
+ * The native-token payment block of a bridge 402, whichever key it arrived
29
+ * under.
30
+ *
31
+ * Both names, newest first. The production bridges renamed the block
32
+ * `pay_matic` → `pay_native` on 2026-08-04 when the on-chain entrypoint became
33
+ * `payNative` — and no SDK branch followed, so `agent.call()` failed against
34
+ * every live bridge with an error blaming facilitator wiring (AIFINP-118).
35
+ * Exported so the captured-fixture test drives this exact function; the
36
+ * original bug survived because the tests built their 402s in the SDK's own
37
+ * vocabulary and never met a real one.
38
+ */
39
+ export function nativePayBlock(challenge) {
40
+ return challenge.pay_native ?? challenge.pay_matic;
41
+ }
26
42
  // ── B2BSplitter contract ABI ─────────────────────────────────────────────
27
43
  // `payMatic` is the splitter's generic NATIVE-token payment entrypoint —
28
44
  // the name is a Polygon-era legacy. On Base/Optimism/Unichain it settles
@@ -72,32 +88,6 @@ const SPLITTER_PAY_NATIVE_ABI = [
72
88
  export function paymentIdFor(orderId) {
73
89
  return keccak256(toHex(orderId));
74
90
  }
75
- // ── B2BSplitter multi-chain deployment registry ──────────────────────────
76
- //
77
- // Chains where the fee-on-top B2BSplitter contract is DEPLOYED AND VERIFIED
78
- // on-chain (eth_getCode returned bytecode for every address below,
79
- // 2026-07-15). Do NOT add chains here without re-running that check.
80
- //
81
- // BOT Chain (677) and XRPL EVM (1440000) are not shipped with viem/chains,
82
- // so we defineChain() them locally.
83
- const botchain = defineChain({
84
- id: 677,
85
- name: "BOT Chain",
86
- nativeCurrency: { name: "BOT", symbol: "BOT", decimals: 18 },
87
- rpcUrls: { default: { http: ["https://rpc.botchain.ai"] } },
88
- blockExplorers: {
89
- default: { name: "BOT Chain Explorer", url: "https://scan.botchain.ai" },
90
- },
91
- });
92
- const xrplevm = defineChain({
93
- id: 1440000,
94
- name: "XRPL EVM",
95
- nativeCurrency: { name: "XRP", symbol: "XRP", decimals: 18 },
96
- rpcUrls: { default: { http: ["https://rpc.xrplevm.org"] } },
97
- blockExplorers: {
98
- default: { name: "XRPL EVM Explorer", url: "https://explorer.xrplevm.org" },
99
- },
100
- });
101
91
  export const SPLITTER_DEPLOYMENTS = {
102
92
  polygon: {
103
93
  version: "1.2",
@@ -384,8 +374,30 @@ export class AiFinPayAgent {
384
374
  }
385
375
  // ── Constructors ─────────────────────────────────────────────────────────
386
376
  /**
387
- * Generate a fresh agent — new Solana keypair + new EVM keypair.
388
- * Use `fromSeed()` to derive both from a single backup phrase instead.
377
+ * Load an existing local identity without creating or replacing a wallet.
378
+ * Priority: SEED_HASH → ./aifinpay/agents.json → AIFINPAY_AGENT_SECRET →
379
+ * the MCP keystore at AIFINPAY_HOME/agent.json (default ~/.aifinpay).
380
+ * AIFINPAY_AGENTS_FILE/AGENT_ID select a project file/record; encrypted
381
+ * keystores require AIFINPAY_WALLET_PASSPHRASE. Reads process.env, not .env.
382
+ * Missing, invalid or ambiguous configuration throws before any network call.
383
+ */
384
+ static async fromEnvironment(opts = {}) {
385
+ const identity = loadLocalWalletIdentity();
386
+ try {
387
+ return identity.seedHash !== undefined
388
+ ? await AiFinPayAgent.fromSeed(identity.seedHash, opts)
389
+ : await AiFinPayAgent.fromSolanaSecret(identity.secretB58, opts);
390
+ }
391
+ catch {
392
+ // Decoder/library errors can include input. Never echo local key material.
393
+ throw new AiFinPayError(`Invalid wallet selected from ${identity.source}; refusing to generate a replacement wallet`);
394
+ }
395
+ }
396
+ /**
397
+ * Generate a fresh, ephemeral agent. Every call selects a new identity;
398
+ * existing environment variables and keystores are NOT loaded or changed.
399
+ * This method does not persist its keys. Use fromEnvironment() for a funded
400
+ * existing wallet, or privately persist the new seed/secret before funding.
389
401
  */
390
402
  static async new(opts = {}) {
391
403
  // Seed-derived, NOT an independent random EVM key.
@@ -422,17 +434,21 @@ export class AiFinPayAgent {
422
434
  * TODO(phase-1): replace with BIP-39/BIP-44 derivation
423
435
  * m/44'/501'/0'/0' (Solana) + m/44'/60'/0'/0/0 (EVM) once the
424
436
  * wallet-import audit recommendation lands.
437
+ * An explicit opts.evmPrivateKey imports an existing EVM wallet instead;
438
+ * keep that key as well as the seed to restore the same EVM address.
425
439
  */
426
440
  static async fromSeed(seedHex, opts = {}) {
427
- const seed = hexToBytes(seedHex.replace(/^0x/, ""));
428
- if (seed.length !== 32) {
441
+ const normalizedSeed = seedHex.replace(/^0x/, "");
442
+ if (!/^[0-9a-fA-F]{64}$/.test(normalizedSeed)) {
429
443
  throw new AiFinPayError(`fromSeed: seed must be 32 bytes (64 hex chars)`);
430
444
  }
445
+ const seed = hexToBytes(normalizedSeed);
431
446
  const kp = nacl.sign.keyPair.fromSeed(seed);
432
447
  const inner = Agent
433
448
  ._ofKeyPair?.(kp, opts) ?? Agent.fromSecretB58(bs58.encode(kp.secretKey), opts);
434
449
  // Derive EVM key from seed (independent, not BIP-44 — see TODO above)
435
- const evmHex = ("0x" + bytesToHex(crypto32(seed)));
450
+ const evmHex = opts.evmPrivateKey
451
+ ?? ("0x" + bytesToHex(crypto32(seed)));
436
452
  const evmAccount = privateKeyToAccount(evmHex);
437
453
  return new AiFinPayAgent(inner, evmAccount, opts);
438
454
  }
@@ -959,14 +975,10 @@ export class AiFinPayAgent {
959
975
  }
960
976
  // ── Call ────────────────────────────────────────────────────────────────
961
977
  /**
962
- * High-level paid call. Resolves the provider, picks a chain, builds the
963
- * payment, sends to the bridge, retries with payment proof.
964
- *
965
- * EVM settlement: per-call splitter via on-chain `B2BSplitter.payMatic()`
966
- * (generic native-token entrypoint) on any SPLITTER_DEPLOYMENTS chain —
967
- * polygon (default), base, optimism, unichain, botchain, xrplevm. Pass
968
- * `chain` in CallOptions to override (the provider must accept it).
969
- * Solana settlement: b2b_pay_with_split (live since 2026-05-18).
978
+ * Legacy bridge call. Free/read-only bridge responses are passed through;
979
+ * paid 402 challenges fail closed because this legacy settlement surface has
980
+ * no independently reviewed target. Use the reviewed fetchPaid v1.3 route
981
+ * with a fresh trusted quote/FX result for paid requests.
970
982
  *
971
983
  * Returns `null` (instead of a `Response`) iff a budget cap was hit
972
984
  * AND budget.on_limit_exceeded is set to "skip" — the call is dropped
@@ -1031,119 +1043,22 @@ export class AiFinPayAgent {
1031
1043
  catch (e) {
1032
1044
  throw new X402Error(`bridge returned 402 with non-JSON body`);
1033
1045
  }
1034
- // ── Solana branch (b2b_pay_with_split atomic split, live 2026-05-18) ──
1035
- if (chain === "solana") {
1036
- if (!challenge.pay_solana) {
1037
- throw new X402Error(`Bridge ${provider.name} returned 402 without a pay_solana block. ` +
1038
- `Either pick chain: "polygon" or ask the operator to set ` +
1039
- `BRIDGE_MERCHANT_SOLANA on the bridge service.`);
1040
- }
1041
- const ps = challenge.pay_solana;
1042
- // Guard: the bridge controls the challenge — verify the demanded
1043
- // amount is in the same ballpark as the declared cost / caps before
1044
- // signing anything. Stops a misquoting bridge from draining the agent.
1045
- {
1046
- const lamports = Number(ps.total_lamports ?? ps.merchant_amount_lamports);
1047
- const solUsd = parseFloat(process.env.AIFINPAY_SOL_USD ?? "200");
1048
- const estUsd = (Number.isFinite(lamports) ? lamports / 1e9 : 0) * solUsd;
1049
- const guarded = this.guardChallengeAmount(estUsd, cost, provider.name);
1050
- if (!guarded)
1051
- return null;
1052
- }
1053
- const solTxSig = await this.submitSolanaB2BPayWithSplit(ps);
1054
- const paidResp = await this.inner.fetchImpl(fullUrl, buildInit({
1055
- "x-solana-tx": solTxSig,
1056
- "x-order-id": ps.order_id,
1057
- }));
1058
- if (!paidResp.ok) {
1059
- const detail = await paidResp.text().catch(() => "<unreadable>");
1060
- throw new AiFinPayError(`Bridge retry failed ${paidResp.status} after Solana payment ${solTxSig}: ${detail.slice(0, 300)}`);
1061
- }
1062
- // Money moved here, and only here. The reservation becomes a
1063
- // settled amount; the finally below then has nothing to release.
1064
- this.spend24h.add(cost);
1065
- if (typeof reservation === "string") {
1066
- await this.ledger.commit(reservation, cost);
1067
- }
1068
- settled = true;
1069
- if (this.telemetry)
1070
- this.reportTelemetry({ kind: "call", provider: provider.name, chain, cost, tx: solTxSig });
1071
- // @internal — attach settlement metadata for consumers (MCP, telemetry
1072
- // dashboards) that need to surface the tx hash without an extra RPC
1073
- // call. Response headers are read-only after construction so we use
1074
- // an instance property + cast on the read side.
1075
- paidResp.aifinpayTx = solTxSig;
1076
- paidResp.aifinpayChain = "solana";
1077
- return paidResp;
1078
- }
1079
- // ── EVM branch (B2BSplitter.payMatic — generic native-token atomic split) ──
1080
- // Settles on any chain in SPLITTER_DEPLOYMENTS (polygon default; base,
1081
- // optimism, unichain, botchain, xrplevm). Native token only — the
1082
- // deployed splitter payment path has no ERC-20/USDC entrypoint.
1083
- if (!challenge.pay_matic) {
1084
- throw new X402Error(`bridge ${provider.name} returned 402 but no pay_matic block — only legacy AiFinPay/Coinbase facilitators not yet wired into AiFinPayAgent.call()`);
1085
- }
1086
- const pm = challenge.pay_matic;
1087
- const deployment = SPLITTER_DEPLOYMENTS[chain];
1088
- if (!deployment) {
1089
- throw new AiFinPayError(`No B2BSplitter deployment registered for chain "${chain}" — supported: ${Object.keys(SPLITTER_DEPLOYMENTS).join(", ")}`);
1046
+ // Legacy bridge settlement is fail-closed. The challenge historically
1047
+ // supplied arbitrary splitter/program addresses and native amounts, so
1048
+ // continuing from here could sign against an unreviewed target or let a
1049
+ // missing price bypass the budget. Keep free/read-only calls above, but
1050
+ // refuse every paid legacy challenge before any signing or broadcast.
1051
+ if (!(typeof provider.price_usd === "number" && Number.isFinite(provider.price_usd) && provider.price_usd > 0) ||
1052
+ !(typeof cost === "number" && Number.isFinite(cost) && cost > 0)) {
1053
+ throw new AiFinPayError(`legacy call() refused ${provider.name}: the provider has no trusted positive USD price; ` +
1054
+ `use the reviewed fetchPaid v1.3 route with fresh trusted FX instead`);
1090
1055
  }
1091
- // Refuse to pay a quote denominated for another chain's native token:
1092
- // total_wei quoted for POL on Polygon would be a massive overpay if
1093
- // blindly re-sent as ETH on Base. Legacy bridges always emit "polygon".
1094
- if (pm.chain && pm.chain !== chain) {
1095
- throw new X402Error(`bridge ${provider.name} quoted pay_matic for chain "${pm.chain}" but the call was routed to "${chain}" — refusing cross-denomination payment`);
1056
+ if (!legacySettlementIsReviewed()) {
1057
+ throw new AiFinPayError(`legacy call() settlement is disabled: challenge targets are not trusted for mainnet payment; ` +
1058
+ `use fetchPaid with a reviewed v1.3 settlement pin and fresh trusted FX`);
1096
1059
  }
1097
- // Guard: same ballpark check as the Solana branch — never sign for an
1098
- // amount wildly above the declared cost / per-call cap. Native-token
1099
- // USD price per chain via SPLITTER_DEPLOYMENTS.nativeUsdEnv.
1100
- {
1101
- const wei = Number(pm.total_wei);
1102
- const { usd: nativeUsd } = await this.nativeUsdFor(deployment);
1103
- // NaN when no price is known — guardChallengeAmount then declines to
1104
- // block rather than blocking on a guess. See nativeUsdFor().
1105
- const estUsd = (Number.isFinite(wei) ? wei / 1e18 : 0) * nativeUsd;
1106
- const guarded = this.guardChallengeAmount(estUsd, cost, provider.name);
1107
- if (!guarded)
1108
- return null;
1109
- }
1110
- // 2. Submit the B2BSplitter payment on the selected chain's mainnet and
1111
- // wait for it to land. Prefer the challenge's splitter address (current
1112
- // bridge behaviour); fall back to the registry address for bridges that
1113
- // omit it.
1114
- const splitterAddress = pm.splitter
1115
- ?? deployment.splitter;
1116
- const txHash = await this.settleSplitterNative({
1117
- chain,
1118
- splitter: splitterAddress,
1119
- splitterVersion: pm.splitter_version,
1120
- merchantWallet: pm.merchant_wallet,
1121
- totalWei: BigInt(pm.total_wei),
1122
- orderId: pm.order_id,
1123
- ipCreator: pm.ip_creator,
1124
- });
1125
- // 3. Retry the bridge with payment proof.
1126
- const paidResp = await this.inner.fetchImpl(fullUrl, buildInit({
1127
- "x-tx-hash": txHash,
1128
- "x-order-id": pm.order_id,
1129
- }));
1130
- if (!paidResp.ok) {
1131
- const detail = await paidResp.text().catch(() => "<unreadable>");
1132
- throw new AiFinPayError(`Bridge retry failed ${paidResp.status} after on-chain payment ${txHash}: ${detail.slice(0, 300)}`);
1133
- }
1134
- // Money moved here, and only here. The reservation becomes a
1135
- // settled amount; the finally below then has nothing to release.
1136
- this.spend24h.add(cost);
1137
- if (typeof reservation === "string") {
1138
- await this.ledger.commit(reservation, cost);
1139
- }
1140
- settled = true;
1141
- if (this.telemetry)
1142
- this.reportTelemetry({ kind: "call", provider: provider.name, chain, cost, tx: txHash });
1143
- // @internal — see Solana branch above for rationale on property-attach.
1144
- paidResp.aifinpayTx = txHash;
1145
- paidResp.aifinpayChain = chain;
1146
- return paidResp;
1060
+ throw new AiFinPayError(`legacy call() settlement is disabled: challenge targets are not trusted for mainnet payment; ` +
1061
+ `use fetchPaid with a reviewed v1.3 settlement pin and fresh trusted FX`);
1147
1062
  }
1148
1063
  finally {
1149
1064
  // Every path that did not move money gives the budget back now rather
@@ -1167,74 +1082,14 @@ export class AiFinPayAgent {
1167
1082
  // a reverted transaction would otherwise be handed to a merchant as proof of
1168
1083
  // a payment that never moved.
1169
1084
  async settleSplitterNative(p) {
1170
- // call() has already checked this for the bridge flow; repeated here
1171
- // because settlement is now reachable without going through call().
1172
- const deployment = SPLITTER_DEPLOYMENTS[p.chain];
1173
- if (!deployment) {
1174
- throw new AiFinPayError(`No B2BSplitter deployment registered for chain "${p.chain}" — supported: ${Object.keys(SPLITTER_DEPLOYMENTS).join(", ")}`);
1175
- }
1176
- // ipCreator routing: prefer the caller's explicit ip_creator; else route
1177
- // the royalty slot to the splitter's treasury (mirrors the Solana branch).
1178
- // Passing address(0) would skip the transfer and permanently strand the
1179
- // 1bp inside B2BSplitter — the contract has no sweep function.
1180
- const ipCreator = p.ipCreator
1181
- ?? await this.splitterTreasury(p.splitter, p.chain)
1182
- ?? "0x0000000000000000000000000000000000000000";
1183
- const { publicClient, walletClient } = this.splitterClients(p.chain);
1184
- // The entrypoint follows the deployed contract, not the SDK release: v1.2
1185
- // (Polygon, Optimism, BOT Chain, XRPL EVM as of 2026-07-31) takes a bytes32
1186
- // paymentId and rejects one it has already settled, while Base and Unichain
1187
- // still run v1.1. Sending v1.2 calldata to a v1.1 contract reverts with no
1188
- // useful reason, so this must be decided per chain.
1189
- // An explicit `splitterVersion` wins — a server that states it knows what
1190
- // it deployed. What it must NOT fall back to is this registry's
1191
- // chain -> version entry, because the ADDRESS came from the server and
1192
- // the version would come from a table: two sources that can disagree.
1193
- //
1194
- // In production they did. The Exa bridge still hands out the pre-v1.2
1195
- // splitter 0xE34Fc0E6… and sends no version; the table says "polygon is
1196
- // 1.2"; the SDK called payNative on a contract that only has payMatic, and
1197
- // the payment reverted with nothing in the reason to explain it.
1198
- //
1199
- // So when the server is silent, ask the contract at the address we were
1200
- // actually handed. The registry is used only if the chain cannot be read.
1201
- const splitterVersion = p.splitterVersion
1202
- ?? await this.detectSplitterVersion(p.splitter, p.chain, deployment.version);
1203
- await this.assertCanAffordNative(publicClient, deployment, p.totalWei);
1204
- const txHash = splitterVersion === "1.2"
1205
- ? await walletClient.writeContract({
1206
- address: p.splitter,
1207
- abi: SPLITTER_PAY_NATIVE_ABI,
1208
- functionName: "payNative",
1209
- args: [
1210
- paymentIdFor(p.orderId),
1211
- p.merchantWallet,
1212
- ipCreator,
1213
- p.orderId,
1214
- ],
1215
- value: p.totalWei,
1216
- chain: deployment.chain,
1217
- account: this.evmAccount,
1218
- })
1219
- : await walletClient.writeContract({
1220
- address: p.splitter,
1221
- abi: SPLITTER_PAY_MATIC_ABI,
1222
- functionName: "payMatic",
1223
- args: [
1224
- p.merchantWallet,
1225
- ipCreator,
1226
- p.orderId,
1227
- ],
1228
- value: p.totalWei,
1229
- chain: deployment.chain,
1230
- account: this.evmAccount,
1231
- });
1232
- // Wait for receipt (inclusion is enough on these fast-block chains).
1233
- const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash });
1234
- if (receipt.status !== "success") {
1235
- throw new AiFinPayError(`${deployment.chain.name} tx reverted: ${txHash}`);
1236
- }
1237
- return txHash;
1085
+ void p;
1086
+ throw new AiFinPayError("legacy splitter settlement is disabled; use fetchPaid with a reviewed v1.3 settlement pin");
1087
+ }
1088
+ // Legacy Solana settlement remains a named private surface only so older
1089
+ // callers fail with a clear migration error instead of reaching a signer.
1090
+ async submitSolanaB2BPayWithSplit(ps) {
1091
+ void ps;
1092
+ throw new AiFinPayError("legacy Solana settlement is disabled; use fetchPaid with a reviewed v1.3 settlement pin");
1238
1093
  }
1239
1094
  // ── AIFP-1 merchant paywall (gateway.aifinpay.io) ───────────────────────
1240
1095
  //
@@ -1251,6 +1106,24 @@ export class AiFinPayAgent {
1251
1106
  * contain bearer JWTs — treat them like keys, and do not log them.
1252
1107
  */
1253
1108
  get aifp1Receipts() { return this._aifp1Cache; }
1109
+ /**
1110
+ * Use the canonical invoice executor only with a caller-supplied deployment
1111
+ * pin. An unauthenticated quote cannot establish its own signing authority.
1112
+ */
1113
+ async settleAifp1NativeV13(p, opts) {
1114
+ const pin = opts.settlementPin;
1115
+ if (!pin || pin.route_class !== "AIFP-1" || pin.chain !== "polygon" || pin.testnet) {
1116
+ throw new AiFinPayError("AIFP-1 fetchPaid requires an independent trusted Polygon v1.3 settlement pin; other receipt networks are not supported");
1117
+ }
1118
+ const client = new SettlementClient({ baseUrl: opts.apiBaseUrl ?? "https://api.aifinpay.io", fetchImpl: this.inner.fetchImpl, timeoutMs: this.inner.timeoutMs });
1119
+ const invoice = await client.invoice({
1120
+ route_class: "AIFP-1", chain: "polygon", asset: "POL", gross_amount: p.grossWei,
1121
+ merchant_wallet: p.merchantWallet, order_id: p.orderId, valid_until: Number(p.validUntil),
1122
+ });
1123
+ const { publicClient, walletClient } = this.polygonClients();
1124
+ const execution = await executeSettlementInvoice(invoice, walletClient, publicClient, pin);
1125
+ return execution.settlement_tx;
1126
+ }
1254
1127
  /**
1255
1128
  * Fetch a paywalled URL, paying the AIFP-1 gateway if it asks.
1256
1129
  *
@@ -1266,7 +1139,6 @@ export class AiFinPayAgent {
1266
1139
  * `budgetCaps.on_limit_exceeded` is "skip".
1267
1140
  */
1268
1141
  async fetchPaid(url, init = {}, opts = {}) {
1269
- const deployment = SPLITTER_DEPLOYMENTS.polygon;
1270
1142
  const deps = {
1271
1143
  fetchImpl: this.inner.fetchImpl,
1272
1144
  cache: this._aifp1Cache,
@@ -1274,25 +1146,9 @@ export class AiFinPayAgent {
1274
1146
  // (backend/aifp/agent-policy.js normalizeAddress) — a Solana pubkey here
1275
1147
  // would silently opt the agent out of its owner's own limits.
1276
1148
  agentId: opts.agentId ?? this.evmAddress,
1277
- settle: (p) => this.settleSplitterNative({
1278
- // Polygon-only, and not by preference: /v1/pay verifies the settlement
1279
- // by reading the Splitter's Payment event, and that verifier implements
1280
- // Polygon alone (backend/aifp/verify-settlement.js — Solana returns
1281
- // notImplemented, anything else unsupported_chain). Settling elsewhere
1282
- // would move real money for a receipt that can never be issued.
1283
- chain: "polygon",
1284
- // The address is not carried in the quote, so it comes from this SDK's
1285
- // own verified registry. The backend checks the tx against its
1286
- // configured SPLITTER_ADDRESS_POLYGON; if an operator has pointed that
1287
- // somewhere else, /v1/pay answers no_splitter_payment_event, which is
1288
- // loud. Override here if you are running against such a deployment.
1289
- splitter: opts.splitter ?? deployment.splitter,
1290
- // Left undetected on purpose — settleSplitterNative asks the contract,
1291
- // which is the only source that cannot disagree with itself.
1292
- merchantWallet: p.merchantWallet,
1293
- totalWei: p.totalWei,
1294
- orderId: p.orderId,
1295
- }),
1149
+ payerAddress: this.evmAddress,
1150
+ signPaymentAuthorization: (message) => this.evmAccount.signMessage({ message }),
1151
+ settle: (p) => this.settleAifp1NativeV13(p, opts),
1296
1152
  checkPerCall: (usd) => this.checkPerCall(usd),
1297
1153
  reserveDaily: (usd) => this.reserveDaily(usd),
1298
1154
  commit: (id, usd) => this.ledger.commit(id, usd),
@@ -1309,66 +1165,6 @@ export class AiFinPayAgent {
1309
1165
  };
1310
1166
  return aifp1Fetch(deps, url, init, opts);
1311
1167
  }
1312
- // ── Solana b2b_pay_with_split — build, sign, send via @solana/web3.js ───
1313
- //
1314
- // Manual instruction encoding (no Anchor dep):
1315
- // discriminator = sha256("global:b2b_pay_with_split")[:8]
1316
- // args (Borsh) = u64 merchant_amount_lamports + string order_id
1317
- // accounts = [config_pda, vault_pda, agent (signer), treasury,
1318
- // ip_creator, merchant_wallet, system_program]
1319
- // PDAs derived from program_id with seeds ["config"] and ["vault"].
1320
- async submitSolanaB2BPayWithSplit(ps) {
1321
- const conn = new Connection(this.solanaRpc, "confirmed");
1322
- const programId = new PublicKey(ps.program_id);
1323
- const merchant = new PublicKey(ps.merchant_wallet);
1324
- const treasury = new PublicKey(ps.treasury);
1325
- // ip_creator slot: not surfaced by current bridge 402s; route through
1326
- // treasury so the on-chain 1bp still settles atomically (treasury earns
1327
- // both 100bp + 1bp). When bridges add per-merchant ip_creator support,
1328
- // accept it from `ps` and pass through.
1329
- const ipCreator = treasury;
1330
- // Solana keypair from legacy Agent inner (tweetnacl 64-byte secret).
1331
- const kp = Keypair.fromSecretKey(this.inner.secretKey);
1332
- if (kp.publicKey.toString() !== this.inner.address) {
1333
- throw new AiFinPayError(`Internal: Solana keypair pubkey ${kp.publicKey.toString()} does not match agent address ${this.inner.address}`);
1334
- }
1335
- // PDAs
1336
- const [configPda] = PublicKey.findProgramAddressSync([Buffer.from("config")], programId);
1337
- const [vaultPda] = PublicKey.findProgramAddressSync([Buffer.from("vault")], programId);
1338
- // Instruction discriminator: Anchor convention sha256("global:<fn_name>")[:8].
1339
- const disc = createHash("sha256").update("global:b2b_pay_with_split").digest().subarray(0, 8);
1340
- // Borsh args: merchant_amount_lamports (u64 LE) + order_id (string = u32 len + utf8)
1341
- const merchantAmount = BigInt(ps.merchant_amount_lamports);
1342
- const amountBuf = Buffer.alloc(8);
1343
- amountBuf.writeBigUInt64LE(merchantAmount);
1344
- const orderBytes = Buffer.from(ps.order_id, "utf8");
1345
- if (orderBytes.length > 64) {
1346
- throw new AiFinPayError(`order_id too long (${orderBytes.length} bytes > 64 limit)`);
1347
- }
1348
- const orderLenBuf = Buffer.alloc(4);
1349
- orderLenBuf.writeUInt32LE(orderBytes.length);
1350
- const data = Buffer.concat([disc, amountBuf, orderLenBuf, orderBytes]);
1351
- const ix = new TransactionInstruction({
1352
- programId,
1353
- keys: [
1354
- { pubkey: configPda, isSigner: false, isWritable: false },
1355
- { pubkey: vaultPda, isSigner: false, isWritable: false },
1356
- { pubkey: kp.publicKey, isSigner: true, isWritable: true },
1357
- { pubkey: treasury, isSigner: false, isWritable: true },
1358
- { pubkey: ipCreator, isSigner: false, isWritable: true },
1359
- { pubkey: merchant, isSigner: false, isWritable: true },
1360
- { pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
1361
- ],
1362
- data,
1363
- });
1364
- const tx = new Transaction().add(ix);
1365
- // sendAndConfirmTransaction sets recentBlockhash + feePayer + signs.
1366
- const sig = await sendAndConfirmTransaction(conn, tx, [kp], {
1367
- commitment: "confirmed",
1368
- preflightCommitment: "confirmed",
1369
- });
1370
- return sig;
1371
- }
1372
1168
  // ── Sessions ────────────────────────────────────────────────────────────
1373
1169
  /**
1374
1170
  * TODO(phase-3): open a metered budget session.
@@ -1475,8 +1271,6 @@ export class AiFinPayAgent {
1475
1271
  return 0; // never block balance() on Solana RPC issues
1476
1272
  }
1477
1273
  }
1478
- // Cache: "chain:splitter address" → treasury address (constant per deployment).
1479
- splitterTreasuryCache = new Map();
1480
1274
  /** Read + cache B2BSplitter.treasury() on the given chain (default
1481
1275
  * polygon for back-compat). Returns null on RPC failure. */
1482
1276
  /**
@@ -1495,8 +1289,8 @@ export class AiFinPayAgent {
1495
1289
  * six registry chains against mainnet.
1496
1290
  *
1497
1291
  * One eth_getCode, cached per (chain, address). Falls back to the registry
1498
- * only when the code cannot be read: guessing beats refusing to pay, and for
1499
- * the addresses we deployed the guess is right.
1292
+ * only for this read-only diagnostic when code cannot be read. The legacy
1293
+ * payment path is disabled regardless of the reported or detected version.
1500
1294
  */
1501
1295
  splitterVersionCache = new Map();
1502
1296
  async detectSplitterVersion(splitter, chainName, fallback) {
@@ -1518,27 +1312,6 @@ export class AiFinPayAgent {
1518
1312
  return fallback;
1519
1313
  }
1520
1314
  }
1521
- async splitterTreasury(splitter, chainName = "polygon") {
1522
- const cacheKey = `${chainName}:${splitter.toLowerCase()}`;
1523
- const cached = this.splitterTreasuryCache.get(cacheKey);
1524
- if (cached)
1525
- return cached;
1526
- try {
1527
- const { publicClient } = this.splitterClients(chainName);
1528
- const treasury = await publicClient.readContract({
1529
- address: splitter,
1530
- abi: SPLITTER_TREASURY_ABI,
1531
- functionName: "treasury",
1532
- });
1533
- if (!treasury || treasury === "0x0000000000000000000000000000000000000000")
1534
- return null;
1535
- this.splitterTreasuryCache.set(cacheKey, treasury);
1536
- return treasury;
1537
- }
1538
- catch {
1539
- return null;
1540
- }
1541
- }
1542
1315
  /**
1543
1316
  * Read the native Polygon USDC (Circle-issued, 6 decimals) balance for the
1544
1317
  * agent's EVM address via the existing viem publicClient. Returns 0
@@ -1583,6 +1356,8 @@ export class AiFinPayAgent {
1583
1356
  }).catch(() => { });
1584
1357
  }
1585
1358
  }
1359
+ /** Legacy bridge settlement has no reviewed mainnet route; keep this closed. */
1360
+ function legacySettlementIsReviewed() { return false; }
1586
1361
  // ── Internal hex helpers (small, no extra deps) ────────────────────────────
1587
1362
  function hexToBytes(hex) {
1588
1363
  const out = new Uint8Array(hex.length / 2);