@envarai/envarpay 0.1.0-alpha.3 → 0.2.0-alpha.1

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 (58) hide show
  1. package/README.md +56 -146
  2. package/dist/commerce/buyer-store.d.ts +93 -0
  3. package/dist/commerce/buyer-store.js +168 -0
  4. package/dist/commerce/buyer.d.ts +129 -0
  5. package/dist/commerce/buyer.js +631 -0
  6. package/dist/commerce/card.d.ts +5 -0
  7. package/dist/commerce/card.js +29 -0
  8. package/dist/commerce/chain.d.ts +24 -0
  9. package/dist/commerce/chain.js +146 -0
  10. package/dist/commerce/cli.d.ts +2 -0
  11. package/dist/commerce/cli.js +268 -0
  12. package/dist/commerce/client-runtime.d.ts +8 -0
  13. package/dist/commerce/client-runtime.js +7 -0
  14. package/dist/commerce/config.d.ts +7 -0
  15. package/dist/commerce/config.js +164 -0
  16. package/dist/commerce/envar-runtime.d.ts +46 -0
  17. package/dist/commerce/envar-runtime.js +256 -0
  18. package/dist/commerce/envar.d.ts +126 -0
  19. package/dist/commerce/envar.js +647 -0
  20. package/dist/commerce/http.d.ts +2 -0
  21. package/dist/commerce/http.js +34 -0
  22. package/dist/commerce/index.d.ts +4 -0
  23. package/dist/commerce/index.js +4 -0
  24. package/dist/commerce/management.d.ts +14 -0
  25. package/dist/commerce/management.js +138 -0
  26. package/dist/commerce/mpp-client.d.ts +59 -0
  27. package/dist/commerce/mpp-client.js +24 -0
  28. package/dist/commerce/mpp-runtime.d.ts +3 -0
  29. package/dist/commerce/mpp-runtime.js +3 -0
  30. package/dist/commerce/mpp.d.ts +53 -0
  31. package/dist/commerce/mpp.js +163 -0
  32. package/dist/commerce/peer-proxy.d.ts +44 -0
  33. package/dist/commerce/peer-proxy.js +667 -0
  34. package/dist/commerce/pricing.d.ts +12 -0
  35. package/dist/commerce/pricing.js +60 -0
  36. package/dist/commerce/runtime.d.ts +8 -0
  37. package/dist/commerce/runtime.js +8 -0
  38. package/dist/commerce/server.d.ts +58 -0
  39. package/dist/commerce/server.js +485 -0
  40. package/dist/commerce/store.d.ts +86 -0
  41. package/dist/commerce/store.js +466 -0
  42. package/dist/commerce/stripe-provider.d.ts +9 -0
  43. package/dist/commerce/stripe-provider.js +97 -0
  44. package/dist/commerce/types.d.ts +113 -0
  45. package/dist/commerce/types.js +8 -0
  46. package/dist/commerce/upstream.d.ts +6 -0
  47. package/dist/commerce/upstream.js +103 -0
  48. package/dist/commerce/vault.d.ts +8 -0
  49. package/dist/commerce/vault.js +55 -0
  50. package/dist/commerce/x402.d.ts +37 -0
  51. package/dist/commerce/x402.js +174 -0
  52. package/examples/buyer-policy.json +18 -0
  53. package/examples/envar-integration-policy.json +18 -0
  54. package/examples/seller.json +52 -0
  55. package/package.json +75 -10
  56. package/schemas/buyer-policy.schema.json +148 -0
  57. package/schemas/envarpay.config.schema.json +376 -0
  58. package/schemas/quote.schema.json +91 -0
package/README.md CHANGED
@@ -1,159 +1,69 @@
1
- # @envarai/envarpay
1
+ # EnvarPay TypeScript
2
2
 
3
- A TypeScript client for an existing **authenticated EnvarPay wallet MCP service**.
4
- The client does not install Python, run a signer, hold wallet keys, or change wallet
5
- policy. Use the Python CLI or a separately operated service for those responsibilities.
3
+ Independent service commerce for user-operated Agents. Public communication uses
4
+ A2A 1.0; payments use native x402 v2 exact USDC or MPP Stripe charge. Prices and
5
+ scope are versioned configuration, enforced before dispatch, not instructions in
6
+ a prompt. Envar is an optional directory and management UI.
6
7
 
