nullpath-mcp 1.2.0 → 1.3.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/.github/workflows/release.yml +43 -0
- package/.releaserc.json +9 -0
- package/README.md +63 -70
- package/SWARM_SPEC.md +75 -0
- package/dist/__tests__/payment.test.d.ts +2 -0
- package/dist/__tests__/payment.test.d.ts.map +1 -0
- package/dist/__tests__/payment.test.js +106 -0
- package/dist/__tests__/payment.test.js.map +1 -0
- package/dist/index.js +144 -23
- package/dist/index.js.map +1 -1
- package/dist/lib/eip3009.d.ts +128 -0
- package/dist/lib/eip3009.d.ts.map +1 -0
- package/dist/lib/eip3009.js +151 -0
- package/dist/lib/eip3009.js.map +1 -0
- package/dist/lib/payment.d.ts +99 -0
- package/dist/lib/payment.d.ts.map +1 -0
- package/dist/lib/payment.js +254 -0
- package/dist/lib/payment.js.map +1 -0
- package/dist/lib/wallet.d.ts +81 -0
- package/dist/lib/wallet.d.ts.map +1 -0
- package/dist/lib/wallet.js +131 -0
- package/dist/lib/wallet.js.map +1 -0
- package/docs/API_DESIGN.md +698 -0
- package/docs/CODE_REVIEW.md +322 -0
- package/package.json +4 -1
- package/src/__tests__/payment.test.ts +126 -0
- package/src/index.ts +160 -27
- package/src/lib/eip3009.ts +201 -0
- package/src/lib/payment.ts +334 -0
- package/src/lib/wallet.ts +164 -0
- package/dist/__tests__/x402.test.d.ts +0 -2
- package/dist/__tests__/x402.test.d.ts.map +0 -1
- package/dist/__tests__/x402.test.js +0 -187
- package/dist/__tests__/x402.test.js.map +0 -1
|
@@ -0,0 +1,698 @@
|
|
|
1
|
+
# x402 Payment Signing API Design
|
|
2
|
+
|
|
3
|
+
> API design for automatic EIP-3009 payment signing in the nullpath MCP client.
|
|
4
|
+
|
|
5
|
+
**Note:** This is a design document. The actual implementation may differ slightly. See the source files for current interfaces.
|
|
6
|
+
|
|
7
|
+
## Overview
|
|
8
|
+
|
|
9
|
+
This document defines the interfaces for three new modules that enable seamless x402 micropayments:
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
src/lib/
|
|
13
|
+
├── wallet.ts # Wallet client setup from env
|
|
14
|
+
├── payment.ts # 402 detection, signing, retry logic
|
|
15
|
+
└── eip3009.ts # EIP-3009 typed data structures
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Type Definitions
|
|
21
|
+
|
|
22
|
+
### Core Types
|
|
23
|
+
|
|
24
|
+
Types are defined within each module file (wallet.ts, payment.ts, eip3009.ts):
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
// Types defined in respective module files
|
|
28
|
+
|
|
29
|
+
import type { Address, Hex } from 'viem';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Wallet configuration derived from environment.
|
|
33
|
+
*/
|
|
34
|
+
export interface WalletConfig {
|
|
35
|
+
/** Private key (hex string with 0x prefix) */
|
|
36
|
+
privateKey: Hex;
|
|
37
|
+
/** Chain ID (8453 for Base mainnet) */
|
|
38
|
+
chainId: number;
|
|
39
|
+
/** USDC contract address for the chain */
|
|
40
|
+
usdcAddress: Address;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Payment requirements parsed from X-PAYMENT-REQUIRED header.
|
|
45
|
+
* Server sends this on 402 response.
|
|
46
|
+
*/
|
|
47
|
+
export interface PaymentRequired {
|
|
48
|
+
/** Recipient wallet address */
|
|
49
|
+
recipient: Address;
|
|
50
|
+
/** Payment amount in USDC base units (6 decimals) */
|
|
51
|
+
amount: bigint;
|
|
52
|
+
/** USDC contract address */
|
|
53
|
+
asset: Address;
|
|
54
|
+
/** Network identifier (e.g., "base") */
|
|
55
|
+
network: string;
|
|
56
|
+
/** Unix timestamp - signature valid after */
|
|
57
|
+
validAfter: bigint;
|
|
58
|
+
/** Unix timestamp - signature valid before */
|
|
59
|
+
validBefore: bigint;
|
|
60
|
+
/** Optional: specific nonce from server */
|
|
61
|
+
nonce?: Hex;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Signed payment to send in X-PAYMENT header.
|
|
66
|
+
*/
|
|
67
|
+
export interface PaymentSignature {
|
|
68
|
+
/** EIP-3009 signature */
|
|
69
|
+
signature: Hex;
|
|
70
|
+
/** Payer address (derived from wallet) */
|
|
71
|
+
from: Address;
|
|
72
|
+
/** Recipient address */
|
|
73
|
+
to: Address;
|
|
74
|
+
/** Amount in base units */
|
|
75
|
+
value: string;
|
|
76
|
+
/** Unix timestamp */
|
|
77
|
+
validAfter: string;
|
|
78
|
+
/** Unix timestamp */
|
|
79
|
+
validBefore: string;
|
|
80
|
+
/** Random nonce (32 bytes) */
|
|
81
|
+
nonce: Hex;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Result of a payment-aware API call.
|
|
86
|
+
*/
|
|
87
|
+
export interface PaymentResult<T = unknown> {
|
|
88
|
+
/** Response data on success */
|
|
89
|
+
data?: T;
|
|
90
|
+
/** Whether payment was made */
|
|
91
|
+
paid: boolean;
|
|
92
|
+
/** Payment details if paid */
|
|
93
|
+
payment?: {
|
|
94
|
+
amount: string;
|
|
95
|
+
recipient: Address;
|
|
96
|
+
txHash?: Hex;
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Error Types
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
// src/lib/errors.ts
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Base error for all payment-related failures.
|
|
108
|
+
*/
|
|
109
|
+
export class PaymentError extends Error {
|
|
110
|
+
constructor(
|
|
111
|
+
message: string,
|
|
112
|
+
public readonly code: PaymentErrorCode,
|
|
113
|
+
public readonly cause?: unknown
|
|
114
|
+
) {
|
|
115
|
+
super(message);
|
|
116
|
+
this.name = 'PaymentError';
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export enum PaymentErrorCode {
|
|
121
|
+
/** NULLPATH_WALLET_KEY not set */
|
|
122
|
+
WALLET_NOT_CONFIGURED = 'WALLET_NOT_CONFIGURED',
|
|
123
|
+
/** Private key format invalid */
|
|
124
|
+
INVALID_PRIVATE_KEY = 'INVALID_PRIVATE_KEY',
|
|
125
|
+
/** Could not parse X-PAYMENT-REQUIRED header */
|
|
126
|
+
INVALID_PAYMENT_HEADER = 'INVALID_PAYMENT_HEADER',
|
|
127
|
+
/** Signature generation failed */
|
|
128
|
+
SIGNING_FAILED = 'SIGNING_FAILED',
|
|
129
|
+
/** Payment was made but server still rejected */
|
|
130
|
+
PAYMENT_REJECTED = 'PAYMENT_REJECTED',
|
|
131
|
+
/** Network mismatch (e.g., server wants Sepolia, client on Base) */
|
|
132
|
+
NETWORK_MISMATCH = 'NETWORK_MISMATCH',
|
|
133
|
+
/** Insufficient balance (optional: if we add balance checks) */
|
|
134
|
+
INSUFFICIENT_BALANCE = 'INSUFFICIENT_BALANCE',
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Type guard for PaymentError.
|
|
139
|
+
*/
|
|
140
|
+
export function isPaymentError(error: unknown): error is PaymentError {
|
|
141
|
+
return error instanceof PaymentError;
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Module APIs
|
|
148
|
+
|
|
149
|
+
### 1. wallet.ts - Wallet Client Setup
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
// src/lib/wallet.ts
|
|
153
|
+
|
|
154
|
+
import type { WalletClient, Account, Chain } from 'viem';
|
|
155
|
+
import type { WalletConfig } from './types';
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Environment variable name for wallet private key.
|
|
159
|
+
*/
|
|
160
|
+
export const WALLET_KEY_ENV = 'NULLPATH_WALLET_KEY';
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Supported chain configurations.
|
|
164
|
+
*/
|
|
165
|
+
export const SUPPORTED_CHAINS = {
|
|
166
|
+
base: {
|
|
167
|
+
chainId: 8453,
|
|
168
|
+
usdcAddress: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913' as const,
|
|
169
|
+
},
|
|
170
|
+
'base-sepolia': {
|
|
171
|
+
chainId: 84532,
|
|
172
|
+
usdcAddress: '0x036CbD53842c5426634e7929541eC2318f3dCF7e' as const,
|
|
173
|
+
},
|
|
174
|
+
} as const;
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Load wallet configuration from environment.
|
|
178
|
+
*
|
|
179
|
+
* @throws {PaymentError} If NULLPATH_WALLET_KEY is not set or invalid
|
|
180
|
+
* @returns Wallet configuration
|
|
181
|
+
*
|
|
182
|
+
* @example
|
|
183
|
+
* ```typescript
|
|
184
|
+
* const config = getWalletConfig();
|
|
185
|
+
* // { privateKey: '0x...', chainId: 8453, usdcAddress: '0x833...' }
|
|
186
|
+
* ```
|
|
187
|
+
*/
|
|
188
|
+
export function getWalletConfig(): WalletConfig;
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Check if wallet is configured (non-throwing).
|
|
192
|
+
*
|
|
193
|
+
* @returns true if NULLPATH_WALLET_KEY is set
|
|
194
|
+
*/
|
|
195
|
+
export function isWalletConfigured(): boolean;
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Create a viem wallet client for signing.
|
|
199
|
+
*
|
|
200
|
+
* @param config - Wallet configuration (from getWalletConfig)
|
|
201
|
+
* @returns Configured wallet client
|
|
202
|
+
*
|
|
203
|
+
* @example
|
|
204
|
+
* ```typescript
|
|
205
|
+
* const config = getWalletConfig();
|
|
206
|
+
* const wallet = createWalletClient(config);
|
|
207
|
+
* const address = wallet.account.address; // '0x...'
|
|
208
|
+
* ```
|
|
209
|
+
*/
|
|
210
|
+
export function createWallet(config: WalletConfig): WalletClient;
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Get the account address from a private key.
|
|
214
|
+
* Useful for display without creating full wallet client.
|
|
215
|
+
*
|
|
216
|
+
* @param privateKey - Hex private key
|
|
217
|
+
* @returns Account with address
|
|
218
|
+
*/
|
|
219
|
+
export function getAccount(privateKey: Hex): Account;
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### 2. eip3009.ts - EIP-3009 Typed Data
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
// src/lib/eip3009.ts
|
|
226
|
+
|
|
227
|
+
import type { Address, Hex, TypedDataDomain } from 'viem';
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* EIP-3009 TransferWithAuthorization typed data.
|
|
231
|
+
* Used for gasless USDC transfers.
|
|
232
|
+
*/
|
|
233
|
+
export interface TransferWithAuthorizationMessage {
|
|
234
|
+
from: Address;
|
|
235
|
+
to: Address;
|
|
236
|
+
value: bigint;
|
|
237
|
+
validAfter: bigint;
|
|
238
|
+
validBefore: bigint;
|
|
239
|
+
nonce: Hex;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Get the EIP-712 domain for USDC on a specific chain.
|
|
244
|
+
*
|
|
245
|
+
* @param chainId - Target chain ID
|
|
246
|
+
* @param usdcAddress - USDC contract address
|
|
247
|
+
* @returns EIP-712 domain separator
|
|
248
|
+
*
|
|
249
|
+
* @example
|
|
250
|
+
* ```typescript
|
|
251
|
+
* const domain = getUSDCDomain(8453, '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913');
|
|
252
|
+
* // { name: 'USD Coin', version: '2', chainId: 8453n, verifyingContract: '0x833...' }
|
|
253
|
+
* ```
|
|
254
|
+
*/
|
|
255
|
+
export function getUSDCDomain(chainId: number, usdcAddress: Address): TypedDataDomain;
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* EIP-712 type definitions for TransferWithAuthorization.
|
|
259
|
+
* Used with viem's signTypedData.
|
|
260
|
+
*/
|
|
261
|
+
export const TRANSFER_WITH_AUTHORIZATION_TYPES: {
|
|
262
|
+
TransferWithAuthorization: readonly [
|
|
263
|
+
{ name: 'from'; type: 'address' },
|
|
264
|
+
{ name: 'to'; type: 'address' },
|
|
265
|
+
{ name: 'value'; type: 'uint256' },
|
|
266
|
+
{ name: 'validAfter'; type: 'uint256' },
|
|
267
|
+
{ name: 'validBefore'; type: 'uint256' },
|
|
268
|
+
{ name: 'nonce'; type: 'bytes32' },
|
|
269
|
+
];
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Generate a random 32-byte nonce for EIP-3009.
|
|
274
|
+
*
|
|
275
|
+
* @returns Random hex nonce
|
|
276
|
+
*/
|
|
277
|
+
export function generateNonce(): Hex;
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Build the complete typed data object for signing.
|
|
281
|
+
*
|
|
282
|
+
* @param domain - EIP-712 domain
|
|
283
|
+
* @param message - Transfer authorization message
|
|
284
|
+
* @returns Complete typed data for signTypedData
|
|
285
|
+
*/
|
|
286
|
+
export function buildTypedData(
|
|
287
|
+
domain: TypedDataDomain,
|
|
288
|
+
message: TransferWithAuthorizationMessage
|
|
289
|
+
): {
|
|
290
|
+
domain: TypedDataDomain;
|
|
291
|
+
types: typeof TRANSFER_WITH_AUTHORIZATION_TYPES;
|
|
292
|
+
primaryType: 'TransferWithAuthorization';
|
|
293
|
+
message: TransferWithAuthorizationMessage;
|
|
294
|
+
};
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
### 3. payment.ts - Payment Flow Orchestration
|
|
298
|
+
|
|
299
|
+
```typescript
|
|
300
|
+
// src/lib/payment.ts
|
|
301
|
+
|
|
302
|
+
import type { Address, Hex } from 'viem';
|
|
303
|
+
import type { PaymentRequired, PaymentSignature, PaymentResult, WalletConfig } from './types';
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Parse the X-PAYMENT-REQUIRED header from a 402 response.
|
|
307
|
+
*
|
|
308
|
+
* @param header - Base64-encoded JSON from X-PAYMENT-REQUIRED
|
|
309
|
+
* @returns Parsed payment requirements
|
|
310
|
+
* @throws {PaymentError} If header is malformed
|
|
311
|
+
*
|
|
312
|
+
* @example
|
|
313
|
+
* ```typescript
|
|
314
|
+
* const header = response.headers.get('X-PAYMENT-REQUIRED');
|
|
315
|
+
* const requirements = parsePaymentRequired(header);
|
|
316
|
+
* // { recipient: '0x...', amount: 10000n, asset: '0x...', ... }
|
|
317
|
+
* ```
|
|
318
|
+
*/
|
|
319
|
+
export function parsePaymentRequired(header: string): PaymentRequired;
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Encode a payment signature for the X-PAYMENT header.
|
|
323
|
+
*
|
|
324
|
+
* @param payment - Signed payment data
|
|
325
|
+
* @returns Base64-encoded JSON for X-PAYMENT header
|
|
326
|
+
*/
|
|
327
|
+
export function encodePaymentHeader(payment: PaymentSignature): string;
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Sign an EIP-3009 TransferWithAuthorization.
|
|
331
|
+
*
|
|
332
|
+
* @param config - Wallet configuration
|
|
333
|
+
* @param requirements - Payment requirements from server
|
|
334
|
+
* @returns Signed payment ready for X-PAYMENT header
|
|
335
|
+
* @throws {PaymentError} If signing fails
|
|
336
|
+
*
|
|
337
|
+
* @example
|
|
338
|
+
* ```typescript
|
|
339
|
+
* const config = getWalletConfig();
|
|
340
|
+
* const requirements = parsePaymentRequired(header);
|
|
341
|
+
* const signed = await signPayment(config, requirements);
|
|
342
|
+
* // { signature: '0x...', from: '0x...', to: '0x...', ... }
|
|
343
|
+
* ```
|
|
344
|
+
*/
|
|
345
|
+
export function signPayment(
|
|
346
|
+
config: WalletConfig,
|
|
347
|
+
requirements: PaymentRequired
|
|
348
|
+
): Promise<PaymentSignature>;
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Make an API call with automatic 402 payment handling.
|
|
352
|
+
*
|
|
353
|
+
* Behavior:
|
|
354
|
+
* 1. Make initial request
|
|
355
|
+
* 2. If 402 received, parse X-PAYMENT-REQUIRED
|
|
356
|
+
* 3. Sign EIP-3009 authorization
|
|
357
|
+
* 4. Retry request with X-PAYMENT header
|
|
358
|
+
* 5. Return result (max 1 retry)
|
|
359
|
+
*
|
|
360
|
+
* @param url - Full API URL
|
|
361
|
+
* @param options - Fetch options (method, body, headers)
|
|
362
|
+
* @param walletConfig - Optional wallet config (uses env if not provided)
|
|
363
|
+
* @returns Response data with payment info
|
|
364
|
+
* @throws {PaymentError} If payment required but wallet not configured
|
|
365
|
+
* @throws {PaymentError} If payment rejected after retry
|
|
366
|
+
*
|
|
367
|
+
* @example
|
|
368
|
+
* ```typescript
|
|
369
|
+
* const result = await apiCallWithPayment('/api/v1/execute', {
|
|
370
|
+
* method: 'POST',
|
|
371
|
+
* body: JSON.stringify({ agentId: '...', input: {...} }),
|
|
372
|
+
* });
|
|
373
|
+
*
|
|
374
|
+
* if (result.paid) {
|
|
375
|
+
* console.log(`Paid ${result.payment.amount} to ${result.payment.recipient}`);
|
|
376
|
+
* }
|
|
377
|
+
* ```
|
|
378
|
+
*/
|
|
379
|
+
export function apiCallWithPayment<T = unknown>(
|
|
380
|
+
url: string,
|
|
381
|
+
options?: RequestInit,
|
|
382
|
+
walletConfig?: WalletConfig
|
|
383
|
+
): Promise<PaymentResult<T>>;
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Check if a response requires payment.
|
|
387
|
+
*
|
|
388
|
+
* @param response - Fetch response
|
|
389
|
+
* @returns true if status is 402
|
|
390
|
+
*/
|
|
391
|
+
export function isPaymentRequired(response: Response): boolean;
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
---
|
|
395
|
+
|
|
396
|
+
## Integration Points in index.ts
|
|
397
|
+
|
|
398
|
+
### Modified apiCall Function
|
|
399
|
+
|
|
400
|
+
```typescript
|
|
401
|
+
// Replace the existing apiCall with payment-aware version
|
|
402
|
+
|
|
403
|
+
import { apiCallWithPayment, isWalletConfigured } from './lib/payment';
|
|
404
|
+
import { PaymentError, PaymentErrorCode } from './lib/errors';
|
|
405
|
+
|
|
406
|
+
async function apiCall<T = unknown>(
|
|
407
|
+
endpoint: string,
|
|
408
|
+
options: RequestInit = {},
|
|
409
|
+
requiresPayment = false
|
|
410
|
+
): Promise<T> {
|
|
411
|
+
const url = `${NULLPATH_API_URL}${endpoint}`;
|
|
412
|
+
|
|
413
|
+
// For endpoints that might require payment
|
|
414
|
+
if (requiresPayment) {
|
|
415
|
+
const result = await apiCallWithPayment<T>(url, {
|
|
416
|
+
...options,
|
|
417
|
+
headers: {
|
|
418
|
+
'Content-Type': 'application/json',
|
|
419
|
+
...options.headers,
|
|
420
|
+
},
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
// Log payment for user visibility (optional)
|
|
424
|
+
if (result.paid && result.payment) {
|
|
425
|
+
console.error(`[nullpath] Paid ${result.payment.amount} USDC to ${result.payment.recipient}`);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return result.data as T;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Standard call for free endpoints
|
|
432
|
+
const response = await fetch(url, {
|
|
433
|
+
...options,
|
|
434
|
+
headers: {
|
|
435
|
+
'Content-Type': 'application/json',
|
|
436
|
+
...options.headers,
|
|
437
|
+
},
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
if (!response.ok) {
|
|
441
|
+
const error = await response.text();
|
|
442
|
+
throw new Error(`API error (${response.status}): ${error}`);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return response.json() as Promise<T>;
|
|
446
|
+
}
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
### Updated Tool Handlers
|
|
450
|
+
|
|
451
|
+
```typescript
|
|
452
|
+
// handleExecuteAgent - now with payment
|
|
453
|
+
|
|
454
|
+
async function handleExecuteAgent(args: {
|
|
455
|
+
agentId: string;
|
|
456
|
+
capabilityId: string;
|
|
457
|
+
input: unknown
|
|
458
|
+
}) {
|
|
459
|
+
// Check wallet early for better error message
|
|
460
|
+
if (!isWalletConfigured()) {
|
|
461
|
+
return {
|
|
462
|
+
error: 'Wallet not configured',
|
|
463
|
+
code: PaymentErrorCode.WALLET_NOT_CONFIGURED,
|
|
464
|
+
help: 'Set NULLPATH_WALLET_KEY environment variable with your private key (0x...)',
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
try {
|
|
469
|
+
return await apiCall('/execute', {
|
|
470
|
+
method: 'POST',
|
|
471
|
+
body: JSON.stringify({
|
|
472
|
+
targetAgentId: args.agentId,
|
|
473
|
+
capabilityId: args.capabilityId,
|
|
474
|
+
input: args.input,
|
|
475
|
+
}),
|
|
476
|
+
}, true); // requiresPayment = true
|
|
477
|
+
} catch (error) {
|
|
478
|
+
if (isPaymentError(error)) {
|
|
479
|
+
return {
|
|
480
|
+
error: error.message,
|
|
481
|
+
code: error.code,
|
|
482
|
+
help: getPaymentErrorHelp(error.code),
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
throw error;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// handleRegisterAgent - now with payment
|
|
490
|
+
|
|
491
|
+
async function handleRegisterAgent(args: {
|
|
492
|
+
name: string;
|
|
493
|
+
description: string;
|
|
494
|
+
wallet: string;
|
|
495
|
+
capabilities: unknown[];
|
|
496
|
+
endpoint: string;
|
|
497
|
+
}) {
|
|
498
|
+
if (!isWalletConfigured()) {
|
|
499
|
+
return {
|
|
500
|
+
error: 'Wallet not configured',
|
|
501
|
+
code: PaymentErrorCode.WALLET_NOT_CONFIGURED,
|
|
502
|
+
help: 'Registration costs $0.10 USDC. Set NULLPATH_WALLET_KEY to proceed.',
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
try {
|
|
507
|
+
return await apiCall('/agents', {
|
|
508
|
+
method: 'POST',
|
|
509
|
+
body: JSON.stringify(args),
|
|
510
|
+
}, true);
|
|
511
|
+
} catch (error) {
|
|
512
|
+
if (isPaymentError(error)) {
|
|
513
|
+
return {
|
|
514
|
+
error: error.message,
|
|
515
|
+
code: error.code,
|
|
516
|
+
help: getPaymentErrorHelp(error.code),
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
throw error;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Helper for user-friendly error messages
|
|
524
|
+
function getPaymentErrorHelp(code: PaymentErrorCode): string {
|
|
525
|
+
switch (code) {
|
|
526
|
+
case PaymentErrorCode.WALLET_NOT_CONFIGURED:
|
|
527
|
+
return 'Set NULLPATH_WALLET_KEY in your Claude Desktop config.';
|
|
528
|
+
case PaymentErrorCode.INVALID_PRIVATE_KEY:
|
|
529
|
+
return 'Private key must be a 64-character hex string starting with 0x.';
|
|
530
|
+
case PaymentErrorCode.PAYMENT_REJECTED:
|
|
531
|
+
return 'Payment signature was rejected. Check wallet has USDC on Base.';
|
|
532
|
+
case PaymentErrorCode.NETWORK_MISMATCH:
|
|
533
|
+
return 'Server expects a different network. Check NULLPATH_API_URL.';
|
|
534
|
+
default:
|
|
535
|
+
return 'See https://nullpath.com/docs/payments for troubleshooting.';
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
---
|
|
541
|
+
|
|
542
|
+
## Error Handling Strategy
|
|
543
|
+
|
|
544
|
+
### Layered Error Handling
|
|
545
|
+
|
|
546
|
+
```
|
|
547
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
548
|
+
│ Tool Handler (handleExecuteAgent) │
|
|
549
|
+
│ - Catches PaymentError, returns user-friendly message │
|
|
550
|
+
│ - Re-throws unexpected errors for MCP error response │
|
|
551
|
+
└─────────────────────────────────────────────────────────────┘
|
|
552
|
+
▼
|
|
553
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
554
|
+
│ apiCallWithPayment │
|
|
555
|
+
│ - Throws PaymentError with specific codes │
|
|
556
|
+
│ - Wraps network errors with context │
|
|
557
|
+
└─────────────────────────────────────────────────────────────┘
|
|
558
|
+
▼
|
|
559
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
560
|
+
│ signPayment / parsePaymentRequired │
|
|
561
|
+
│ - Throws PaymentError for signing/parsing failures │
|
|
562
|
+
│ - Preserves original error as cause │
|
|
563
|
+
└─────────────────────────────────────────────────────────────┘
|
|
564
|
+
▼
|
|
565
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
566
|
+
│ viem (signTypedData) │
|
|
567
|
+
│ - Low-level errors wrapped by signPayment │
|
|
568
|
+
└─────────────────────────────────────────────────────────────┘
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
### Error Code to User Message Mapping
|
|
572
|
+
|
|
573
|
+
| Code | User Message | Recovery Action |
|
|
574
|
+
|------|--------------|-----------------|
|
|
575
|
+
| `WALLET_NOT_CONFIGURED` | "Wallet not configured" | Set `NULLPATH_WALLET_KEY` env var |
|
|
576
|
+
| `INVALID_PRIVATE_KEY` | "Invalid private key format" | Check key is 0x + 64 hex chars |
|
|
577
|
+
| `INVALID_PAYMENT_HEADER` | "Server sent invalid payment request" | Report bug to nullpath |
|
|
578
|
+
| `SIGNING_FAILED` | "Could not sign payment" | Check key permissions |
|
|
579
|
+
| `PAYMENT_REJECTED` | "Payment was rejected" | Check USDC balance on Base |
|
|
580
|
+
| `NETWORK_MISMATCH` | "Network mismatch" | Verify API URL matches network |
|
|
581
|
+
|
|
582
|
+
---
|
|
583
|
+
|
|
584
|
+
## Usage Examples
|
|
585
|
+
|
|
586
|
+
### Claude Desktop Config
|
|
587
|
+
|
|
588
|
+
```json
|
|
589
|
+
{
|
|
590
|
+
"mcpServers": {
|
|
591
|
+
"nullpath": {
|
|
592
|
+
"command": "npx",
|
|
593
|
+
"args": ["nullpath-mcp"],
|
|
594
|
+
"env": {
|
|
595
|
+
"NULLPATH_WALLET_KEY": "0x..."
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
### Programmatic Usage
|
|
603
|
+
|
|
604
|
+
```typescript
|
|
605
|
+
import {
|
|
606
|
+
getWalletConfig,
|
|
607
|
+
isWalletConfigured,
|
|
608
|
+
signPayment,
|
|
609
|
+
parsePaymentRequired
|
|
610
|
+
} from 'nullpath-mcp/lib';
|
|
611
|
+
|
|
612
|
+
// Check if ready for payments
|
|
613
|
+
if (!isWalletConfigured()) {
|
|
614
|
+
console.log('Set NULLPATH_WALLET_KEY for paid features');
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// Manual signing (advanced usage)
|
|
618
|
+
const config = getWalletConfig();
|
|
619
|
+
const requirements = parsePaymentRequired(header);
|
|
620
|
+
const signed = await signPayment(config, requirements);
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
---
|
|
624
|
+
|
|
625
|
+
## File Structure After Implementation
|
|
626
|
+
|
|
627
|
+
```
|
|
628
|
+
src/
|
|
629
|
+
├── index.ts # Main MCP server (modified)
|
|
630
|
+
├── lib/
|
|
631
|
+
│ ├── index.ts # Re-exports for clean imports
|
|
632
|
+
│ ├── types.ts # Type definitions
|
|
633
|
+
│ ├── errors.ts # PaymentError class
|
|
634
|
+
│ ├── wallet.ts # Wallet setup
|
|
635
|
+
│ ├── payment.ts # Payment flow
|
|
636
|
+
│ └── eip3009.ts # EIP-3009 typed data
|
|
637
|
+
└── __tests__/
|
|
638
|
+
├── wallet.test.ts
|
|
639
|
+
├── payment.test.ts
|
|
640
|
+
└── eip3009.test.ts
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
---
|
|
644
|
+
|
|
645
|
+
## Implementation Notes
|
|
646
|
+
|
|
647
|
+
### For Blockchain Engineer (wallet.ts, eip3009.ts)
|
|
648
|
+
|
|
649
|
+
1. Use `viem` v2.21+ for all signing operations
|
|
650
|
+
2. `privateKeyToAccount` for deriving address from key
|
|
651
|
+
3. `signTypedData` for EIP-712 signatures
|
|
652
|
+
4. Domain separator must match USDC contract exactly:
|
|
653
|
+
```typescript
|
|
654
|
+
{
|
|
655
|
+
name: 'USD Coin',
|
|
656
|
+
version: '2',
|
|
657
|
+
chainId: 8453n,
|
|
658
|
+
verifyingContract: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'
|
|
659
|
+
}
|
|
660
|
+
```
|
|
661
|
+
5. Nonce is random 32 bytes (not sequential like EIP-2612)
|
|
662
|
+
|
|
663
|
+
### For Backend Engineer (payment.ts)
|
|
664
|
+
|
|
665
|
+
1. Headers are base64-encoded JSON
|
|
666
|
+
2. `X-PAYMENT-REQUIRED` → PaymentRequired (server sends)
|
|
667
|
+
3. `X-PAYMENT` → PaymentSignature (client sends)
|
|
668
|
+
4. Single retry on 402 (no retry loops)
|
|
669
|
+
5. Log payment details to stderr for visibility in Claude Desktop
|
|
670
|
+
|
|
671
|
+
### Testing Strategy
|
|
672
|
+
|
|
673
|
+
1. **Unit tests**: Mock viem signing, test parse/encode functions
|
|
674
|
+
2. **Integration tests**: Use Base Sepolia with test USDC
|
|
675
|
+
3. **E2E tests**: Against nullpath staging with real signatures
|
|
676
|
+
|
|
677
|
+
---
|
|
678
|
+
|
|
679
|
+
## Open Questions
|
|
680
|
+
|
|
681
|
+
1. **Balance checking**: Should we pre-check USDC balance before attempting payment?
|
|
682
|
+
- Pro: Better error messages
|
|
683
|
+
- Con: Extra RPC call, may be stale
|
|
684
|
+
|
|
685
|
+
2. **Transaction hash**: Should client wait for on-chain confirmation?
|
|
686
|
+
- Current design: No, server handles settlement
|
|
687
|
+
- Alternative: Return txHash in PaymentResult for receipt
|
|
688
|
+
|
|
689
|
+
3. **Multi-network**: Support both Base mainnet and Sepolia?
|
|
690
|
+
- Recommend: Auto-detect from NULLPATH_API_URL or separate env var
|
|
691
|
+
|
|
692
|
+
---
|
|
693
|
+
|
|
694
|
+
## Revision History
|
|
695
|
+
|
|
696
|
+
| Date | Author | Changes |
|
|
697
|
+
|------|--------|---------|
|
|
698
|
+
| 2026-02-10 | API Design Agent | Initial design |
|