@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
@@ -1,4 +1,121 @@
1
- # v1.4 payment authorization (2.3.0)
1
+ # Solana payment authorization (2.5.0 source candidate)
2
+
3
+ High-level execution still requires an accepted enabled canonical registry record.
4
+ Both real Solana records remain disabled. This configuration describes the
5
+ conditional source implementation; do not override availability in an application.
6
+
7
+ ```ts
8
+ import { AiFinPayAgent, Aifp1FinalizedFailureError } from "@aifinpay/agent";
9
+
10
+ const agent = await AiFinPayAgent.fromEnvironment({ solanaRpc: ownerSolanaRpc });
11
+ agent.setBudget({ daily_usd: ownerDailyUsdLimit, per_call_usd: ownerBatchUsdLimit });
12
+ await agent.fetchPaid(
13
+ url,
14
+ {},
15
+ {
16
+ gatewayOrigins: ["https://merchant.example"],
17
+ resourcePathMode: "direct",
18
+ maxAmountUsd: ownerBatchUsdLimit,
19
+ nativeUsdPrice: readFreshIndependentSolPrice,
20
+ solanaV14: {
21
+ environment: "prod",
22
+ network: "mainnet",
23
+ asset: "SOL", // USDC or USDT on mainnet; only reviewed USDC on devnet
24
+ maxFeeLamports: ownerFeeAndRentLimitLamports, // bigint, independently chosen
25
+ onPrepared: persistPrivatePaymentJournal,
26
+ },
27
+ }
28
+ );
29
+ ```
30
+
31
+ Use `environment:"dev",network:"devnet"` together for devnet; never infer a
32
+ cluster from the payer's address. `nativeUsdPrice` is required even for SPL to
33
+ value fee/rent costs, and must be independent and fresh within 60 seconds. Keys
34
+ remain local. Quotes use request-bound Ed25519 ownership authorization before
35
+ backend signing; the payer verifies the backend's secp256k1 quote signature
36
+ against freshly read Config. This is separate from Ed25519 receipt authorization.
37
+
38
+ Before the quote POST, the shared private ledger persists the exact owner-signed
39
+ request, nonce, statement and RPC/limits/cluster/program/IDL context. A timeout or
40
+ restart replays those same bytes, even after authorization expiry; the server may
41
+ return only its already-issued original quote. The response is adopted durably,
42
+ then its zero-debit phase converts atomically into the monetary reservation.
43
+ Unknown, malformed, policy or preflight failures keep the original admission;
44
+ changing owner context refuses replay. Custom Solana ledgers require the additive
45
+ quote-admission hooks as well as the existing bound recovery hooks.
46
+
47
+ Only an exact authenticated expired `410 not_admitted` from the owner API can
48
+ close an absent zero-debit admission. An expired original quote can become a
49
+ local nonbroadcast terminal only if no monetary reservation/signing occurred,
50
+ its exact stored signed deadline and current Config signer are verified, and
51
+ its original ID/time/evidence survive the atomic transition. That invocation
52
+ returns failure and never asks for a replacement quote. A later explicit fetch
53
+ may request a fresh admission; the backend still counts the old issued quote,
54
+ so its policy allowance can remain constrained. This is no backend refund.
55
+ Never clear state, rotate an unknown nonce, or downgrade while unresolved.
56
+
57
+ `maxFeeLamports` bounds transaction fees plus missing payer-nonce/consumed-nonce
58
+ PDA and destination ATA rent. Payer SPL ATA must already exist and be funded.
59
+ Destination ATA creation is idempotent within the **same** settlement transaction,
60
+ never a preparatory payment. Only classic Tokenkeg and independently pinned
61
+ mint/decimal identities are accepted. Token-2022 and unreviewed mints refuse.
62
+
63
+ The saved `Aifp1SolanaPaymentRecovery` includes `family:"solana"`, exact signature,
64
+ base64 signed bytes, network/program/IDL, blockhash, nonce, owner-approved fee caps
65
+ and independently sourced admission SOL/USD rate. The private ledger binds that
66
+ context and the common factory wallet identity before the callback resolves.
67
+ A failed fsync callback prevents broadcast. Never put journal bytes or JWTs in logs.
68
+
69
+ ```ts
70
+ try {
71
+ const paid = await agent.recoverPaidPayment(savedRecovery, {
72
+ solanaEnvironment: "prod",
73
+ solanaNetwork: "mainnet",
74
+ });
75
+ // Store the verified receipt privately; no transaction was resent.
76
+ } catch (error) {
77
+ if (error instanceof Aifp1FinalizedFailureError) {
78
+ // No paid access: original failure reconciled once to feeAmountUsd.
79
+ // actualFeeLamports and recovery identify the immutable original purchase.
80
+ }
81
+ throw error;
82
+ }
83
+ ```
84
+
85
+ Recovery validates the original signed transaction and owner-selected cluster;
86
+ quote expiry, changed current admin profiles and expired recent blockhash never
87
+ permit a second payment. A finalized failure requires the original raw bytes,
88
+ valid error/slot, pinned genesis, canonical finalized block inclusion and safe
89
+ actual fee within the original caps. The local fee-only transition is atomic and
90
+ idempotent, survives restart/day rollover and cannot overwrite confirmed payment.
91
+ An unavailable/malformed proof or local journal error keeps the original guard.
92
+ The backend's outstanding signed-quote policy reservation remains separate.
93
+ Older SDKs must not operate on unreconciled 2.5 journals; retain IDs while recovery
94
+ is possible, and reconcile before downgrade. Never clear state to retry payment.
95
+
96
+ Public reads need no passport binding or payer signature:
97
+
98
+ ```ts
99
+ await agent.getPaymentHistory({ network: "solana", solanaNetwork: "mainnet", source: "transactions" });
100
+ await agent.getQuota({ network: "solana", solanaNetwork: "mainnet" });
101
+ ```
102
+
103
+ `balance({ solanaNetwork: "mainnet" })` independently verifies the owner RPC's
104
+ genesis and uses that cluster's pinned USDC mint. Failed or malformed explicit
105
+ cluster balance reads throw; they never become a verified zero balance.
106
+ Devnet raw balances have test
107
+ context, no SOL USD price and no contribution to the aggregate fiat value.
108
+ Omitted legacy balance context is explicitly unverified owner RPC/mainnet mint;
109
+ it does not establish payment availability. No devnet USDT pin is provided.
110
+
111
+ These query independently pinned cluster/program and preserve base58 case. They
112
+ return retained partial-history coverage and explicitly unallocated costs, validate
113
+ exact wallet identity and conserved integer billing units, exclude arbitrary wallet transfers, may lag the
114
+ chain and redact bearer JWTs.
115
+
116
+ ---
117
+
118
+ # v1.4 payment authorization (2.4.0 source candidate)
2
119
 