7
- This client uses the alpha release channel. See
8
- [installation channels](https://github.com/EnvarAI/EnvarPay/blob/main/docs/packages.md)
9
- for the current registry status and available distributions.
8
+ The 0.2 source line replaces the previous npm wallet-only MCP client. Build from
9
+ this checkout while the release is undergoing acceptance; a source version is
10
+ not evidence that its npm/OCI artifact is published.
10
11
 
11
- ## Install and start a wallet
12
+ ## Runtime and entry points
12
13
 
13
- Install the npm alpha client:
14
+ Node.js 22.14+ (Node 24 recommended). SQLite is single-instance. Contracts can be
15
+ read without loading the seller or buyer runtime; Bun validation covers the
16
+ contracts entry, not the SQLite server.
14
17
 
15
- ```sh
16
- npm install @envarai/envarpay@next
17
- ```
18
-
19
- If your project uses a custom package registry, route this scope to the public
20
- registry in its `.npmrc`: `@envarai:registry=https://registry.npmjs.org/`.
21
-
22
- Node.js >=22.14 and Bun are supported. Import this package from an ESM application.
23
- The npm library is a client; the wallet runs as a separate service:
24
-
25
- ```sh
26
- uv tool install --python 3.13 envarpay==0.1.0a10
27
- envarpay init --agent mcp --role buyer --directory ./buyer \
28
- --peer-url https://seller.example/mcp --pay-to SELLER_FULL_RECEIVING_ADDRESS \
29
- --tool ask_agent --max-per-call 0.01 --budget 0.05
30
- envarpay keygen --output ./buyer/buyer.key
31
- ```
32
-
33
- The generated policy uses Base Sepolia test USDC and payments off. Fund the
34
- dedicated test wallet, review the recipient/tool/limits and enable payments only
35
- for your intended purchases. The agent never receives the wallet key.
36
-
37
- Create the wallet-access credential in your private operator environment. This
38
- command never prints it and refuses to overwrite an existing file:
18
+ | Entry | Purpose |
19
+ |---|---|
20
+ | `@envarai/envarpay` | Config types/validation, integer pricing, quotes and A2A Cards |
21
+ | `/server` | Durable seller A2A handler, exact x402 gate and native upstream executor |
22
+ | `/client` | Buyer policy, atomic budgets, original-operation recovery and private management |
23
+ | `/mpp` | Opt-in native MPP seller and read-only Stripe reconciliation |
24
+ | `/envar` | Optional bounded config pull/ack and durable observation reports |
39
25
 
40
26
  ```sh
41
- python3 - <<'PY'
42
- import os, secrets
43
- with os.fdopen(os.open('buyer/wallet-service.token', os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600), 'w') as f:
44
- f.write(secrets.token_urlsafe(32))
45
- PY
27
+ npm ci --ignore-scripts
28
+ npm run build
29
+ node dist/commerce/cli.js init --directory ./private
30
+ node dist/commerce/cli.js validate --config ./private/seller.json
46
31
  ```
47
32
 
48
- Add this table to the existing `buyer.toml`:
49
-
50
- ```toml
51
- [wallet_server]
52
- host = "127.0.0.1"
53
- port = 4021
54
- allowed_hosts = ["127.0.0.1:*", "localhost:*"]
55
- bearer_token_file = "./wallet-service.token"
56
- ```
33
+ Initialization writes examples and a vault key; it never creates a purchase or
34
+ funds a wallet. Replace example addresses/hosts and review service limits before
35
+ starting. Keep Agent model credentials in the Agent, and signer keys in a separate
36
+ private wallet boundary.
57
37
 
58
38
  ```sh
59
- envarpay doctor --config ./buyer/buyer.toml
60
- envarpay wallet-serve --config ./buyer/buyer.toml
61
- ```
62
-
63
- For local development, connect to `http://127.0.0.1:4021/mcp`. For a separate wallet
64
- permission boundary, run it under another OS identity/container or on a private
65
- service with HTTPS; only the wallet receives its key, policy and ledger. Keep the
66
- wallet service private. Give the client its wallet-access token, not a seller's
67
- token or an Envar machine credential.
68
-
69
- ## Discover tools and make a purchase
70
-
71
- ```ts
72
- import { WalletClient, OperationUnknownError } from '@envarai/envarpay';
73
-
74
- const wallet = await WalletClient.connect({
75
- url: 'https://wallet.example/mcp',
76
- token: process.env.ENVARPAY_WALLET_TOKEN!,
77
- });
78
- try {
79
- const tools = await wallet.listPaidTools('seller');
80
- console.log(tools.tools.map(tool => tool.name));
81
- // This may pay within the wallet operator's existing allowlist and budget.
82
- const result = await wallet.callPaidTool({
83
- peer: 'seller', tool: 'ask_agent',
84
- arguments: { question: 'Review my API design' }, requestId: 'review-001',
85
- });
86
- console.log(result.result.content);
87
- } catch (error) {
88
- if (error instanceof OperationUnknownError) {
89
- console.log(await wallet.paymentStatus(error.requestId));
90
- // When the operator-approved seller supports recovery:
91
- // await wallet.recoverPayment(error.requestId);
92
- } else throw error;
93
- } finally {
94
- await wallet.close();
95
- }
39
+ node dist/commerce/cli.js serve --config seller.json \
40
+ --credentials private/seller-auth.json --state private/seller.sqlite3 \
41
+ --origin https://seller.example --port 4020
42
+ node dist/commerce/cli.js buyer-serve --config buyer-policy.json \
43
+ --credentials private/buyer-auth.json --state private/buyer.sqlite3 \
44
+ --origin https://buyer-management.example --port 4021
96
45
  ```
97
46
 
98
- The example uses the `seller` alias generated by the quickstart. If you changed it,
99
- use the alias actually configured in `buyer.toml`.
100
- `listPaidTools` does not pay. `callPaidTool` may spend within the existing wallet
101
- policy; `paymentStatus` and `recoverPayment` retain the original purchase.
102
-
103
- ## Envar discovery and transaction pages
104
-
105
- With the optional `[connection]` in the wallet, these additional methods read
106
- Envar's public catalog:
107
-
108
- ```ts
109
- const candidates = await wallet.discoverAgents('research');
110
- console.log(candidates.payment_authorized); // always false
111
- const profile = await wallet.getAgent('THE_PUBLISHED_AGENT_HANDLE');
112
- ```
113
-
114
- The operator still approves and configures a peer before purchasing from it.
115
- The Python wallet handles optional durable Envar reports; the npm client does not
116
- duplicate signing, settlement or the platform's payment verification.
117
- [Complete Envar setup](https://github.com/EnvarAI/EnvarPay/blob/main/docs/envar.md).
118
-
119
- ## Receiving and asynchronous tasks
120
-
121
- For **receiving**, put the Python `envarpay` payment gate in front of your existing
122
- private JS/TS MCP tool and expose only the gate through HTTPS. No buyer-wallet
123
- client is needed to receive payments; the seller does not need a signing key.
124
- [Seller quickstart](https://github.com/EnvarAI/EnvarPay/blob/main/docs/selling.md).
125
-
126
- For **asynchronous escrow/acceptance/refunds**, use Python `envarpay[task]` and
127
- `envarpay task wallet` with your host's standard MCP client. The task mode is an
128
- experimental Base Sepolia-only flow, separate from this upfront wallet API.
129
- [Task guide](https://github.com/EnvarAI/EnvarPay/blob/main/docs/tasks/README.md).
130
-
131
- ## Errors and recovery
132
-
133
- Persist the request ID in your application before calling. Never replace it after
134
- an uncertain result. The client does not retry paid calls; the wallet retains the
135
- original authorization and ledger. A `WalletToolError` preserves an explicit
136
- wallet refusal/execution error. An `OperationUnknownError` means the result is
137
- uncertain, including transport failures and timeouts; it does not mean no payment occurred.
138
-
139
- HTTPS is required except for loopback HTTP. Redirects are rejected, including
140
- redirects to another wallet URL. The bearer token belongs to the wallet service,
141
- not to a seller or public directory; keep it in the application's private config.
142
-
143
- Targets Node.js >=22.14 and Bun. Local tests use the actual Python wallet MCP
144
- adapter with a fake service and transfer no funds; they are not real payment evidence.
145
- This client has no task-escrow API or framework-native plugin manifest.
146
-
147
- ## Reviewed purchases from Envar
148
-
149
- Use the published Python EnvarPay 0.1.0a10 service with this client. `walletPolicy()` reads limits
150
- and already-approved peers. `callAgent()` selects only a peer bound to the reviewed
151
- Agent and endpoint IDs; it never adds a directory candidate to wallet policy. Supply
152
- `expectedNetwork`, `expectedPayTo` and `expectedAmountAtomic` from the terms you
153
- reviewed. A different live price is refused before signing. Keep `requestId` across
154
- uncertain outcomes and use the original status/recovery methods.
155
-
156
- The website can use the same authenticated wallet through its owner-only entry.
157
- For task creation, result verification and acceptance/refunds, connect the private
158
- `envarpay task wallet-serve` using a standard MCP client. The task signer and
159
- contract verification stay in Python; the task mode remains experimental/testnet.
47
+ The management service is private and owner-authenticated. A website may request a
48
+ quote and confirm an already-reviewed purchase; it cannot change peers, recipient
49
+ allowlists, budgets or signing keys. Unknown outcomes retain the original request,
50
+ signature/SPT and reserved budget. Never create a replacement purchase after a
51
+ timeout.
52
+
53
+ [Seller and wire behavior](../../docs/a2a-commerce.md) ·
54
+ [Buyer management](../../docs/a2a-buyer.md) ·
55
+ [Continuation and recovery](../../docs/a2a-runtime-recovery.md) ·
56
+ [Envar configuration integration](../../docs/a2a-envar.md)
57
+
58
+ ## Payment guarantees
59
+
60
+ Payment confirmation, task completion and acceptance are separate facts. The MVP
61
+ charges upfront for a new task; reads and permitted clarification use that same
62
+ purchase. It does not provide escrow, automatic refunds, subscriptions or billing
63
+ for arbitrary internal tools. A2A compatibility alone does not give an Agent a
64
+ wallet or authorize payment.
65
+
66
+ MPP requires an eligible Stripe merchant, a permitted buyer token-creation flow,
67
+ explicit test/live mode and any required customer authentication. Simulator tests
68
+ are not card charges. Provider and chain acceptance must be recorded separately
69
+ from SDK tests and build results.
@@ -0,0 +1,93 @@
1
+ import type { PaymentRequired, PaymentRequirements, SettleResponse } from '@x402/core/types';
2
+ import type { Challenge } from 'mppx';
3
+ import type { MppReceipt } from './mpp-client.js';
4
+ import { type BuyerPolicy, type Input } from './types.js';
5
+ export interface BuyerQuote {
6
+ messageId: string;
7
+ payer: string;
8
+ amount: string;
9
+ currency: string | null;
10
+ recipient: string | null;
11
+ expiresAt: string;
12
+ serviceId: string;
13
+ serviceRevision: number;
14
+ offerId: string;
15
+ inputDigest: string;
16
+ termsDigest: string;
17
+ quoteId: string;
18
+ }
19
+ export interface BuyerRecord {
20
+ id: string;
21
+ caller: string;
22
+ peerId: string;
23
+ cardUrl: string;
24
+ endpoint: string;
25
+ messageId: string;
26
+ fingerprint: string;
27
+ body: string;
28
+ quoteToken: string;
29
+ quote: BuyerQuote;
30
+ input: Input;
31
+ inputSchema: Record<string, unknown>;
32
+ continuations?: {
33
+ messageId: string;
34
+ input: Input;
35
+ body?: string;
36
+ state: 'pending' | 'unknown' | 'resolved';
37
+ beforeDigest: string;
38
+ }[];
39
+ protocol?: 'x402' | 'mpp' | 'free';
40
+ mppChallenge?: Challenge.Challenge;
41
+ tokenOperationId?: string;
42
+ tokenRef?: string;
43
+ mppMode?: 'test' | 'live';
44
+ mppPaymentMethod?: string;
45
+ required?: PaymentRequired;
46
+ requirements?: PaymentRequirements;
47
+ createdAt: string;
48
+ updatedAt: string;
49
+ state: 'previewed' | 'signing' | 'submitted' | 'unknown' | 'completed' | 'failed';
50
+ paymentState: 'quoted' | 'reserved' | 'unknown' | 'confirmed' | 'rejected' | 'not_required';
51
+ executionState: string;
52
+ credentialRef?: string;
53
+ nonce?: string;
54
+ fromBlock?: string;
55
+ receipt?: SettleResponse | MppReceipt;
56
+ task?: Record<string, unknown>;
57
+ result?: unknown;
58
+ errorCode?: string;
59
+ rejectionEvidence?: {
60
+ kind: 'finalized_expired_unused';
61
+ checkedAt: string;
62
+ nonce: string;
63
+ };
64
+ }
65
+ /** One wallet ledger, one process. Unknown authorizations retain cumulative budget. */
66
+ export declare class BuyerStore {
67
+ private readonly db;
68
+ private readonly owner?;
69
+ constructor(path: string);
70
+ close(): void;
71
+ private transaction;
72
+ get(id: string, caller: string): BuyerRecord | undefined;
73
+ assertMessageOwner(caller: string, messageId: string): void;
74
+ find(caller: string, messageId: string): BuyerRecord | undefined;
75
+ insert(value: Omit<BuyerRecord, 'id' | 'createdAt' | 'updatedAt' | 'state' | 'paymentState' | 'executionState'>): BuyerRecord;
76
+ update(id: string, patch: Partial<Pick<BuyerRecord, 'state' | 'paymentState' | 'executionState' | 'credentialRef' | 'nonce' | 'fromBlock' | 'receipt' | 'task' | 'result' | 'errorCode' | 'continuations' | 'tokenOperationId' | 'tokenRef' | 'rejectionEvidence'>>): BuyerRecord;
77
+ pendingTasks(): BuyerRecord[];
78
+ usage(currency: string): {
79
+ reserved: string;
80
+ spent: string;
81
+ };
82
+ reserve(id: string, caller: string, policy: BuyerPolicy): {
83
+ record: BuyerRecord;
84
+ fresh: boolean;
85
+ };
86
+ beginFree(id: string, caller: string): {
87
+ record: BuyerRecord;
88
+ fresh: boolean;
89
+ };
90
+ rejectBeforeSigning(id: string, caller: string, errorCode: string): BuyerRecord;
91
+ rejectExpiredAuthorization(id: string, caller: string): BuyerRecord;
92
+ confirmed(id: string, receipt: SettleResponse | MppReceipt): BuyerRecord;
93
+ }
@@ -0,0 +1,168 @@
1
+ import { DatabaseSync } from 'node:sqlite';
2
+ import { chmodSync, mkdirSync } from 'node:fs';
3
+ import { dirname } from 'node:path';
4
+ import { randomUUID } from 'node:crypto';
5
+ import { atomic } from './config.js';
6
+ import { CommerceError } from './types.js';
7
+ /** One wallet ledger, one process. Unknown authorizations retain cumulative budget. */
8
+ export class BuyerStore {
9
+ db;
10
+ owner;
11
+ constructor(path) {
12
+ if (path !== ':memory:') {
13
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
14
+ this.owner = new DatabaseSync(path + '.owner.sqlite3');
15
+ chmodSync(path + '.owner.sqlite3', 0o600);
16
+ try {
17
+ this.owner.exec('PRAGMA busy_timeout=0; BEGIN EXCLUSIVE');
18
+ }
19
+ catch {
20
+ this.owner.close();
21
+ throw new CommerceError('store_locked', 'Buyer ledger already has an active process');
22
+ }
23
+ }
24
+ try {
25
+ this.db = new DatabaseSync(path);
26
+ if (path !== ':memory:')
27
+ chmodSync(path, 0o600);
28
+ this.db.exec(`PRAGMA journal_mode=WAL; PRAGMA synchronous=FULL; PRAGMA busy_timeout=5000;
29
+ CREATE TABLE IF NOT EXISTS buyer_meta(version INTEGER NOT NULL);
30
+ INSERT INTO buyer_meta SELECT 1 WHERE NOT EXISTS(SELECT 1 FROM buyer_meta);
31
+ CREATE TABLE IF NOT EXISTS buyer_purchases(
32
+ id TEXT PRIMARY KEY, caller TEXT NOT NULL, message_id TEXT NOT NULL,
33
+ currency TEXT NOT NULL, amount TEXT NOT NULL, budget_state TEXT NOT NULL,
34
+ record_json TEXT NOT NULL, UNIQUE(caller,message_id), UNIQUE(message_id)
35
+ );`);
36
+ if (this.db.prepare('SELECT version FROM buyer_meta').get()?.version !== 1)
37
+ throw new CommerceError('store_version', 'Unsupported buyer ledger version');
38
+ // Never resume signing after a crash: no second nonce may be created.
39
+ for (const row of this.db.prepare('SELECT record_json FROM buyer_purchases').all()) {
40
+ const record = JSON.parse(String(row.record_json));
41
+ if (record.state === 'signing' || record.state === 'submitted')
42
+ this.update(record.id, { state: 'unknown', paymentState: ['confirmed', 'not_required'].includes(record.paymentState) ? record.paymentState : 'unknown', errorCode: 'interrupted_purchase' });
43
+ }
44
+ }
45
+ catch (error) {
46
+ this.owner?.close();
47
+ throw error;
48
+ }
49
+ }
50
+ close() { this.db.close(); if (this.owner) {
51
+ this.owner.exec('ROLLBACK');
52
+ this.owner.close();
53
+ } }
54
+ transaction(fn) {
55
+ this.db.exec('BEGIN IMMEDIATE');
56
+ try {
57
+ const result = fn();
58
+ this.db.exec('COMMIT');
59
+ return result;
60
+ }
61
+ catch (error) {
62
+ this.db.exec('ROLLBACK');
63
+ throw error;
64
+ }
65
+ }
66
+ get(id, caller) {
67
+ const row = this.db.prepare('SELECT record_json FROM buyer_purchases WHERE id=? AND caller=?').get(id, caller);
68
+ return row ? JSON.parse(String(row.record_json)) : undefined;
69
+ }
70
+ assertMessageOwner(caller, messageId) {
71
+ const row = this.db.prepare('SELECT caller FROM buyer_purchases WHERE message_id=?').get(messageId);
72
+ if (row && row.caller !== caller)
73
+ throw new CommerceError('purchase_conflict', 'Message ID is already reserved to another caller');
74
+ }
75
+ find(caller, messageId) {
76
+ const row = this.db.prepare('SELECT record_json FROM buyer_purchases WHERE caller=? AND message_id=?').get(caller, messageId);
77
+ return row ? JSON.parse(String(row.record_json)) : undefined;
78
+ }
79
+ insert(value) {
80
+ return this.transaction(() => {
81
+ this.assertMessageOwner(value.caller, value.messageId);
82
+ const prior = this.find(value.caller, value.messageId);
83
+ if (prior) {
84
+ if (prior.fingerprint !== value.fingerprint)
85
+ throw new CommerceError('purchase_conflict', 'Message ID already identifies another purchase');
86
+ return prior;
87
+ }
88
+ const now = new Date().toISOString();
89
+ const record = { ...value, id: randomUUID(), createdAt: now, updatedAt: now, state: 'previewed', paymentState: 'quoted', executionState: 'not_started' };
90
+ this.db.prepare('INSERT INTO buyer_purchases VALUES(?,?,?,?,?,?,?)').run(record.id, record.caller, record.messageId, record.quote.currency ?? '', record.quote.amount, 'none', JSON.stringify(record));
91
+ return record;
92
+ });
93
+ }
94
+ update(id, patch) {
95
+ const row = this.db.prepare('SELECT record_json FROM buyer_purchases WHERE id=?').get(id);
96
+ if (!row)
97
+ throw new CommerceError('purchase_not_found', 'Purchase not found');
98
+ const record = { ...JSON.parse(String(row.record_json)), ...patch, updatedAt: new Date().toISOString() };
99
+ this.db.prepare('UPDATE buyer_purchases SET record_json=? WHERE id=?').run(JSON.stringify(record), id);
100
+ return record;
101
+ }
102
+ pendingTasks() {
103
+ return this.db.prepare('SELECT record_json FROM buyer_purchases').all()
104
+ .map(row => JSON.parse(String(row.record_json)))
105
+ .filter(record => record.task?.id && ['working', 'unknown'].includes(record.executionState));
106
+ }
107
+ usage(currency) {
108
+ let reserved = 0n, spent = 0n;
109
+ for (const row of this.db.prepare("SELECT amount,budget_state FROM buyer_purchases WHERE currency=? AND budget_state!='none'").all(currency)) {
110
+ if (row.budget_state === 'spent')
111
+ spent += BigInt(String(row.amount));
112
+ else
113
+ reserved += BigInt(String(row.amount));
114
+ }
115
+ return { reserved: reserved.toString(), spent: spent.toString() };
116
+ }
117
+ reserve(id, caller, policy) {
118
+ return this.transaction(() => {
119
+ const record = this.get(id, caller);
120
+ if (!record)
121
+ throw new CommerceError('purchase_not_found', 'Purchase not found');
122
+ if (record.state !== 'previewed')
123
+ return { record, fresh: false };
124
+ const limit = policy.budgets.find(b => b.currency === record.quote.currency);
125
+ const usage = this.usage(record.quote.currency ?? '');
126
+ if (!limit || BigInt(usage.reserved) + BigInt(usage.spent) + atomic(record.quote.amount) > atomic(limit.maxTotal))
127
+ throw new CommerceError('budget_exceeded', 'Cumulative wallet budget is exhausted or reserved');
128
+ this.db.prepare("UPDATE buyer_purchases SET budget_state='reserved' WHERE id=?").run(id);
129
+ return { record: this.update(id, { state: 'signing', paymentState: 'reserved' }), fresh: true };
130
+ });
131
+ }
132
+ beginFree(id, caller) {
133
+ return this.transaction(() => {
134
+ const record = this.get(id, caller);
135
+ if (!record || record.protocol !== 'free' || record.quote.amount !== '0' || record.quote.currency !== null || record.quote.recipient !== null)
136
+ throw new CommerceError('free_purchase_binding', 'Invalid original free purchase');
137
+ if (record.state !== 'previewed')
138
+ return { record, fresh: false };
139
+ return { record: this.update(id, { state: 'submitted', paymentState: 'not_required' }), fresh: true };
140
+ });
141
+ }
142
+ rejectBeforeSigning(id, caller, errorCode) {
143
+ return this.transaction(() => {
144
+ const record = this.get(id, caller);
145
+ if (!record || record.state !== 'signing' || record.credentialRef || record.nonce)
146
+ throw new CommerceError('authorization_may_exist', 'Cannot release an existing or ambiguous authorization');
147
+ this.db.prepare("UPDATE buyer_purchases SET budget_state='none' WHERE id=? AND budget_state='reserved'").run(id);
148
+ return this.update(id, { state: 'failed', paymentState: 'rejected', executionState: 'not_started', errorCode });
149
+ });
150
+ }
151
+ rejectExpiredAuthorization(id, caller) {
152
+ return this.transaction(() => {
153
+ const record = this.get(id, caller);
154
+ if (!record || record.protocol === 'mpp' || record.protocol === 'free' || !record.credentialRef || !record.nonce)
155
+ throw new CommerceError('authorization_recovery_required', 'Original x402 authorization is required');
156
+ if (record.paymentState === 'confirmed')
157
+ return record;
158
+ this.db.prepare("UPDATE buyer_purchases SET budget_state='none' WHERE id=? AND budget_state='reserved'").run(id);
159
+ return this.update(id, { state: 'failed', paymentState: 'rejected', errorCode: 'authorization_expired_unused', rejectionEvidence: { kind: 'finalized_expired_unused', checkedAt: new Date().toISOString(), nonce: record.nonce } });
160
+ });
161
+ }
162
+ confirmed(id, receipt) {
163
+ return this.transaction(() => {
164
+ this.db.prepare("UPDATE buyer_purchases SET budget_state='spent' WHERE id=? AND budget_state IN ('reserved','spent')").run(id);
165
+ return this.update(id, { paymentState: 'confirmed', receipt });
166
+ });
167
+ }
168
+ }
@@ -0,0 +1,129 @@
1
+ import type { ClientEvmSigner } from '@x402/evm';
2
+ import type { PaymentPayload, PaymentRequirements, SettleResponse } from '@x402/core/types';
3
+ import { type BuyerPolicy, type Input } from './types.js';
4
+ import { CredentialVault } from './vault.js';
5
+ import { BuyerStore, type BuyerQuote } from './buyer-store.js';
6
+ import { type MppBuyerOptions, type MppReceipt } from './mpp-client.js';
7
+ export interface BuyerPreviewInput {
8
+ cardUrl: string;
9
+ messageId: string;
10
+ input: Input;
11
+ offerId: string;
12
+ }
13
+ export interface BuyerConfirmInput {
14
+ previewId: string;
15
+ quoteToken: string;
16
+ messageId: string;
17
+ }
18
+ export interface CommerceBuyerOptions {
19
+ policy: BuyerPolicy;
20
+ store: BuyerStore;
21
+ vault?: CredentialVault;
22
+ signer?: ClientEvmSigner;
23
+ mpp?: MppBuyerOptions;
24
+ peerTokens: Readonly<Record<string, string>>;
25
+ /** Override sibling /a2a only when the local wallet owner explicitly allows it. */
26
+ peerEndpoints?: Readonly<Record<string, string>>;
27
+ verifyReceipt?: (receipt: SettleResponse, payload: PaymentPayload, requirements: PaymentRequirements) => Promise<boolean>;
28
+ checkpoint?: (requirements: PaymentRequirements) => Promise<string>;
29
+ proveExpiredUnused?: (payload: PaymentPayload, requirements: PaymentRequirements) => Promise<boolean>;
30
+ findOriginalReceipt?: (payload: PaymentPayload, requirements: PaymentRequirements, fromBlock: string) => Promise<SettleResponse | undefined>;
31
+ fetchImpl?: typeof fetch;
32
+ }
33
+ export interface BuyerSnapshot {
34
+ id: string;
35
+ quoteToken: string;
36
+ messageId: string;
37
+ payer: string;
38
+ state: string;
39
+ quote: BuyerQuote;
40
+ paymentState: string;
41
+ executionState: string;
42
+ task?: Record<string, unknown>;
43
+ result?: unknown;
44
+ errorCode?: string;
45
+ continuation?: {
46
+ messageId: string;
47
+ state: 'pending' | 'unknown' | 'resolved';
48
+ input: Input;
49
+ };
50
+ receipt?: SettleResponse | MppReceipt;
51
+ nonce?: string;
52
+ createdAt: string;
53
+ updatedAt: string;
54
+ }
55
+ /** Independent x402 wallet; payment and execution state are deliberately separate. */
56
+ export declare class CommerceBuyer {
57
+ private readonly options;
58
+ readonly policy: BuyerPolicy;
59
+ private readonly fetchImpl;
60
+ private readonly active;
61
+ private timer?;
62
+ private readonly previews;
63
+ constructor(options: CommerceBuyerOptions);
64
+ /** Background observation only; never signs, replays credentials or changes reservations. */
65
+ start(intervalMs?: number): void;
66
+ stop(): Promise<void>;
67
+ policySnapshot(): {
68
+ capabilities: ("free" | "x402" | "mpp")[];
69
+ state: string;
70
+ payer: string | undefined;
71
+ budgets: {
72
+ reserved: string;
73
+ spent: string;
74
+ currency: string;
75
+ maxTotal: string;
76
+ period: "cumulative";
77
+ }[];
78
+ policyVersion: 1;
79
+ paymentsEnabled: boolean;
80
+ approval: "per_purchase" | "within_preapproved_limits";
81
+ peers: ({
82
+ id: string;
83
+ cardUrl: string;
84
+ protocol: "x402" | "mpp";
85
+ currency: string;
86
+ recipient: string;
87
+ maxPerPurchase: string;
88
+ } | {
89
+ id: string;
90
+ cardUrl: string;
91
+ protocol: "free";
92
+ currency: null;
93
+ recipient: null;
94
+ maxPerPurchase: "0";
95
+ })[];
96
+ };
97
+ private peer;
98
+ private endpoint;
99
+ private scopedFetch;
100
+ private record;
101
+ private snapshot;
102
+ get(caller: string, id: string): BuyerSnapshot;
103
+ preview(caller: string, input: BuyerPreviewInput): Promise<BuyerSnapshot>;
104
+ private prepare;
105
+ private prepareFree;
106
+ private replayFree;
107
+ private prepareMpp;
108
+ private mppCredential;
109
+ private purchaseMpp;
110
+ private savedMpp;
111
+ private verifyMpp;
112
+ private replayMpp;
113
+ private recoverMpp;
114
+ private assertCurrentPolicy;
115
+ confirm(caller: string, input: BuyerConfirmInput): Promise<BuyerSnapshot>;
116
+ private exclusive;
117
+ private purchase;
118
+ private saved;
119
+ private verify;
120
+ private task;
121
+ private replay;
122
+ private client;
123
+ private observe;
124
+ continue(caller: string, id: string, input: {
125
+ messageId: string;
126
+ input: Input;
127
+ }): Promise<BuyerSnapshot>;
128
+ recover(caller: string, id: string): Promise<BuyerSnapshot>;
129
+ }