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.
@@ -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 |