3
120
  The existing v1.3 documentation below remains applicable to explicitly pinned
4
121
  v1.3 callers. The v1.4 path uses the original backend-signed quote:
@@ -37,10 +154,10 @@ owner reconciliation; receipt recovery is not a replacement broadcast.
37
154
 
38
155
  The public MCP candidate implements this private journal/cache/lock integration.
39
156
  Applications using the SDK directly own persistence and process coordination.
40
- `fetchPaid` supports Polygon and explicitly selected Base in live mode, with
41
- native POL/ETH or a stablecoin pinned for that chain. It never falls back to a
42
- legacy route or changes assets after a refusal. The low-level executor retains
43
- Polygon/Amoy support and requires `expectedChain: "base"` to execute on Base.
157
+ `fetchPaid` supports the current nine EVM mainnets in live mode, with the
158
+ selected native currency or a stablecoin pinned for that chain. Every non-Polygon
159
+ rail requires explicit owner selection. It never changes routes or assets after
160
+ a refusal. Amoy remains a low-level test executor, separate from live receipts.
44
161
 
45
162
  A successful executor result requires a mined success receipt with the matching
46
163
  Payment event. Unknown broadcast/confirmation yields the original hash. Profile
@@ -54,18 +171,22 @@ SDK verifies its `eth_chainId` is 8453 before signing. `polygonRpc` remains the
54
171
  Polygon transport and is never reused for Base.
55
172
 
56
173
  ```ts
57
- await agent.fetchPaid(url, {}, {
58
- gatewayOrigins: ["https://merchant.example"],
59
- resourcePathMode: "direct",
60
- maxAmountUsd: ownerLimitUsd,
61
- nativeUsdPrice: readFreshIndependentEthPrice,
62
- v14: {
63
- chain: "base", // trusted caller choice, never copied from a quote
64
- asset: "ETH", // or "USDC"; omit nativeUsdPrice for a USDC purchase
65
- maxGasWei: ownerFeeBudgetWei,
66
- onPrepared: persistPrivatePaymentJournal,
67
- },
68
- });
174
+ await agent.fetchPaid(
175
+ url,
176
+ {},
177
+ {
178
+ gatewayOrigins: ["https://merchant.example"],
179
+ resourcePathMode: "direct",
180
+ maxAmountUsd: ownerLimitUsd,
181
+ nativeUsdPrice: readFreshIndependentEthPrice,
182
+ v14: {
183
+ chain: "base", // trusted caller choice, never copied from a quote
184
+ asset: "ETH", // or "USDC"; omit nativeUsdPrice for a USDC purchase
185
+ maxGasWei: ownerFeeBudgetWei,
186
+ onPrepared: persistPrivatePaymentJournal,
187
+ },
188
+ }
189
+ );
69
190
  ```
70
191
 
71
192
  `asset` defaults to ETH on Base and POL on Polygon. `nativeUsdPrice` must report
