@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.
- package/PAYMENT_RECEIPTS.md +194 -19
- package/README.md +58 -27
- package/dist/agentHistory.d.ts +9 -0
- package/dist/agentHistory.d.ts.map +1 -1
- package/dist/agentHistory.js +110 -16
- package/dist/agentHistory.js.map +1 -1
- package/dist/aifp1.d.ts +124 -17
- package/dist/aifp1.d.ts.map +1 -1
- package/dist/aifp1.js +580 -82
- package/dist/aifp1.js.map +1 -1
- package/dist/deploymentErrors.d.ts +5 -0
- package/dist/deploymentErrors.d.ts.map +1 -0
- package/dist/deploymentErrors.js +8 -0
- package/dist/deploymentErrors.js.map +1 -0
- package/dist/deploymentResolver.d.ts +2 -3
- package/dist/deploymentResolver.d.ts.map +1 -1
- package/dist/deploymentResolver.js +2 -6
- package/dist/deploymentResolver.js.map +1 -1
- package/dist/facilitators/index.d.ts +1 -1
- package/dist/facilitators/index.d.ts.map +1 -1
- package/dist/generated/solanaV14Deployments.generated.d.ts +1 -0
- package/dist/generated/solanaV14Deployments.generated.d.ts.map +1 -1
- package/dist/generated/solanaV14Deployments.generated.js +4 -2
- package/dist/generated/solanaV14Deployments.generated.js.map +1 -1
- package/dist/index.d.ts +10 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -3
- package/dist/index.js.map +1 -1
- package/dist/paymentChains.d.ts +95 -0
- package/dist/paymentChains.d.ts.map +1 -0
- package/dist/paymentChains.js +116 -0
- package/dist/paymentChains.js.map +1 -0
- package/dist/settlement.d.ts +5 -5
- package/dist/settlementSolanaV14.d.ts +135 -0
- package/dist/settlementSolanaV14.d.ts.map +1 -0
- package/dist/settlementSolanaV14.js +733 -0
- package/dist/settlementSolanaV14.js.map +1 -0
- package/dist/settlementV14.d.ts +9 -2
- package/dist/settlementV14.d.ts.map +1 -1
- package/dist/settlementV14.js +81 -14
- package/dist/settlementV14.js.map +1 -1
- package/dist/solanaDeploymentResolver.d.ts +2 -1
- package/dist/solanaDeploymentResolver.d.ts.map +1 -1
- package/dist/solanaDeploymentResolver.js +1 -32
- package/dist/solanaDeploymentResolver.js.map +1 -1
- package/dist/spendLedger.d.ts +74 -33
- package/dist/spendLedger.d.ts.map +1 -1
- package/dist/spendLedger.js +523 -130
- package/dist/spendLedger.js.map +1 -1
- package/dist/unifiedAgent.d.ts +17 -8
- package/dist/unifiedAgent.d.ts.map +1 -1
- package/dist/unifiedAgent.js +227 -42
- package/dist/unifiedAgent.js.map +1 -1
- package/package.json +3 -3
package/PAYMENT_RECEIPTS.md
CHANGED
|
@@ -1,4 +1,121 @@
|
|
|
1
|
-
#
|
|
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
|
|
41
|
-
native
|
|
42
|
-
|
|
43
|
-
|
|
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(
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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`.
|
|
84
|
-
Polygon
|
|
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.
|
|
4
|
-
Polygon
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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: <
|
|
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
|
|
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
|
-
-
|
|
177
|
+
- Payments settle on public chains — the explicitly selected EVM network — and are visible on-chain.
|
|
147
178
|
|
|
148
179
|
## License
|
|
149
180
|
|
package/dist/agentHistory.d.ts
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/agentHistory.js
CHANGED
|
@@ -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 &&
|
|
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
|
-
|
|
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
|
-
|
|
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}
|
|
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) =>
|
|
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
|
|
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
|
-
:
|
|
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
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|