@envarai/envarpay 0.1.0-alpha.2

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/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 EnvarAI contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
package/README.md ADDED
@@ -0,0 +1,158 @@
1
+ # @envarai/envarpay
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.
6
+
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.
10
+
11
+ ## Install and start a wallet
12
+
13
+ Install the npm alpha client:
14
+
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 --prerelease allow envarpay
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:
39
+
40
+ ```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
46
+ ```
47
+
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
+ ```
57
+
58
+ ```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('approved-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: 'approved-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
+ }
96
+ ```
97
+
98
+ Use the alias actually configured in `buyer.toml` (the quickstart generates `seller`).
99
+ `listPaidTools` does not pay. `callPaidTool` may spend within the existing wallet
100
+ policy; `paymentStatus` and `recoverPayment` retain the original purchase.
101
+
102
+ ## Envar discovery and transaction pages
103
+
104
+ With the optional `[connection]` in the wallet, these additional methods read
105
+ Envar's public catalog:
106
+
107
+ ```ts
108
+ const candidates = await wallet.discoverAgents('research');
109
+ console.log(candidates.payment_authorized); // always false
110
+ const profile = await wallet.getAgent('THE_PUBLISHED_AGENT_HANDLE');
111
+ ```
112
+
113
+ The operator still approves and configures a peer before purchasing from it.
114
+ The Python wallet handles optional durable Envar reports; the npm client does not
115
+ duplicate signing, settlement or the platform's payment verification.
116
+ [Complete Envar setup](https://github.com/EnvarAI/EnvarPay/blob/main/docs/envar.md).
117
+
118
+ ## Receiving and asynchronous tasks
119
+
120
+ For **receiving**, put the Python `envarpay` payment gate in front of your existing
121
+ private JS/TS MCP tool and expose only the gate through HTTPS. No buyer-wallet
122
+ client is needed to receive payments; the seller does not need a signing key.
123
+ [Seller quickstart](https://github.com/EnvarAI/EnvarPay/blob/main/docs/selling.md).
124
+
125
+ For **asynchronous escrow/acceptance/refunds**, use Python `envarpay[task]` and
126
+ `envarpay task wallet` with your host's standard MCP client. The task mode is an
127
+ experimental Base Sepolia-only flow, separate from this upfront wallet API.
128
+ [Task guide](https://github.com/EnvarAI/EnvarPay/blob/main/docs/tasks/README.md).
129
+
130
+ ## Errors and recovery
131
+
132
+ Persist the request ID in your application before calling. Never replace it after
133
+ an uncertain result. The client does not retry paid calls; the wallet retains the
134
+ original authorization and ledger. A `WalletToolError` preserves an explicit
135
+ wallet refusal/execution error. An `OperationUnknownError` means the result is
136
+ uncertain, including transport failures and timeouts; it does not mean no payment occurred.
137
+
138
+ HTTPS is required except for loopback HTTP. Redirects are rejected, including
139
+ redirects to another wallet URL. The bearer token belongs to the wallet service,
140
+ not to a seller or public directory; keep it in the application's private config.
141
+
142
+ Targets Node.js >=22.14 and Bun. Local tests use the actual Python wallet MCP
143
+ adapter with a fake service and transfer no funds; they are not real payment evidence.
144
+ This client has no task-escrow API or framework-native plugin manifest.
145
+
146
+ ## Reviewed purchases from Envar
147
+
148
+ Run Python EnvarPay 0.1.0a8 or newer for these methods. `walletPolicy()` reads limits
149
+ and already-approved peers. `callAgent()` selects only a peer bound to the reviewed
150
+ Agent and endpoint IDs; it never adds a directory candidate to wallet policy. Supply
151
+ `expectedNetwork`, `expectedPayTo` and `expectedAmountAtomic` from the terms you
152
+ reviewed. A different live price is refused before signing. Keep `requestId` across
153
+ uncertain outcomes and use the original status/recovery methods.
154
+
155
+ The website can use the same authenticated wallet through its owner-only entry.
156
+ For task creation, result verification and acceptance/refunds, connect the private
157
+ `envarpay task wallet-serve` using a standard MCP client. The task signer and
158
+ contract verification stay in Python; the task mode remains experimental/testnet.
@@ -0,0 +1,95 @@
1
+ import type { CallToolResult, Tool } from '@modelcontextprotocol/sdk/types.js';
2
+ export type Json = null | boolean | number | string | Json[] | {
3
+ [key: string]: Json;
4
+ };
5
+ export interface PaidTools {
6
+ peer: string;
7
+ tools: Tool[];
8
+ }
9
+ export interface AgentDiscovery {
10
+ candidates: Record<string, unknown>[];
11
+ payment_authorized: false;
12
+ }
13
+ export interface WalletPolicy {
14
+ network: string;
15
+ payments_enabled: boolean;
16
+ max_per_call_atomic: number;
17
+ max_total_atomic: number;
18
+ peers: {
19
+ name: string;
20
+ agent_id: string | null;
21
+ endpoint_id: string | null;
22
+ pay_to: string;
23
+ tools: string[];
24
+ }[];
25
+ }
26
+ export interface ReviewedAgentCall {
27
+ agentId: string;
28
+ endpointId: string;
29
+ tool: string;
30
+ arguments: Record<string, Json>;
31
+ requestId: string;
32
+ expectedNetwork: string;
33
+ expectedPayTo: string;
34
+ expectedAmountAtomic: number;
35
+ }
36
+ export interface PaidCall {
37
+ request_id: string;
38
+ payment_made: boolean;
39
+ payment: Record<string, unknown> | null;
40
+ result: CallToolResult;
41
+ }
42
+ export interface PaymentStatus {
43
+ id: string;
44
+ status: string;
45
+ amount: number;
46
+ updated: number;
47
+ transaction: string | null;
48
+ payment_state: string | null;
49
+ execution_state: string | null;
50
+ invocation_id: string | null;
51
+ report_pending: boolean;
52
+ }
53
+ export type RecoveryResult = PaidCall | ({
54
+ request_id: string;
55
+ } & Record<string, unknown>);
56
+ export interface WalletConnection {
57
+ url: string | URL;
58
+ token: string;
59
+ timeoutMs?: number;
60
+ }
61
+ /** A wallet refusal or execution error, preserved from the MCP tool response. */
62
+ export declare class WalletToolError extends Error {
63
+ readonly tool: string;
64
+ readonly response: CallToolResult;
65
+ constructor(tool: string, response: CallToolResult);
66
+ }
67
+ /** The call may have reached the wallet. Inspect/recover this ID before another call. */
68
+ export declare class OperationUnknownError extends Error {
69
+ readonly requestId: string;
70
+ constructor(requestId: string, cause: unknown);
71
+ }
72
+ /** A remote wallet client. Signing, policy and durable recovery stay in the wallet. */
73
+ export declare class WalletClient {
74
+ private readonly client;
75
+ private readonly timeoutMs;
76
+ private constructor();
77
+ static connect(options: WalletConnection): Promise<WalletClient>;
78
+ private call;
79
+ listPaidTools(peer: string): Promise<PaidTools>;
80
+ /** Requires an optional Envar connection in the wallet; discovery grants no payment authority. */
81
+ discoverAgents(query: string): Promise<AgentDiscovery>;
82
+ /** Read a published profile without modifying the wallet's operator-owned peers. */
83
+ getAgent(handle: string): Promise<Record<string, unknown>>;
84
+ callPaidTool(input: {
85
+ peer: string;
86
+ tool: string;
87
+ arguments: Record<string, Json>;
88
+ requestId: string;
89
+ }): Promise<PaidCall>;
90
+ walletPolicy(): Promise<WalletPolicy>;
91
+ callAgent(input: ReviewedAgentCall): Promise<PaidCall>;
92
+ paymentStatus(id: string): Promise<PaymentStatus[]>;
93
+ recoverPayment(id: string): Promise<RecoveryResult>;
94
+ close(): Promise<void>;
95
+ }
package/dist/index.js ADDED
@@ -0,0 +1,161 @@
1
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
2
+ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
3
+ /** A wallet refusal or execution error, preserved from the MCP tool response. */
4
+ export class WalletToolError extends Error {
5
+ tool;
6
+ response;
7
+ constructor(tool, response) {
8
+ super(response.content.filter(c => c.type === 'text').map(c => c.text).join('\n') || 'Wallet tool failed');
9
+ this.tool = tool;
10
+ this.response = response;
11
+ this.name = 'WalletToolError';
12
+ }
13
+ }
14
+ /** The call may have reached the wallet. Inspect/recover this ID before another call. */
15
+ export class OperationUnknownError extends Error {
16
+ requestId;
17
+ constructor(requestId, cause) {
18
+ super(`Operation ${requestId} is unresolved; inspect paymentStatus or recoverPayment with this ID`, { cause });
19
+ this.requestId = requestId;
20
+ this.name = 'OperationUnknownError';
21
+ }
22
+ }
23
+ function requestId(value) {
24
+ if (!/^[A-Za-z0-9_-]{1,100}$/.test(value)) {
25
+ throw new TypeError('requestId must be 1-100 letters, digits, underscores or hyphens');
26
+ }
27
+ }
28
+ function object(value) {
29
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
30
+ }
31
+ function decode(result) {
32
+ if (result.structuredContent !== undefined)
33
+ return result.structuredContent;
34
+ const texts = result.content.filter(c => c.type === 'text');
35
+ if (texts.length !== 1)
36
+ throw new TypeError('Expected a JSON wallet tool response');
37
+ return JSON.parse(texts[0].text);
38
+ }
39
+ /** A remote wallet client. Signing, policy and durable recovery stay in the wallet. */
40
+ export class WalletClient {
41
+ client;
42
+ timeoutMs;
43
+ constructor(client, timeoutMs) {
44
+ this.client = client;
45
+ this.timeoutMs = timeoutMs;
46
+ }
47
+ static async connect(options) {
48
+ const url = new URL(options.url);
49
+ const local = ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname);
50
+ if ((url.protocol !== 'https:' && !(url.protocol === 'http:' && local)) || url.username || url.password || url.hash || url.search) {
51
+ throw new TypeError('Wallet URL must use HTTPS or loopback HTTP, without credentials or a fragment');
52
+ }
53
+ if (!options.token || /[\r\n]/.test(options.token))
54
+ throw new TypeError('A wallet bearer token is required');
55
+ const timeoutMs = options.timeoutMs ?? 300_000;
56
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0)
57
+ throw new TypeError('timeoutMs must be positive');
58
+ const client = new Client({ name: '@envarai/envarpay', version: '0.1.0-alpha.2' });
59
+ const transport = new StreamableHTTPClientTransport(url, {
60
+ requestInit: { headers: { Authorization: `Bearer ${options.token}` }, redirect: 'error' },
61
+ });
62
+ try {
63
+ await client.connect(transport, { timeout: timeoutMs });
64
+ return new WalletClient(client, timeoutMs);
65
+ }
66
+ catch (error) {
67
+ await client.close().catch(() => { });
68
+ throw error;
69
+ }
70
+ }
71
+ async call(tool, args) {
72
+ const response = await this.client.callTool({ name: tool, arguments: args }, undefined, { timeout: this.timeoutMs });
73
+ if (response.isError)
74
+ throw new WalletToolError(tool, response);
75
+ return decode(response);
76
+ }
77
+ async listPaidTools(peer) {
78
+ const value = await this.call('list_paid_tools', { peer });
79
+ if (!object(value) || typeof value.peer !== 'string' || !Array.isArray(value.tools)) {
80
+ throw new TypeError('Invalid paid tools response');
81
+ }
82
+ return value;
83
+ }
84
+ /** Requires an optional Envar connection in the wallet; discovery grants no payment authority. */
85
+ async discoverAgents(query) {
86
+ const value = await this.call('discover_agents', { query });
87
+ if (!object(value) || value.payment_authorized !== false || !Array.isArray(value.candidates) || value.candidates.some(candidate => !object(candidate))) {
88
+ throw new TypeError('Invalid agent discovery response');
89
+ }
90
+ return value;
91
+ }
92
+ /** Read a published profile without modifying the wallet's operator-owned peers. */
93
+ async getAgent(handle) {
94
+ const value = await this.call('get_agent', { handle });
95
+ if (!object(value))
96
+ throw new TypeError('Invalid agent profile response');
97
+ return value;
98
+ }
99
+ async callPaidTool(input) {
100
+ requestId(input.requestId);
101
+ try {
102
+ const value = await this.call('call_paid_tool', {
103
+ peer: input.peer, tool: input.tool, arguments: input.arguments, request_id: input.requestId,
104
+ });
105
+ if (!object(value) || value.request_id !== input.requestId || typeof value.payment_made !== 'boolean' || !object(value.result)) {
106
+ throw new TypeError('Invalid paid call response');
107
+ }
108
+ return value;
109
+ }
110
+ catch (error) {
111
+ if (error instanceof WalletToolError)
112
+ throw error;
113
+ throw new OperationUnknownError(input.requestId, error);
114
+ }
115
+ }
116
+ async walletPolicy() {
117
+ const value = await this.call('wallet_policy', {});
118
+ if (!object(value) || typeof value.network !== 'string' || typeof value.payments_enabled !== 'boolean' || !Array.isArray(value.peers)) {
119
+ throw new TypeError('Invalid wallet policy response');
120
+ }
121
+ return value;
122
+ }
123
+ async callAgent(input) {
124
+ requestId(input.requestId);
125
+ if (!Number.isSafeInteger(input.expectedAmountAtomic) || input.expectedAmountAtomic <= 0)
126
+ throw new TypeError('Review an integer USDC atomic amount');
127
+ try {
128
+ const value = await this.call('call_agent', {
129
+ agent_id: input.agentId, endpoint_id: input.endpointId, tool: input.tool, arguments: input.arguments,
130
+ request_id: input.requestId, expected_network: input.expectedNetwork, expected_pay_to: input.expectedPayTo,
131
+ expected_amount_atomic: input.expectedAmountAtomic,
132
+ });
133
+ if (!object(value) || value.request_id !== input.requestId || typeof value.payment_made !== 'boolean' || !object(value.result))
134
+ throw new TypeError('Invalid paid Agent response');
135
+ return value;
136
+ }
137
+ catch (error) {
138
+ if (error instanceof WalletToolError)
139
+ throw error;
140
+ throw new OperationUnknownError(input.requestId, error);
141
+ }
142
+ }
143
+ async paymentStatus(id) {
144
+ requestId(id);
145
+ const value = await this.call('payment_status', { request_id: id });
146
+ // FastMCP wraps non-object return types under "result" in structuredContent.
147
+ const rows = object(value) ? value.result : value;
148
+ if (!Array.isArray(rows) || rows.some(row => !object(row) || typeof row.id !== 'string' || typeof row.status !== 'string')) {
149
+ throw new TypeError('Invalid payment status response');
150
+ }
151
+ return rows;
152
+ }
153
+ async recoverPayment(id) {
154
+ requestId(id);
155
+ const value = await this.call('recover_payment', { request_id: id });
156
+ if (!object(value) || value.request_id !== id)
157
+ throw new TypeError('Invalid recovery response');
158
+ return value;
159
+ }
160
+ async close() { await this.client.close(); }
161
+ }
package/package.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "@envarai/envarpay",
3
+ "version": "0.1.0-alpha.2",
4
+ "description": "Typed MCP client for an operator-configured EnvarPay wallet service",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "engines": { "node": ">=22.14.0" },
8
+ "exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" } },
9
+ "files": ["dist", "README.md", "LICENSE"],
10
+ "repository": { "type": "git", "url": "git+https://github.com/EnvarAI/EnvarPay.git", "directory": "packages/typescript" },
11
+ "publishConfig": { "access": "public", "tag": "next" },
12
+ "scripts": { "build": "tsc", "test": "npm run build && node --test test/client.test.mjs", "prepack": "npm run build" },
13
+ "dependencies": { "@modelcontextprotocol/sdk": "^1.31.0" },
14
+ "devDependencies": { "typescript": "~5.9.3", "@types/node": "^22.0.0" }
15
+ }