@@ -80,12 +201,41 @@ are rechecked before settlement. Missing or malformed oracle answers block payme
80
201
  The gas cap and balance checks use this total, but L1/operator fees may change
81
202
  before inclusion and are not capped by the signed EIP-1559 transaction.
82
203
 
83
- New recovery records include `chain`. A record without this field is treated as
84
- Polygon for compatibility and cannot recover a Base payment. The stored chain,
204
+ New recovery records include `chain`. The low-level legacy default without it
205
+ remains Polygon. MCP may migrate a missing-chain journal only from independent
206
+ owner configuration after actual signed transaction bytes prove the chain ID,
207
+ payer, pinned target, exact calldata/value and token/approval. Insufficient
208
+ evidence requires owner reconciliation; no rail is guessed. The stored chain,
85
209
  asset and original quote must agree before the SDK signs a receipt claim.
86
210
  Recovery works after quote expiry when the existing transaction already settled;
87
211
  it never signs a replacement payment. Receipt JWT format is unchanged.
88
212
 
213
+ ## Durable USD reservations
214
+
215
+ The built-in file ledger shares one daily cap across processes on one local
216
+ filesystem. It reserves before approval/signing, reloads state under a private
217
+ exclusive lock, and writes through a mode-600 temporary file with atomic rename
218
+ and file/directory fsync. Corrupt or unreadable state refuses payment. A held lock
219
+ times out without being stolen; after a crash the owner must stop all users and
220
+ reconcile outstanding transactions before recovering that lock.
221
+
222
+ Unknown broadcasts retain their debit reservation indefinitely. Confirmed debits
223
+ count for 24 hours, while the unresolved purchase guard remains until a verified
224
+ receipt completes reconciliation. API/issuer, payer, merchant/scope/resource/mode,
225
+ chain/asset/token/gross/quote ID and signed-byte hash bind the reservation to the
226
+ journal. Changing rails cannot repurchase unresolved access. A confirmed debit is
227
+ never refunded for a receipt or HTTP failure. Recovery commits it once, and keeps
228
+ its reconciliation identity after the budget window expires without counting
229
+ that old debit. Bound history therefore grows; never truncate unresolved state.
230
+
231
+ Old custom capped v1.4 adapters must implement `prepare`, `assertRecovery` and
232
+ `complete`; absence fails before payment. The uncapped/legacy source API remains
233
+ compatible. `MemorySpendLedger` coordinates one shared instance only. These are
234
+ local filesystem guarantees, not a distributed cap across hosts. Do not downgrade
235
+ to an older SDK while a 2.4 reservation is pending: old code can expire/reset it.
236
+ Preserve the journal and reconcile first. Recovery never accepts a ledger path,
237
+ USD valuation or increased cap from a quote or journal.
238
+
89
239
  ---
90
240
 
91
241
  # Authorize and recover payment receipts
@@ -202,3 +352,28 @@ compatibility changes do not activate a disabled deployment. v1.4 execution
202
352
  and legacy `call()` payment submission remain disabled in this RC. Current
203
353
  v1.4 quotes omit mutable fees/treasury from the signed commitment; a new
204
354
  contract/SDK release is required. There is no automatic version fallback.
355
+
356
+ ## Nine-network v1.4 source capability (2.4.0)
357
+
358
+ `v14.chain` accepts polygon (default), base, optimism, arbitrum, avalanche, bnb,
359
+ unichain, xrplevm and robinhood. Quotes cannot choose or expand that setting.
360
+ `evmRpcUrls` keys use those canonical names (BNB uses bnb, not the legacy bridge
361
+ bsc name). The backend must independently authorize the requested settlement_chain
362
+ for the merchant. Native/USD prices must match POL/ETH/AVAX/BNB/XRP. Gas budgets
363
+ are in the selected native asset's18-decimal wei; approval and settlement share
364
+ the budget. OP models include oracle data/operator estimates, Nitro estimates
365
+ include parent data already. Unknown or unavailable fee estimates refuse signing.
366
+
367
+ The quote amount and existing settlement aliases still contain USD micro-units.
368
+ `token_settlement` contains asset, token, decimals, total_units, merchant_units,
369
+ protocol_fee_units, creator_units and settlement_semantics=gross-inclusive. Client
370
+ address/decimal pins, signed gross and exact approval bind its total to USDmicro
371
+ multiplied by10^(decimals-6). Legs split the token gross, not rounded USD legs.
372
+ Metadata is checked whenever present;18dp requires it and old6dp can omit it.
373
+ Signer/runtime/current-profile/TokenList checks are still performed before broadcast.
374
+ Profiles remain administratively mutable under the accepted v1.4 trust model;
375
+ these quote-time checks do not create immutable fee guarantees. Recovery never
376
+ re-sends a payment and retains its original chain even after quote expiry.
377
+
378
+ Source capability does not activate a production network. Per-chain deployment,
379
+ backend/indexing and actual paid acceptance gates are recorded in the release evidence.
package/README.md CHANGED
@@ -1,21 +1,41 @@
1
1
  # @aifinpay/agent (Node / TypeScript)
2
2
 
3
- Version **2.3.0** adds Base ETH and USDC to the AIFP-1 `fetchPaid` flow.
4
- Polygon remains the default. Select Base independently with `v14.chain: "base"`;
5
- a merchant quote cannot change that choice. Runtime, signer, token, RPC chain and
6
- current profile checks remain mandatory, along with a durable pre-broadcast
7
- journal and an explicit gas budget. Legacy `call()` remains unavailable.
8
-
9
- Native payments require a fresh independent `nativeUsdPrice`: POL/USD on Polygon,
10
- ETH/USD on Base. The SDK does not infer or fetch this price from a payment quote.
11
- Stablecoin payments use the selected deployment's pinned token and exact USD minor
12
- units. No native price is needed for stablecoin settlement.
13
-
14
- On Base, `maxGasWei` covers the estimated L2 execution fee plus buffered L1 data and
15
- operator fees, including approval when needed. This preflight estimate can change
16
- before inclusion; the transaction cannot enforce a hard cap on L1/operator fees.
17
- An unavailable fee oracle blocks signing. See the [Base configuration and recovery
18
- guide](./PAYMENT_RECEIPTS.md).
3
+ Version **2.5.0** (source candidate) extends the existing signed AIFP-1 `fetchPaid`
4
+ flow to Polygon, Base, Optimism, Arbitrum, Avalanche, BNB, Unichain, XRPL EVM and
5
+ Robinhood. Polygon remains the default; select another network explicitly with
6
+ `v14.chain`. A descriptor is client capability, not production readiness: the
7
+ merchant must authorize the chain and the backend must serve/verifiably settle it.
8
+ Runtime, signer, token, RPC-chain and current profile checks remain mandatory,
9
+ along with a durable pre-broadcast journal and explicit native gas budget.
10
+
11
+ Native purchases require a fresh independent `nativeUsdPrice` for POL, ETH, AVAX,
12
+ BNB or XRP on the selected chain. Stablecoins use an independently pinned token
13
+ address and decimals. USD micro-units remain unchanged; `token_settlement` binds
14
+ exact6/18-decimal token units and every gross-inclusive leg to the signed gross
15
+ and exact approval.18-decimal tokens require this metadata; legacy6-decimal
16
+ quotes remain accepted. XRPL EVM currently has no pinned stablecoin.
17
+
18
+ `maxGasWei` covers approval plus settlement. OP chains (Base/Optimism/Unichain)
19
+ include buffered L1/operator estimates; Nitro (Arbitrum/Robinhood) already includes
20
+ parent-data gas in the RPC estimate, so it is counted once. Unavailable OP fees
21
+ block signing. These are preflight estimates, not inclusion-time guarantees.
22
+ Current admin profiles remain mutable; receipt and recovery policy is unchanged.
23
+ See [payment configuration and recovery](./PAYMENT_RECEIPTS.md).
24
+
25
+ The 2.5 candidate also adds owner-selected `solanaV14` paid access for SOL9 and
26
+ classic SPL6 USDC/USDT. Network and environment, local Ed25519 payer,
27
+ independently sourced SOL/USD and `maxFeeLamports` (transaction fees **plus all
28
+ nonce/ATA rent**) are separate from EVM options. `v14` and `solanaV14` together
29
+ are rejected. Canonical Solana flags remain disabled: this source is conditional,
30
+ not a production activation or published 2.5 claim.
31
+
32
+ The exact 216-byte quote and 297-byte instruction follow the packaged pinned IDL.
33
+ Fresh genesis/config/profile/mint/nonce/account evidence and unsigned final-message
34
+ simulation precede budget reservation and local signing. Signed bytes are saved
35
+ before the single send. Unknown signature outcomes never expire or authorize a
36
+ replacement transaction. A canonical finalized failure retains only its proven
37
+ fee debit; missing proof retains the full reservation. Public history/quota
38
+ select Solana cluster explicitly and never expose bearer receipts.
19
39
 
20
40
  ### Link the agent to its owner's dashboard
21
41
 
@@ -69,7 +89,7 @@ npm pack
69
89
  Install the resulting tarball in your application:
70
90
 
71
91
  ```bash
72
- npm install /absolute/path/to/sdk/node/aifinpay-agent-2.3.0.tgz
92
+ npm install /absolute/path/to/sdk/node/aifinpay-agent-<version>.tgz
73
93
  ```
74
94
 
75
95
  ## Quick start
@@ -104,7 +124,8 @@ inputs and wallet files out of chats, logs and version control.
104
124
  The same configured inputs restore the same addresses after a process restart.
105
125
  An explicit `evmPrivateKey` option overrides the derived EVM identity; retain
106
126
  that separate key as well to recover the imported wallet. Loading a wallet
107
- does not enable the RC's gated settlement routes.
127
+ does not by itself pay for anything; paid execution goes through `fetchPaid`
128
+ and its checks.
108
129
 
109
130
  `AiFinPayAgent.new()` and `Agent.new()` intentionally create a fresh ephemeral
110
131
  wallet each time. They do not load existing environment variables or keystores
@@ -126,24 +147,34 @@ const agent2 = Agent.fromSecretB58("3RvZm7Gw...");
126
147
 
127
148
  ## How x402 auth works under the hood
128
149
 
129
- For every gated request the SDK:
130
-
131
- 1. `GET /nonce` → receives a one-time UUID with 60s TTL.
132
- 2. computes `SHA-256("AiFinPay-x402:{nonce}:{pubkey}")`.
133
- 3. signs with Ed25519, base58-encodes the signature.
150
+ `Agent.pay(url)` (and `get`/`post`/`request`) handles an AiFinPay-native 402
151
+ with request-bound auth (version 2). For a gated request the SDK:
152
+
153
+ 1. sends the request without credentials; the 402 body carries a one-time
154
+ `x-nonce`, its expiry `x-nonce-expires-at`, `x-aifinpay-auth-version: 2`
155
+ and the SHA-256 of the request body it was issued for;
156
+ 2. refuses to sign unless the request went to the origin configured as the
157
+ agent's `baseUrl`, the response came from that origin, and the body digest
158
+ matches the body it sent;
159
+ 3. signs, with Ed25519, the SHA-256 of
160
+ `JSON.stringify(["AiFinPay-x402", "v2", nonce, pubkey, origin, METHOD, path+query, bodySha256, expiresAt])`
161
+ and base58-encodes the signature;
134
162
  4. retries the original request with headers:
135
163
  - `x-agent-pubkey: <base58 pubkey>`
136
- - `x-nonce: <uuid>`
164
+ - `x-nonce: <nonce>`
137
165
  - `x-signature: <base58 sig>`
166
+ - `x-aifinpay-auth-version: 2`
138
167
 
139
- The server verifies the signature, checks the agent has a live Seat PDA
140
- on-chain, and serves the resource.
168
+ The server verifies the signature, consumes the nonce, checks the agent has a
169
+ live Seat PDA on-chain, and serves the resource. The retired v1 format
170
+ (`GET /nonce`, `SHA-256("AiFinPay-x402:{nonce}:{pubkey}")`) is refused, and
171
+ `Agent.authHeaders()` throws.
141
172
 
142
173
  ## Privacy
143
174
 
144
175
  - **The server never sees your private key.** Period.
145
176
  - Nonces are consumed on use; replay-resistant.
146
- - All transactions are public and on-chain — Solana + Polygon mainnet.
177
+ - Payments settle on public chains — the explicitly selected EVM network — and are visible on-chain.
147
178
 
148
179
  ## License
149
180
 
@@ -1,9 +1,12 @@
1
1
  import { type AgentPassportNetwork } from "./agentPassport.js";
2
+ import type { SolanaNetwork } from "./generated/solanaV14Deployments.generated.js";
2
3
  export interface AgentHistoryOptions {
3
4
  address?: string;
4
5
  /** Public @username, AIFP number or aifp_agent_* identifier; never a secret. */
5
6
  passport?: string;
6
7
  network?: AgentPassportNetwork;
8
+ /** Required with network="solana"; never infer a cluster from an address. */
9
+ solanaNetwork?: SolanaNetwork;
7
10
  source?: "transactions" | "receipts";
8
11
  limit?: number;
9
12
  offset?: number;
@@ -18,6 +21,8 @@ export declare const AGENT_TRANSACTION_FIELDS: string[];
18
21
  export declare function getAgentHistory(options: AgentHistoryOptions): Promise<Record<string, unknown>>;
19
22
  export interface QuotaOptions {
20
23
  address: string;
24
+ network?: AgentPassportNetwork;
25
+ solanaNetwork?: SolanaNetwork;
21
26
  /** Optional mrch_… filter — only batches bought from this service. */
22
27
  merchantId?: string;
23
28
  /** Also list batches with 0 remaining (still retained, not yet expired). */
@@ -36,6 +41,10 @@ export interface QuotaBatch {
36
41
  paid?: string;
37
42
  expires?: string;
38
43
  receipt_id?: string;
44
+ chain?: string;
45
+ network?: SolanaNetwork;
46
+ program?: string;
47
+ asset?: string;
39
48
  }
40
49
  export interface QuotaSummary {
41
50
  agent: string;
@@ -1 +1 @@
1
- {"version":3,"file":"agentHistory.d.ts","sourceRoot":"","sources":["../src/agentHistory.ts"],"names":[],"mappings":"AAAA,OAAO,EAA6C,KAAK,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAG1G,MAAM,WAAW,mBAAmB;IAClC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,gFAAgF;IAChF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,oBAAoB,CAAC;IAC/B,MAAM,CAAC,EAAE,cAAc,GAAG,UAAU,CAAC;IACrC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CAC1B;AAED,wGAAwG;AACxG,eAAO,MAAM,oBAAoB,UAoBhC,CAAC;AAEF,4GAA4G;AAC5G,eAAO,MAAM,wBAAwB,UAcpC,CAAC;AAEF,uFAAuF;AACvF,wBAAsB,eAAe,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CA8DpG;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,sEAAsE;IACtE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,4EAA4E;IAC5E,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CAC1B;AAED,MAAM,WAAW,UAAU;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC/D,OAAO,EAAE,UAAU,EAAE,CAAC;CACvB;AAED;;;;;GAKG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CAsC3E"}
1
+ {"version":3,"file":"agentHistory.d.ts","sourceRoot":"","sources":["../src/agentHistory.ts"],"names":[],"mappings":"AAAA,OAAO,EAA6C,KAAK,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAI1G,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,+CAA+C,CAAC;AAEnF,MAAM,WAAW,mBAAmB;IAClC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,gFAAgF;IAChF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,oBAAoB,CAAC;IAC/B,6EAA6E;IAC7E,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,MAAM,CAAC,EAAE,cAAc,GAAG,UAAU,CAAC;IACrC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CAC1B;AAED,wGAAwG;AACxG,eAAO,MAAM,oBAAoB,UAuBhC,CAAC;AAEF,4GAA4G;AAC5G,eAAO,MAAM,wBAAwB,UAyBpC,CAAC;AAgDF,uFAAuF;AACvF,wBAAsB,eAAe,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAyEpG;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,oBAAoB,CAAC;IAC/B,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,sEAAsE;IACtE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,4EAA4E;IAC5E,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CAC1B;AAED,MAAM,WAAW,UAAU;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC/D,OAAO,EAAE,UAAU,EAAE,CAAC;CACvB;AAED;;;;;GAKG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CAkE3E"}
@@ -1,5 +1,7 @@
1
1
  import { resolveAgentPassport, agentPassportWallet } from "./agentPassport.js";
2
2
  import { aifinpayApiUrl } from "./apiUrl.js";
3
+ import { PublicKey } from "@solana/web3.js";
4
+ import { solanaV14Inventory } from "./settlementSolanaV14.js";
3
5
  /** Public fields returned for source="receipts". Single source of truth — MCP must import, not copy. */
4
6
  export const AGENT_RECEIPT_FIELDS = [
5
7
  "receipt_id",
@@ -21,6 +23,9 @@ export const AGENT_RECEIPT_FIELDS = [
21
23
  "expires_at",
22
24
  "unit_quota",
23
25
  "metering_version",
26
+ "asset",
27
+ "network",
28
+ "program",
24
29
  ];
25
30
  /** Public fields returned for source="transactions". Single source of truth — MCP must import, not copy. */
26
31
  export const AGENT_TRANSACTION_FIELDS = [
@@ -37,7 +42,58 @@ export const AGENT_TRANSACTION_FIELDS = [
37
42
  "merchant_amount",
38
43
  "treasury_fee",
39
44
  "ip_creator_fee",
45
+ "network",
46
+ "program",
47
+ "block_hash",
48
+ "nonce",
49
+ "treasury_address",
50
+ "fee_lamports",
51
+ "nonce_rent_lamports",
52
+ "ata_rent_lamports",
53
+ "cost_allocation",
54
+ "order_id_hash",
55
+ "route_id",
40
56
  ];
57
+ function historyIdentity(address, network, cluster) {
58
+ if (network === "solana") {
59
+ if (!cluster)
60
+ throw new Error("Solana history requires an explicit solanaNetwork");
61
+ const d = solanaV14Inventory(cluster === "mainnet" ? "prod" : "dev", cluster);
62
+ try {
63
+ if (!address || new PublicKey(address).toBase58() !== address)
64
+ throw new Error();
65
+ }
66
+ catch {
67
+ throw new Error("History requires a canonical case-preserved Solana address");
68
+ }
69
+ return { address: address, network: cluster, program: d.programId };
70
+ }
71
+ if (cluster !== undefined)
72
+ throw new Error("solanaNetwork requires network=solana");
73
+ if (!address || !/^0x[0-9a-fA-F]{40}$/.test(address))
74
+ throw new Error("History requires an EVM address");
75
+ return { address: address.toLowerCase() };
76
+ }
77
+ function assertHistoryContext(body, selected, identityField = "address") {
78
+ if (selected.network &&
79
+ (!body ||
80
+ typeof body !== "object" ||
81
+ body.chain !== "solana" ||
82
+ body.network !== selected.network ||
83
+ body.program !== selected.program ||
84
+ body[identityField] !== selected.address))
85
+ throw new Error("History response changed the selected Solana payer/network/program");
86
+ }
87
+ function solanaCoverage(value) {
88
+ const v = value;
89
+ if (!v ||
90
+ typeof v !== "object" ||
91
+ v.coverage_kind !== "retained_rpc_inventory" ||
92
+ v.archive_complete !== false ||
93
+ !(v.coverage_start === null || (Number.isSafeInteger(v.coverage_start) && Number(v.coverage_start) > 0)))
94
+ throw new Error("Invalid retained Solana history coverage");
95
+ return { coverage_kind: v.coverage_kind, coverage_start: v.coverage_start, archive_complete: false };
96
+ }
41
97
  /** Read public payment metadata. A passport is resolved to a verified wallet first. */
42
98
  export async function getAgentHistory(options) {
43
99
  const base = options.baseUrl || "https://aifinpay.io";
@@ -58,41 +114,55 @@ export async function getAgentHistory(options) {
58
114
  let address = options.address;
59
115
  if (options.passport) {
60
116
  const identity = await resolveAgentPassport(options.passport, base, fetchImpl);
61
- identity.wallets = identity.wallets.filter((wallet) => wallet.chain_family === "evm" && Number(wallet.verified_at) > 0);
117
+ identity.wallets = identity.wallets.filter((wallet) => wallet.chain_family === (network === "solana" ? "solana" : "evm") && Number(wallet.verified_at) > 0);
62
118
  const wallet = agentPassportWallet(identity, network);
63
- if (address && address.toLowerCase() !== wallet.address.toLowerCase()) {
119
+ if (address &&
120
+ (network === "solana" ? address !== wallet.address : address.toLowerCase() !== wallet.address.toLowerCase())) {
64
121
  throw new Error("Address does not match this passport's verified wallet on the requested network");
65
122
  }
66
123
  address = wallet.address;
67
124
  }
68
- if (!address || !/^0x[0-9a-fA-F]{40}$/.test(address))
69
- throw new Error("History requires an EVM address or a passport with an EVM wallet binding");
125
+ const selected = historyIdentity(address, network, options.solanaNetwork);
70
126
  const query = new URLSearchParams({ limit: String(limit), offset: String(offset) });
71
- if (source === "transactions")
127
+ if (source === "transactions" || network === "solana")
72
128
  query.set("chain", network);
73
- const response = await fetchImpl(aifinpayApiUrl(base, `/v1/agents/${address.toLowerCase()}/${source}?${query}`), {
129
+ if (selected.network) {
130
+ query.set("network", selected.network);
131
+ query.set("program", selected.program);
132
+ }
133
+ const response = await fetchImpl(aifinpayApiUrl(base, `/v1/agents/${selected.address}/${source}?${query}`), {
74
134
  headers: { accept: "application/json" },
75
135
  });
76
136
  if (!response.ok)
77
- throw new Error(`Payment history unavailable: HTTP ${response.status}; source=${source}. Use receipts for retained prepaid batches; indexed transactions currently cover Polygon only.`);
137
+ throw new Error(`Payment history unavailable: HTTP ${response.status}; source=${source}; chain=${network}.`);
78
138
  const body = (await response.json());
79
139
  if (!body || typeof body !== "object" || !Array.isArray(body[source]))
80
140
  throw new Error("Invalid history response");
141
+ assertHistoryContext(body, selected);
81
142
  // Defense in depth: even a misconfigured/older backend must not send bearer
82
143
  // tokens to a caller of this metadata-only API.
83
144
  const fields = source === "receipts" ? AGENT_RECEIPT_FIELDS : AGENT_TRANSACTION_FIELDS;
84
- const items = body[source].map((item) => Object.fromEntries(fields.filter((field) => item[field] !== undefined).map((field) => [field, item[field]])));
145
+ const items = body[source].map((item) => {
146
+ if (!item || typeof item !== "object")
147
+ throw new Error("Invalid history row");
148
+ assertHistoryContext(item, selected, source === "receipts" ? "payer" : "agent_address");
149
+ return Object.fromEntries(fields.filter((field) => item[field] !== undefined).map((field) => [field, item[field]]));
150
+ });
85
151
  return {
86
- address: address.toLowerCase(),
152
+ address: selected.address,
153
+ ...(selected.network ? { chain: "solana", network: selected.network, program: selected.program } : {}),
87
154
  ...(options.passport ? { passport: options.passport } : {}),
88
155
  source,
89
156
  items,
90
157
  limit,
91
158
  offset,
92
159
  next_offset: typeof body.next_offset === "number" ? body.next_offset : null,
160
+ ...(selected.network && body.indexing !== undefined ? { indexing: solanaCoverage(body.indexing) } : {}),
93
161
  coverage: source === "receipts"
94
162
  ? "Retained AiFinPay receipts only; external merchant quota may lag"
95
- : "Indexed AiFinPay Polygon settlements only; excludes arbitrary wallet transfers and may lag the chain",
163
+ : selected.network
164
+ ? "Verified AiFinPay settlements from retained RPC inventory only; partial history, no archive/full-wallet attestation. Shared transaction costs may be unallocated."
165
+ : `Indexed AiFinPay ${network} settlements only; excludes arbitrary wallet transfers and may lag the chain`,
96
166
  };
97
167
  }
98
168
  /**
@@ -104,15 +174,34 @@ export async function getAgentHistory(options) {
104
174
  export async function getQuota(options) {
105
175
  const base = (options.baseUrl || "https://aifinpay.io").replace(/\/+$/, "");
106
176
  const fetchImpl = options.fetchImpl || fetch;
107
- const address = options.address.toLowerCase();
108
- if (!/^0x[0-9a-fA-F]{40}$/.test(address))
109
- throw new Error("Quota requires an EVM address");
110
- const response = await fetchImpl(aifinpayApiUrl(base, `/v1/agents/${address}/receipts`), {
177
+ const selected = historyIdentity(options.address, options.network ?? "polygon", options.solanaNetwork);
178
+ const query = new URLSearchParams();
179
+ if (options.network)
180
+ query.set("chain", options.network);
181
+ if (selected.network) {
182
+ query.set("network", selected.network);
183
+ query.set("program", selected.program);
184
+ }
185
+ const response = await fetchImpl(aifinpayApiUrl(base, `/v1/agents/${selected.address}/receipts${query.size ? `?${query}` : ""}`), {
111
186
  headers: { accept: "application/json" },
112
187
  });
113
188
  if (!response.ok)
114
189
  throw new Error(`Quota lookup failed: HTTP ${response.status}`);
115
190
  const body = (await response.json());
191
+ if (!body || !Array.isArray(body.receipts))
192
+ throw new Error("Invalid quota response");
193
+ assertHistoryContext(body, selected);
194
+ body.receipts.forEach((row) => {
195
+ assertHistoryContext(row, selected, "payer");
196
+ if (selected.network &&
197
+ (![row.used, row.remaining, row.unit_quota].every((v) => Number.isSafeInteger(v) && Number(v) >= 0) ||
198
+ !Number.isSafeInteger(Number(row.used) + Number(row.remaining)) ||
199
+ Number(row.used) + Number(row.remaining) !== row.unit_quota ||
200
+ (row.exp !== undefined &&
201
+ row.exp !== null &&
202
+ (!Number.isSafeInteger(row.exp) || Number(row.exp) <= 0 || Number(row.exp) > 8640000000000))))
203
+ throw new Error("Malformed Solana unit quota, usage or expiry");
204
+ });
116
205
  const now = Math.floor(Date.now() / 1000);
117
206
  const num = (v, dflt) => (typeof v === "number" && Number.isFinite(v) ? v : dflt);
118
207
  const batches = (body.receipts ?? [])
@@ -126,10 +215,13 @@ export async function getQuota(options) {
126
215
  tier: typeof r.tier === "string" ? r.tier : undefined,
127
216
  used: num(r.used, 0),
128
217
  remaining: num(r.remaining, 0),
129
- quota: num(r.quota, 1),
218
+ quota: selected.network ? Number(r.unit_quota) : num(r.quota, 1),
130
219
  paid: r.amount != null ? `${r.amount} ${typeof r.currency === "string" ? r.currency : "USD"}` : undefined,
131
220
  expires: typeof r.exp === "number" ? new Date(r.exp * 1000).toISOString() : undefined,
132
221
  receipt_id: typeof r.receipt_id === "string" ? r.receipt_id : undefined,
222
+ chain: typeof r.chain === "string" ? r.chain : undefined,
223
+ ...(selected.network ? { network: selected.network, program: selected.program } : {}),
224
+ asset: typeof r.asset === "string" ? r.asset : undefined,
133
225
  }))
134
226
  .sort((a, b) => b.remaining - a.remaining);
135
227
  const totals = {};
@@ -138,8 +230,10 @@ export async function getQuota(options) {
138
230
  const cur = totals[key] ?? { remaining: 0, batches: 0 };
139
231
  cur.remaining += b.remaining;
140
232
  cur.batches += 1;
233
+ if (selected.network && (!Number.isSafeInteger(cur.remaining) || !Number.isSafeInteger(cur.batches)))
234
+ throw new Error("Solana quota totals exceed safe integer bounds");
141
235
  totals[key] = cur;
142
236
  }
143
- return { agent: address, totals, batches };
237
+ return { agent: selected.address, totals, batches };
144
238
  }
145
239
  //# sourceMappingURL=agentHistory.js.map