@profullstack/coinpay 0.6.7 → 0.6.10

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/bin/coinpay.js CHANGED
@@ -119,6 +119,13 @@ function createClient() {
119
119
  return new CoinPayClient({ apiKey, baseUrl: getBaseUrl() });
120
120
  }
121
121
 
122
+ /**
123
+ * Create unauthenticated client for auth operations
124
+ */
125
+ function createUnauthenticatedClient() {
126
+ return new CoinPayClient({ baseUrl: getBaseUrl() });
127
+ }
128
+
122
129
  /**
123
130
  * Parse command line arguments
124
131
  */
@@ -171,6 +178,11 @@ ${colors.cyan}Commands:${colors.reset}
171
178
  set-url <base-url> Set custom API URL
172
179
  show Show current configuration
173
180
 
181
+ ${colors.bright}auth${colors.reset}
182
+ register Register new merchant account
183
+ login Login to merchant account
184
+ me Show current merchant info
185
+
174
186
  ${colors.bright}payment${colors.reset}
175
187
  create Create a new payment
176
188
  get <id> Get payment details
@@ -241,6 +253,8 @@ ${colors.cyan}Commands:${colors.reset}
241
253
  badge [did] Get embeddable reputation badge URL
242
254
  verify <id> Verify a credential
243
255
  revocations List revoked credentials
256
+ did claim Claim (auto-generate) a new DID
257
+ did me Show your current DID
244
258
  issuer register Register platform issuer (--name, --domain)
245
259
  issuer list List your platform issuers
246
260
  issuer rotate Rotate issuer API key (--id)
@@ -1715,6 +1729,136 @@ async function handleEscrow(subcommand, args, flags) {
1715
1729
  }
1716
1730
  }
1717
1731
 
1732
+ /**
1733
+ * Auth commands
1734
+ */
1735
+ async function handleAuth(subcommand, args, flags) {
1736
+ const config = loadConfig();
1737
+
1738
+ switch (subcommand) {
1739
+ case 'register': {
1740
+ const email = flags.email;
1741
+ const password = flags.password;
1742
+ const name = flags.name;
1743
+
1744
+ if (!email || !password) {
1745
+ print.error('Required: --email <email> --password <password>');
1746
+ print.info('Example: coinpay auth register --email user@example.com --password mysecret --name "My Company"');
1747
+ return;
1748
+ }
1749
+
1750
+ try {
1751
+ const client = createUnauthenticatedClient();
1752
+ const { registerMerchant } = await import('../src/auth.js');
1753
+ const result = await registerMerchant(client, { email, password, name });
1754
+
1755
+ if (result.success) {
1756
+ // Store the JWT token in config
1757
+ config.jwtToken = result.token;
1758
+ saveConfig(config);
1759
+
1760
+ print.success('Registration successful!');
1761
+ print.info(`Merchant ID: ${result.merchant.id}`);
1762
+ print.info(`Email: ${result.merchant.email}`);
1763
+ if (result.merchant.name) {
1764
+ print.info(`Name: ${result.merchant.name}`);
1765
+ }
1766
+ print.success('JWT token saved to config');
1767
+ } else {
1768
+ print.error(result.error || 'Registration failed');
1769
+ }
1770
+
1771
+ if (flags.json) {
1772
+ print.json(result);
1773
+ }
1774
+ } catch (error) {
1775
+ print.error(error.message);
1776
+ }
1777
+ break;
1778
+ }
1779
+
1780
+ case 'login': {
1781
+ const email = flags.email;
1782
+ const password = flags.password;
1783
+
1784
+ if (!email || !password) {
1785
+ print.error('Required: --email <email> --password <password>');
1786
+ print.info('Example: coinpay auth login --email user@example.com --password mysecret');
1787
+ return;
1788
+ }
1789
+
1790
+ try {
1791
+ const client = createUnauthenticatedClient();
1792
+ const { loginMerchant } = await import('../src/auth.js');
1793
+ const result = await loginMerchant(client, { email, password });
1794
+
1795
+ if (result.success) {
1796
+ // Store the JWT token in config
1797
+ config.jwtToken = result.token;
1798
+ saveConfig(config);
1799
+
1800
+ print.success('Login successful!');
1801
+ print.info(`Merchant ID: ${result.merchant.id}`);
1802
+ print.info(`Email: ${result.merchant.email}`);
1803
+ if (result.merchant.name) {
1804
+ print.info(`Name: ${result.merchant.name}`);
1805
+ }
1806
+ print.success('JWT token saved to config');
1807
+ } else {
1808
+ print.error(result.error || 'Login failed');
1809
+ }
1810
+
1811
+ if (flags.json) {
1812
+ print.json(result);
1813
+ }
1814
+ } catch (error) {
1815
+ print.error(error.message);
1816
+ }
1817
+ break;
1818
+ }
1819
+
1820
+ case 'me': {
1821
+ try {
1822
+ // Check if we have a JWT token
1823
+ const jwtToken = config.jwtToken;
1824
+ if (!jwtToken) {
1825
+ print.error('Not authenticated. Run: coinpay auth login');
1826
+ return;
1827
+ }
1828
+
1829
+ // Create client with JWT token instead of API key
1830
+ const client = new CoinPayClient({ apiKey: jwtToken, baseUrl: getBaseUrl() });
1831
+ const { getMe } = await import('../src/auth.js');
1832
+ const result = await getMe(client);
1833
+
1834
+ print.success('Current merchant:');
1835
+ print.info(`ID: ${result.id}`);
1836
+ print.info(`Email: ${result.email}`);
1837
+ if (result.name) {
1838
+ print.info(`Name: ${result.name}`);
1839
+ }
1840
+ print.info(`Created: ${result.created_at}`);
1841
+
1842
+ if (flags.json) {
1843
+ print.json(result);
1844
+ }
1845
+ } catch (error) {
1846
+ if (error.message.includes('401') || error.message.includes('403')) {
1847
+ print.error('Authentication failed. Please run: coinpay auth login');
1848
+ } else {
1849
+ print.error(error.message);
1850
+ }
1851
+ }
1852
+ break;
1853
+ }
1854
+
1855
+ default:
1856
+ print.error(`Unknown auth command: ${subcommand}`);
1857
+ print.info('Available: register, login, me');
1858
+ process.exit(1);
1859
+ }
1860
+ }
1861
+
1718
1862
  /**
1719
1863
  * Reputation commands
1720
1864
  */
@@ -1896,6 +2040,21 @@ async function handleReputation(subcommand, args, flags) {
1896
2040
  print.error(result.error || 'Failed to link DID');
1897
2041
  }
1898
2042
 
2043
+ if (flags.json) print.json(result);
2044
+ } else if (didSubcommand === 'me') {
2045
+ // Show current DID
2046
+ const { getMyDid } = await import('../src/reputation.js');
2047
+ const result = await getMyDid(client);
2048
+
2049
+ if (result.did) {
2050
+ print.success(`Your DID: ${result.did}`);
2051
+ console.log(` Public Key: ${result.public_key}`);
2052
+ console.log(` Verified: ${result.verified}`);
2053
+ console.log(` Created: ${result.created_at}`);
2054
+ } else {
2055
+ print.info('No DID found. Run: coinpay reputation did claim');
2056
+ }
2057
+
1899
2058
  if (flags.json) print.json(result);
1900
2059
  } else {
1901
2060
  // Default: show current DID
@@ -2449,9 +2608,17 @@ async function main() {
2449
2608
  await handleCard(subcommand, args, flags);
2450
2609
  break;
2451
2610
 
2611
+ case 'auth':
2612
+ await handleAuth(subcommand, args, flags);
2613
+ break;
2614
+
2452
2615
  case 'reputation':
2453
2616
  await handleReputation(subcommand, args, flags);
2454
2617
  break;
2618
+
2619
+ case 'x402':
2620
+ await handleX402(subcommand, args, flags);
2621
+ break;
2455
2622
 
2456
2623
  default:
2457
2624
  print.error(`Unknown command: ${command}`);
@@ -2467,4 +2634,119 @@ async function main() {
2467
2634
  }
2468
2635
  }
2469
2636
 
2637
+ /**
2638
+ * Handle x402 commands
2639
+ */
2640
+ async function handleX402(subcommand, args, flags) {
2641
+ const config = loadConfig();
2642
+
2643
+ if (!subcommand) {
2644
+ console.log(`
2645
+ ${colors.bright}x402 Payment Protocol${colors.reset}
2646
+
2647
+ Usage: coinpay x402 <command> [options]
2648
+
2649
+ Commands:
2650
+ status Check x402 facilitator status
2651
+ test Test x402 flow against an endpoint
2652
+
2653
+ Options:
2654
+ --url <url> Endpoint URL for testing
2655
+ --network <net> Network: base, ethereum, polygon (default: base)
2656
+ --amount <amt> Amount in USDC smallest unit (default: 1000000 = 1 USDC)
2657
+ `);
2658
+ return;
2659
+ }
2660
+
2661
+ switch (subcommand) {
2662
+ case 'status': {
2663
+ const apiKey = flags['api-key'] || config.apiKey;
2664
+ const baseUrl = flags['base-url'] || config.baseUrl || 'https://coinpayportal.com';
2665
+
2666
+ print.info('Checking x402 facilitator status...');
2667
+
2668
+ try {
2669
+ const response = await fetch(`${baseUrl}/api/x402/verify`, {
2670
+ method: 'POST',
2671
+ headers: {
2672
+ 'Content-Type': 'application/json',
2673
+ ...(apiKey ? { 'X-API-Key': apiKey } : {}),
2674
+ },
2675
+ body: JSON.stringify({ payment: { test: true } }),
2676
+ });
2677
+
2678
+ if (response.status === 401) {
2679
+ print.error('API key required or invalid. Set with: coinpay config set apiKey <key>');
2680
+ } else if (response.status === 400) {
2681
+ print.success('x402 facilitator is reachable and responding');
2682
+ print.info(`Facilitator URL: ${baseUrl}/api/x402`);
2683
+ print.info('Networks: Base, Ethereum, Polygon (Solana coming soon)');
2684
+ } else {
2685
+ print.warn(`Unexpected response: HTTP ${response.status}`);
2686
+ }
2687
+ } catch (err) {
2688
+ print.error(`Cannot reach facilitator at ${baseUrl}: ${err.message}`);
2689
+ }
2690
+ break;
2691
+ }
2692
+
2693
+ case 'test': {
2694
+ const url = flags.url || args[0];
2695
+ if (!url) {
2696
+ print.error('URL required: coinpay x402 test --url http://localhost:3000/api/premium');
2697
+ return;
2698
+ }
2699
+
2700
+ const network = flags.network || 'base';
2701
+ const amount = flags.amount || '1000000';
2702
+
2703
+ print.info(`Testing x402 flow against ${url}`);
2704
+
2705
+ // Step 1: Request without payment
2706
+ try {
2707
+ print.info('Step 1: Requesting resource without payment...');
2708
+ const response = await fetch(url);
2709
+
2710
+ if (response.status === 402) {
2711
+ const body = await response.json();
2712
+ print.success('Received HTTP 402 Payment Required');
2713
+
2714
+ if (body.x402Version) {
2715
+ print.success(`x402 version: ${body.x402Version}`);
2716
+ }
2717
+
2718
+ if (body.accepts && body.accepts.length > 0) {
2719
+ print.success(`${body.accepts.length} payment method(s) offered:`);
2720
+ for (const accept of body.accepts) {
2721
+ const label = accept.extra?.label || `${accept.asset} on ${accept.network}`;
2722
+ const symbol = accept.extra?.assetSymbol || accept.asset || '';
2723
+ print.info(` • ${label}`);
2724
+ print.info(` Amount: ${accept.maxAmountRequired} ${symbol}`);
2725
+ print.info(` Pay to: ${accept.payTo || 'not specified'}`);
2726
+ }
2727
+ const facilitator = body.accepts[0]?.extra?.facilitator;
2728
+ if (facilitator) {
2729
+ print.info(` Facilitator: ${facilitator}`);
2730
+ }
2731
+ }
2732
+
2733
+ print.success('x402 endpoint is correctly configured!');
2734
+ print.warn('Note: Full payment test requires a funded wallet. Use the dashboard for end-to-end testing.');
2735
+ } else if (response.status === 200) {
2736
+ print.warn('Endpoint returned 200 OK — no payment required (is x402 middleware active?)');
2737
+ } else {
2738
+ print.warn(`Unexpected status: HTTP ${response.status}`);
2739
+ }
2740
+ } catch (err) {
2741
+ print.error(`Failed to reach ${url}: ${err.message}`);
2742
+ }
2743
+ break;
2744
+ }
2745
+
2746
+ default:
2747
+ print.error(`Unknown x402 command: ${subcommand}`);
2748
+ print.info('Available: status, test');
2749
+ }
2750
+ }
2751
+
2470
2752
  main().then(() => process.exit(0));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@profullstack/coinpay",
3
- "version": "0.6.7",
3
+ "version": "0.6.10",
4
4
  "description": "CoinPay SDK & CLI — Accept cryptocurrency payments (BTC, ETH, SOL, POL, BCH, USDC) with wallet and swap support",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
package/src/auth.d.ts ADDED
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Auth SDK Module Types
3
+ */
4
+
5
+ import { CoinPayClient } from './client.js';
6
+
7
+ /**
8
+ * Merchant object returned from auth operations
9
+ */
10
+ export interface Merchant {
11
+ id: string;
12
+ email: string;
13
+ name?: string;
14
+ created_at: string;
15
+ updated_at?: string;
16
+ [key: string]: any;
17
+ }
18
+
19
+ /**
20
+ * Auth response for register/login
21
+ */
22
+ export interface AuthResponse {
23
+ success: boolean;
24
+ merchant: Merchant;
25
+ token: string;
26
+ }
27
+
28
+ /**
29
+ * Register params
30
+ */
31
+ export interface RegisterParams {
32
+ email: string;
33
+ password: string;
34
+ name?: string;
35
+ }
36
+
37
+ /**
38
+ * Login params
39
+ */
40
+ export interface LoginParams {
41
+ email: string;
42
+ password: string;
43
+ }
44
+
45
+ /**
46
+ * Register a new merchant account
47
+ * @param client - CoinPay client (can be unauthenticated)
48
+ * @param params - Registration parameters
49
+ * @returns Promise with registration response
50
+ */
51
+ export function registerMerchant(
52
+ client: CoinPayClient,
53
+ params: RegisterParams
54
+ ): Promise<AuthResponse>;
55
+
56
+ /**
57
+ * Login to merchant account
58
+ * @param client - CoinPay client (can be unauthenticated)
59
+ * @param params - Login parameters
60
+ * @returns Promise with login response
61
+ */
62
+ export function loginMerchant(
63
+ client: CoinPayClient,
64
+ params: LoginParams
65
+ ): Promise<AuthResponse>;
66
+
67
+ /**
68
+ * Get current authenticated merchant info
69
+ * @param client - Authenticated CoinPay client
70
+ * @returns Promise with merchant information
71
+ */
72
+ export function getMe(client: CoinPayClient): Promise<Merchant>;
package/src/auth.js ADDED
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Auth SDK Module
3
+ *
4
+ * Authentication functions for registration, login, and merchant management.
5
+ *
6
+ * @example
7
+ * import { CoinPayClient } from '@profullstack/coinpay';
8
+ * import { registerMerchant, loginMerchant, getMe } from '@profullstack/coinpay/auth';
9
+ */
10
+
11
+ /**
12
+ * Register a new merchant account
13
+ * @param {import('./client.js').CoinPayClient} client - CoinPay client (can be unauthenticated)
14
+ * @param {Object} params
15
+ * @param {string} params.email - Merchant email
16
+ * @param {string} params.password - Account password
17
+ * @param {string} [params.name] - Optional merchant name
18
+ * @returns {Promise<Object>} { success: boolean, merchant: Object, token: string }
19
+ */
20
+ export async function registerMerchant(client, { email, password, name }) {
21
+ return client.requestUnauthenticated('/auth/register', {
22
+ method: 'POST',
23
+ body: JSON.stringify({
24
+ email,
25
+ password,
26
+ name,
27
+ }),
28
+ });
29
+ }
30
+
31
+ /**
32
+ * Login to merchant account
33
+ * @param {import('./client.js').CoinPayClient} client - CoinPay client (can be unauthenticated)
34
+ * @param {Object} params
35
+ * @param {string} params.email - Merchant email
36
+ * @param {string} params.password - Account password
37
+ * @returns {Promise<Object>} { success: boolean, merchant: Object, token: string }
38
+ */
39
+ export async function loginMerchant(client, { email, password }) {
40
+ return client.requestUnauthenticated('/auth/login', {
41
+ method: 'POST',
42
+ body: JSON.stringify({
43
+ email,
44
+ password,
45
+ }),
46
+ });
47
+ }
48
+
49
+ /**
50
+ * Get current authenticated merchant info
51
+ * @param {import('./client.js').CoinPayClient} client - Authenticated CoinPay client
52
+ * @returns {Promise<Object>} Merchant information
53
+ */
54
+ export async function getMe(client) {
55
+ return client.request('/auth/me', {
56
+ method: 'POST',
57
+ });
58
+ }
package/src/client.js CHANGED
@@ -3,6 +3,8 @@
3
3
  * Main client class for interacting with the CoinPay API
4
4
  */
5
5
 
6
+ import { LightningClient } from './lightning.js';
7
+
6
8
  const DEFAULT_BASE_URL = 'https://coinpayportal.com/api';
7
9
 
8
10
  /**
@@ -16,18 +18,17 @@ export class CoinPayClient {
16
18
  /**
17
19
  * Create a new CoinPay client
18
20
  * @param {Object} options - Client options
19
- * @param {string} options.apiKey - Your CoinPay API key
21
+ * @param {string} [options.apiKey] - Your CoinPay API key (optional for auth operations)
20
22
  * @param {string} [options.baseUrl] - API base URL (default: https://coinpayportal.com/api)
21
23
  * @param {number} [options.timeout] - Request timeout in ms (default: 30000)
22
24
  */
23
- constructor({ apiKey, baseUrl = DEFAULT_BASE_URL, timeout = 30000 }) {
24
- if (!apiKey) {
25
- throw new Error('API key is required');
26
- }
27
-
25
+ constructor({ apiKey, baseUrl = DEFAULT_BASE_URL, timeout = 30000 } = {}) {
28
26
  this.#apiKey = apiKey;
29
27
  this.#baseUrl = baseUrl.replace(/\/$/, ''); // Remove trailing slash
30
28
  this.#timeout = timeout;
29
+
30
+ /** @type {LightningClient} Lightning/BOLT12 operations */
31
+ this.lightning = new LightningClient(this);
31
32
  }
32
33
 
33
34
  /**
@@ -37,6 +38,10 @@ export class CoinPayClient {
37
38
  * @returns {Promise<Object>} API response
38
39
  */
39
40
  async request(endpoint, options = {}) {
41
+ if (!this.#apiKey) {
42
+ throw new Error('API key is required for authenticated requests. Use requestUnauthenticated() for registration/login.');
43
+ }
44
+
40
45
  const url = `${this.#baseUrl}${endpoint}`;
41
46
  const controller = new AbortController();
42
47
  const timeoutId = setTimeout(() => controller.abort(), this.#timeout);
@@ -72,6 +77,47 @@ export class CoinPayClient {
72
77
  }
73
78
  }
74
79
 
80
+ /**
81
+ * Make an unauthenticated API request (for registration/login)
82
+ * @param {string} endpoint - API endpoint
83
+ * @param {Object} [options] - Fetch options
84
+ * @returns {Promise<Object>} API response
85
+ */
86
+ async requestUnauthenticated(endpoint, options = {}) {
87
+ const url = `${this.#baseUrl}${endpoint}`;
88
+ const controller = new AbortController();
89
+ const timeoutId = setTimeout(() => controller.abort(), this.#timeout);
90
+
91
+ try {
92
+ const response = await fetch(url, {
93
+ ...options,
94
+ signal: controller.signal,
95
+ headers: {
96
+ 'Content-Type': 'application/json',
97
+ ...options.headers,
98
+ },
99
+ });
100
+
101
+ const data = await response.json();
102
+
103
+ if (!response.ok) {
104
+ const error = new Error(data.error || `HTTP ${response.status}`);
105
+ error.status = response.status;
106
+ error.response = data;
107
+ throw error;
108
+ }
109
+
110
+ return data;
111
+ } catch (error) {
112
+ if (error.name === 'AbortError') {
113
+ throw new Error(`Request timeout after ${this.#timeout}ms`);
114
+ }
115
+ throw error;
116
+ } finally {
117
+ clearTimeout(timeoutId);
118
+ }
119
+ }
120
+
75
121
  /**
76
122
  * Create a new payment
77
123
  *
package/src/index.js CHANGED
@@ -88,8 +88,18 @@ import {
88
88
  getBadgeUrl,
89
89
  verifyCredential,
90
90
  getRevocationList,
91
+ claimDid,
92
+ linkDid,
93
+ getMyDid,
91
94
  } from './reputation.js';
92
95
 
96
+ // Auth exports
97
+ import {
98
+ registerMerchant,
99
+ loginMerchant,
100
+ getMe,
101
+ } from './auth.js';
102
+
93
103
  import {
94
104
  SwapClient,
95
105
  SwapCoins,
@@ -137,6 +147,21 @@ import {
137
147
  SubscriptionStatus,
138
148
  } from './subscriptions.js';
139
149
 
150
+ // Lightning
151
+ import { LightningClient } from './lightning.js';
152
+
153
+ // x402 Protocol
154
+ import {
155
+ createX402Middleware,
156
+ verifyX402Payment,
157
+ settleX402Payment,
158
+ buildPaymentRequired,
159
+ convertUsdToAssetAmount,
160
+ PAYMENT_METHODS,
161
+ USDC_CONTRACTS,
162
+ CHAIN_IDS,
163
+ } from './x402.js';
164
+
140
165
  export {
141
166
  // Client
142
167
  CoinPayClient,
@@ -216,8 +241,32 @@ export {
216
241
  submitReceipt,
217
242
  getReputation,
218
243
  getCredential,
244
+ getCredentials,
245
+ getReceipts,
246
+ getBadgeUrl,
219
247
  verifyCredential,
220
248
  getRevocationList,
249
+ claimDid,
250
+ linkDid,
251
+ getMyDid,
252
+
253
+ // Lightning
254
+ LightningClient,
255
+
256
+ // Auth
257
+ registerMerchant,
258
+ loginMerchant,
259
+ getMe,
260
+
261
+ // x402 Protocol
262
+ createX402Middleware,
263
+ verifyX402Payment,
264
+ settleX402Payment,
265
+ buildPaymentRequired,
266
+ convertUsdToAssetAmount,
267
+ PAYMENT_METHODS,
268
+ USDC_CONTRACTS,
269
+ CHAIN_IDS,
221
270
 
222
271
  // Webhook utilities
223
272
  verifyWebhookSignature,
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Lightning Module for CoinPay SDK
3
+ *
4
+ * Manages BOLT12 Lightning Network operations: node provisioning,
5
+ * offer creation, and payment tracking.
6
+ */
7
+
8
+ const DEFAULT_BASE_URL = 'https://coinpayportal.com/api';
9
+
10
+ /**
11
+ * Lightning client for BOLT12 operations
12
+ */
13
+ export class LightningClient {
14
+ #client;
15
+
16
+ /**
17
+ * @param {import('./client.js').CoinPayClient} client - Parent CoinPay client
18
+ */
19
+ constructor(client) {
20
+ this.#client = client;
21
+ }
22
+
23
+ // ──────────────────────────────────────────────
24
+ // Nodes
25
+ // ──────────────────────────────────────────────
26
+
27
+ /**
28
+ * Provision a Greenlight CLN node for a wallet.
29
+ * @param {Object} params
30
+ * @param {string} params.wallet_id - Wallet UUID
31
+ * @param {string} params.mnemonic - BIP39 mnemonic (used server-side to derive LN keys)
32
+ * @param {string} [params.business_id] - Optional business UUID
33
+ * @returns {Promise<Object>} The provisioned node
34
+ */
35
+ async provisionNode({ wallet_id, mnemonic, business_id }) {
36
+ return this.#client.request('/lightning/nodes', {
37
+ method: 'POST',
38
+ body: JSON.stringify({ wallet_id, mnemonic, business_id }),
39
+ });
40
+ }
41
+
42
+ /**
43
+ * Get node status by ID.
44
+ * @param {string} nodeId
45
+ * @returns {Promise<Object>}
46
+ */
47
+ async getNode(nodeId) {
48
+ return this.#client.request(`/lightning/nodes/${nodeId}`);
49
+ }
50
+
51
+ /**
52
+ * Get node by wallet ID.
53
+ * @param {string} walletId - Wallet UUID
54
+ * @returns {Promise<Object>}
55
+ */
56
+ async getNodeByWallet(walletId) {
57
+ return this.#client.request(`/lightning/nodes?wallet_id=${walletId}`);
58
+ }
59
+
60
+ // ──────────────────────────────────────────────
61
+ // Offers
62
+ // ──────────────────────────────────────────────
63
+
64
+ /**
65
+ * Create a BOLT12 offer.
66
+ * @param {Object} params
67
+ * @param {string} params.node_id - Node UUID
68
+ * @param {string} params.description - Human-readable description
69
+ * @param {number} [params.amount_msat] - Fixed amount in millisatoshis (omit for any-amount)
70
+ * @param {string} [params.currency] - Currency code (default: "BTC")
71
+ * @param {string} [params.business_id] - Optional business UUID
72
+ * @returns {Promise<Object>}
73
+ */
74
+ async createOffer({ node_id, description, amount_msat, currency, business_id }) {
75
+ return this.#client.request('/lightning/offers', {
76
+ method: 'POST',
77
+ body: JSON.stringify({ node_id, description, amount_msat, currency, business_id }),
78
+ });
79
+ }
80
+
81
+ /**
82
+ * Get offer by ID.
83
+ * @param {string} offerId
84
+ * @returns {Promise<Object>} Includes offer and qr_uri
85
+ */
86
+ async getOffer(offerId) {
87
+ return this.#client.request(`/lightning/offers/${offerId}`);
88
+ }
89
+
90
+ /**
91
+ * List offers with optional filters.
92
+ * @param {Object} [params]
93
+ * @param {string} [params.business_id]
94
+ * @param {string} [params.node_id]
95
+ * @param {string} [params.status] - "active" | "disabled" | "archived"
96
+ * @param {number} [params.limit] - Default 20
97
+ * @param {number} [params.offset] - Default 0
98
+ * @returns {Promise<Object>} { offers, total, limit, offset }
99
+ */
100
+ async listOffers(params = {}) {
101
+ const qs = new URLSearchParams();
102
+ for (const [k, v] of Object.entries(params)) {
103
+ if (v !== undefined && v !== null) qs.set(k, String(v));
104
+ }
105
+ return this.#client.request(`/lightning/offers?${qs}`);
106
+ }
107
+
108
+ // ──────────────────────────────────────────────
109
+ // Payments
110
+ // ──────────────────────────────────────────────
111
+
112
+ /**
113
+ * List Lightning payments.
114
+ * @param {Object} [params]
115
+ * @param {string} [params.business_id]
116
+ * @param {string} [params.node_id]
117
+ * @param {string} [params.offer_id]
118
+ * @param {string} [params.status] - "pending" | "settled" | "failed"
119
+ * @param {number} [params.limit] - Default 50
120
+ * @param {number} [params.offset] - Default 0
121
+ * @returns {Promise<Object>} { payments, total, limit, offset }
122
+ */
123
+ async listPayments(params = {}) {
124
+ const qs = new URLSearchParams();
125
+ for (const [k, v] of Object.entries(params)) {
126
+ if (v !== undefined && v !== null) qs.set(k, String(v));
127
+ }
128
+ return this.#client.request(`/lightning/payments?${qs}`);
129
+ }
130
+
131
+ /**
132
+ * Send a payment to a BOLT12 offer or invoice.
133
+ * @param {Object} params
134
+ * @param {string} params.node_id - Node UUID
135
+ * @param {string} params.bolt12 - BOLT12 offer or invoice string
136
+ * @param {number} params.amount_sats - Amount in satoshis
137
+ * @returns {Promise<Object>}
138
+ */
139
+ async sendPayment({ node_id, bolt12, amount_sats }) {
140
+ return this.#client.request('/lightning/payments', {
141
+ method: 'POST',
142
+ body: JSON.stringify({ node_id, bolt12, amount_sats }),
143
+ });
144
+ }
145
+
146
+ /**
147
+ * Get payment status by payment hash.
148
+ * @param {string} paymentHash
149
+ * @returns {Promise<Object>}
150
+ */
151
+ async getPayment(paymentHash) {
152
+ return this.#client.request(`/lightning/payments/${paymentHash}`);
153
+ }
154
+ }
155
+
156
+ export default LightningClient;
package/src/x402.js ADDED
@@ -0,0 +1,585 @@
1
+ /**
2
+ * x402 Payment Protocol Support
3
+ *
4
+ * CoinPayPortal as the first multi-chain, multi-asset x402 facilitator.
5
+ * Supports native crypto (BTC, ETH, SOL, POL, BCH), USDC stablecoins,
6
+ * Lightning (BOLT12), and Stripe fiat — all via HTTP 402.
7
+ *
8
+ * @module x402
9
+ */
10
+
11
+ /**
12
+ * All supported payment methods with their network/asset metadata.
13
+ *
14
+ * Each entry defines a scheme the facilitator can advertise in the
15
+ * 402 `accepts` array, letting the buyer choose their preferred method.
16
+ */
17
+ export const PAYMENT_METHODS = {
18
+ // ── Native crypto ──────────────────────────────────────────────
19
+ btc: {
20
+ network: 'bitcoin',
21
+ asset: 'BTC',
22
+ scheme: 'exact',
23
+ decimals: 8,
24
+ label: 'Bitcoin',
25
+ },
26
+ bch: {
27
+ network: 'bitcoin-cash',
28
+ asset: 'BCH',
29
+ scheme: 'exact',
30
+ decimals: 8,
31
+ label: 'Bitcoin Cash',
32
+ },
33
+ eth: {
34
+ network: 'ethereum',
35
+ asset: 'ETH',
36
+ scheme: 'exact',
37
+ decimals: 18,
38
+ chainId: 1,
39
+ label: 'Ethereum',
40
+ },
41
+ pol: {
42
+ network: 'polygon',
43
+ asset: 'POL',
44
+ scheme: 'exact',
45
+ decimals: 18,
46
+ chainId: 137,
47
+ label: 'Polygon',
48
+ },
49
+ sol: {
50
+ network: 'solana',
51
+ asset: 'SOL',
52
+ scheme: 'exact',
53
+ decimals: 9,
54
+ label: 'Solana',
55
+ },
56
+
57
+ // ── USDC stablecoins ──────────────────────────────────────────
58
+ usdc_eth: {
59
+ network: 'ethereum',
60
+ asset: 'USDC',
61
+ scheme: 'exact',
62
+ decimals: 6,
63
+ chainId: 1,
64
+ contractAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
65
+ label: 'USDC on Ethereum',
66
+ },
67
+ usdc_polygon: {
68
+ network: 'polygon',
69
+ asset: 'USDC',
70
+ scheme: 'exact',
71
+ decimals: 6,
72
+ chainId: 137,
73
+ contractAddress: '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359',
74
+ label: 'USDC on Polygon',
75
+ },
76
+ usdc_solana: {
77
+ network: 'solana',
78
+ asset: 'USDC',
79
+ scheme: 'exact',
80
+ decimals: 6,
81
+ contractAddress: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
82
+ label: 'USDC on Solana',
83
+ },
84
+ usdc_base: {
85
+ network: 'base',
86
+ asset: 'USDC',
87
+ scheme: 'exact',
88
+ decimals: 6,
89
+ chainId: 8453,
90
+ contractAddress: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
91
+ label: 'USDC on Base',
92
+ },
93
+
94
+ // ── Lightning ──────────────────────────────────────────────────
95
+ lightning: {
96
+ network: 'lightning',
97
+ asset: 'BTC',
98
+ scheme: 'bolt12',
99
+ decimals: 0, // sats
100
+ label: 'Lightning (BOLT12)',
101
+ },
102
+
103
+ // ── Fiat via Stripe ────────────────────────────────────────────
104
+ stripe: {
105
+ network: 'stripe',
106
+ asset: 'USD',
107
+ scheme: 'stripe-checkout',
108
+ decimals: 2,
109
+ label: 'Card (Stripe)',
110
+ },
111
+ };
112
+
113
+ /**
114
+ * USDC contract addresses by network (convenience re-export)
115
+ */
116
+ export const USDC_CONTRACTS = {
117
+ base: PAYMENT_METHODS.usdc_base.contractAddress,
118
+ ethereum: PAYMENT_METHODS.usdc_eth.contractAddress,
119
+ polygon: PAYMENT_METHODS.usdc_polygon.contractAddress,
120
+ solana: PAYMENT_METHODS.usdc_solana.contractAddress,
121
+ };
122
+
123
+ /**
124
+ * Chain IDs for EVM networks
125
+ */
126
+ export const CHAIN_IDS = {
127
+ base: 8453,
128
+ ethereum: 1,
129
+ polygon: 137,
130
+ };
131
+
132
+ /**
133
+ * Default facilitator URL
134
+ */
135
+ const DEFAULT_FACILITATOR_URL = 'https://coinpayportal.com/api/x402';
136
+
137
+ /**
138
+ * x402 protocol version
139
+ */
140
+ const X402_VERSION = 1;
141
+
142
+ /**
143
+ * All payment method keys
144
+ */
145
+ const ALL_METHOD_KEYS = Object.keys(PAYMENT_METHODS);
146
+
147
+ /**
148
+ * Convert a fiat amount (USD cents or dollars) to the smallest unit for a
149
+ * given payment method, using a rates lookup.
150
+ *
151
+ * @param {number} amountUsd - Amount in USD (e.g. 1.00)
152
+ * @param {string} methodKey - Key from PAYMENT_METHODS
153
+ * @param {Object} rates - Map of asset→USD rate (e.g. { BTC: 65000, ETH: 3500 })
154
+ * @returns {string} Amount in the asset's smallest unit
155
+ */
156
+ export function convertUsdToAssetAmount(amountUsd, methodKey, rates = {}) {
157
+ const method = PAYMENT_METHODS[methodKey];
158
+ if (!method) throw new Error(`Unknown payment method: ${methodKey}`);
159
+
160
+ if (method.asset === 'USD') {
161
+ // Stripe: amount in cents
162
+ return String(Math.round(amountUsd * 100));
163
+ }
164
+ if (method.asset === 'USDC') {
165
+ // USDC: 6 decimals, 1:1 with USD
166
+ return String(Math.round(amountUsd * 1e6));
167
+ }
168
+
169
+ const rate = rates[method.asset];
170
+ if (!rate) throw new Error(`No exchange rate for ${method.asset}`);
171
+
172
+ const assetAmount = amountUsd / rate;
173
+ const smallest = Math.round(assetAmount * Math.pow(10, method.decimals));
174
+ return String(smallest);
175
+ }
176
+
177
+ /**
178
+ * Build a single `accepts` entry for a payment method.
179
+ */
180
+ function buildAcceptEntry(methodKey, { payTo, amount, resource, description, mimeType, maxTimeoutSeconds, facilitatorUrl }) {
181
+ const method = PAYMENT_METHODS[methodKey];
182
+ if (!method) throw new Error(`Unknown payment method: ${methodKey}`);
183
+
184
+ const entry = {
185
+ scheme: method.scheme,
186
+ network: method.network,
187
+ asset: method.contractAddress || method.asset,
188
+ maxAmountRequired: String(amount),
189
+ resource: resource || '',
190
+ description: description || 'Payment required',
191
+ mimeType: mimeType || 'application/json',
192
+ payTo,
193
+ maxTimeoutSeconds: maxTimeoutSeconds || 300,
194
+ extra: {
195
+ facilitator: facilitatorUrl || DEFAULT_FACILITATOR_URL,
196
+ methodKey,
197
+ assetSymbol: method.asset,
198
+ label: method.label,
199
+ },
200
+ };
201
+
202
+ if (method.chainId) {
203
+ entry.extra.chainId = method.chainId;
204
+ }
205
+
206
+ return entry;
207
+ }
208
+
209
+ /**
210
+ * Build a 402 response payload advertising multiple payment options.
211
+ *
212
+ * This is CoinPayPortal's key differentiator: the `accepts` array includes
213
+ * every supported chain and asset, letting the buyer choose.
214
+ *
215
+ * @param {Object} options
216
+ * @param {string} options.payTo - Merchant wallet address (or object mapping network→address)
217
+ * @param {number} options.amountUsd - Price in USD
218
+ * @param {Object} [options.rates] - Exchange rates { BTC: 65000, ETH: 3500, ... }
219
+ * @param {string[]} [options.methods] - Payment method keys to include (default: all)
220
+ * @param {string} [options.resource] - Resource URL
221
+ * @param {string} [options.description] - Human-readable description
222
+ * @param {string} [options.mimeType='application/json'] - Response MIME type
223
+ * @param {number} [options.maxTimeoutSeconds=300] - Payment timeout
224
+ * @param {string} [options.facilitatorUrl] - Custom facilitator URL
225
+ * @returns {Object} x402 payment required response body
226
+ *
227
+ * @example
228
+ * // Advertise all methods
229
+ * const body = buildPaymentRequired({
230
+ * payTo: { ethereum: '0x...', bitcoin: 'bc1...', solana: 'So1...' },
231
+ * amountUsd: 5.00,
232
+ * rates: { BTC: 65000, ETH: 3500, SOL: 150, POL: 0.50, BCH: 350 },
233
+ * });
234
+ *
235
+ * @example
236
+ * // Only accept USDC and Lightning
237
+ * const body = buildPaymentRequired({
238
+ * payTo: '0xMyWallet',
239
+ * amountUsd: 1.00,
240
+ * methods: ['usdc_eth', 'usdc_polygon', 'usdc_base', 'lightning'],
241
+ * });
242
+ */
243
+ export function buildPaymentRequired(options) {
244
+ const {
245
+ payTo,
246
+ amountUsd,
247
+ amount, // legacy: raw amount for a single method
248
+ network, // legacy: single network
249
+ rates = {},
250
+ methods,
251
+ resource,
252
+ description = 'Payment required',
253
+ mimeType = 'application/json',
254
+ maxTimeoutSeconds = 300,
255
+ facilitatorUrl = DEFAULT_FACILITATOR_URL,
256
+ } = options;
257
+
258
+ // Legacy single-method mode (backwards compatible)
259
+ if (network && amount && !methods) {
260
+ const methodKey = _networkToMethodKey(network);
261
+ const addr = typeof payTo === 'object' ? (payTo[network] || Object.values(payTo)[0]) : payTo;
262
+ return {
263
+ x402Version: X402_VERSION,
264
+ accepts: [buildAcceptEntry(methodKey, {
265
+ payTo: addr, amount, resource, description, mimeType, maxTimeoutSeconds, facilitatorUrl,
266
+ })],
267
+ };
268
+ }
269
+
270
+ // Multi-method mode
271
+ const methodKeys = methods || ALL_METHOD_KEYS;
272
+ const accepts = [];
273
+
274
+ for (const key of methodKeys) {
275
+ const method = PAYMENT_METHODS[key];
276
+ if (!method) continue;
277
+
278
+ // Resolve pay-to address for this network
279
+ let addr;
280
+ if (typeof payTo === 'object') {
281
+ addr = payTo[method.network] || payTo[key];
282
+ } else {
283
+ addr = payTo;
284
+ }
285
+ if (!addr) continue; // skip methods where merchant has no address
286
+
287
+ // Convert USD to asset amount
288
+ let assetAmount;
289
+ try {
290
+ if (amountUsd != null) {
291
+ assetAmount = convertUsdToAssetAmount(amountUsd, key, rates);
292
+ } else if (amount) {
293
+ assetAmount = String(amount);
294
+ } else {
295
+ continue;
296
+ }
297
+ } catch {
298
+ // No rate available for this asset — skip it
299
+ continue;
300
+ }
301
+
302
+ accepts.push(buildAcceptEntry(key, {
303
+ payTo: addr, amount: assetAmount, resource, description, mimeType, maxTimeoutSeconds, facilitatorUrl,
304
+ }));
305
+ }
306
+
307
+ if (accepts.length === 0) {
308
+ throw new Error('No payment methods could be built. Check payTo addresses and rates.');
309
+ }
310
+
311
+ return {
312
+ x402Version: X402_VERSION,
313
+ accepts,
314
+ };
315
+ }
316
+
317
+ /**
318
+ * Map a simple network name to the best-guess method key (legacy compat).
319
+ */
320
+ function _networkToMethodKey(network) {
321
+ const map = {
322
+ base: 'usdc_base',
323
+ ethereum: 'eth',
324
+ polygon: 'pol',
325
+ solana: 'sol',
326
+ bitcoin: 'btc',
327
+ 'bitcoin-cash': 'bch',
328
+ lightning: 'lightning',
329
+ stripe: 'stripe',
330
+ };
331
+ return map[network] || network;
332
+ }
333
+
334
+ /**
335
+ * Create Express/Next.js middleware that gates routes behind x402 payments.
336
+ *
337
+ * Returns a function that accepts per-route options and returns middleware.
338
+ * The 402 response advertises ALL supported payment methods by default.
339
+ *
340
+ * @param {Object} globalOptions
341
+ * @param {string} globalOptions.apiKey - CoinPayPortal API key
342
+ * @param {string|Object} globalOptions.payTo - Wallet address(es): string or { network: address }
343
+ * @param {string[]} [globalOptions.methods] - Payment methods to accept (default: all with addresses)
344
+ * @param {Object} [globalOptions.rates] - Exchange rates { BTC: 65000, ... }
345
+ * @param {string} [globalOptions.ratesEndpoint] - URL to fetch live rates (polled periodically)
346
+ * @param {string} [globalOptions.description='Payment required'] - Default description
347
+ * @param {string} [globalOptions.facilitatorUrl] - Custom facilitator URL
348
+ * @param {string} [globalOptions.apiBaseUrl='https://coinpayportal.com'] - CoinPayPortal API base
349
+ * @returns {Function} Middleware factory: (routeOptions) => middleware
350
+ *
351
+ * @example
352
+ * const x402 = createX402Middleware({
353
+ * apiKey: 'cp_live_xxxxx',
354
+ * payTo: {
355
+ * ethereum: '0xAbC...',
356
+ * bitcoin: 'bc1q...',
357
+ * solana: 'So1...',
358
+ * lightning: 'lno1...',
359
+ * stripe: 'acct_xxx',
360
+ * },
361
+ * rates: { BTC: 65000, ETH: 3500, SOL: 150, POL: 0.50, BCH: 350 },
362
+ * });
363
+ *
364
+ * // Charge $5 for premium access — buyer picks their chain
365
+ * app.get('/premium', x402({ amountUsd: 5.00 }), handler);
366
+ */
367
+ export function createX402Middleware(globalOptions) {
368
+ const {
369
+ apiKey,
370
+ payTo,
371
+ methods,
372
+ rates = {},
373
+ description = 'Payment required',
374
+ facilitatorUrl = DEFAULT_FACILITATOR_URL,
375
+ apiBaseUrl = 'https://coinpayportal.com',
376
+ } = globalOptions;
377
+
378
+ if (!apiKey) throw new Error('x402 middleware requires an apiKey');
379
+ if (!payTo) throw new Error('x402 middleware requires a payTo address');
380
+
381
+ // Mutable rates cache — can be updated externally or via ratesEndpoint
382
+ let currentRates = { ...rates };
383
+
384
+ // If ratesEndpoint provided, poll for fresh rates
385
+ if (globalOptions.ratesEndpoint) {
386
+ const fetchRates = async () => {
387
+ try {
388
+ const res = await fetch(globalOptions.ratesEndpoint);
389
+ if (res.ok) currentRates = await res.json();
390
+ } catch { /* ignore */ }
391
+ };
392
+ fetchRates();
393
+ setInterval(fetchRates, 60_000); // refresh every 60s
394
+ }
395
+
396
+ /**
397
+ * Route-level middleware factory.
398
+ *
399
+ * @param {Object} routeOptions
400
+ * @param {number} [routeOptions.amountUsd] - Price in USD (preferred)
401
+ * @param {string} [routeOptions.amount] - Raw amount (legacy, single-method)
402
+ * @param {string} [routeOptions.network] - Single network (legacy)
403
+ * @param {string[]} [routeOptions.methods] - Override accepted methods for this route
404
+ * @param {string} [routeOptions.description] - Route-specific description
405
+ */
406
+ return function x402Route(routeOptions = {}) {
407
+ const routeAmountUsd = routeOptions.amountUsd;
408
+ const routeAmount = routeOptions.amount;
409
+ const routeDescription = routeOptions.description || description;
410
+ const routeMethods = routeOptions.methods || methods;
411
+
412
+ if (!routeAmountUsd && !routeAmount) {
413
+ throw new Error('x402 route requires amountUsd or amount');
414
+ }
415
+
416
+ return async function x402Middleware(req, res, next) {
417
+ const paymentHeader = req.headers['x-payment'] || req.headers['X-Payment'];
418
+
419
+ if (!paymentHeader) {
420
+ const resource = `${req.protocol}://${req.get('host')}${req.originalUrl}`;
421
+ try {
422
+ const body = buildPaymentRequired({
423
+ payTo,
424
+ amountUsd: routeAmountUsd,
425
+ amount: routeAmount,
426
+ network: routeOptions.network,
427
+ rates: currentRates,
428
+ methods: routeMethods,
429
+ resource,
430
+ description: routeDescription,
431
+ facilitatorUrl,
432
+ });
433
+ return res.status(402).json(body);
434
+ } catch (err) {
435
+ return res.status(500).json({ error: 'Failed to build payment options', details: err.message });
436
+ }
437
+ }
438
+
439
+ // Verify the payment
440
+ try {
441
+ const result = await verifyX402Payment(paymentHeader, {
442
+ apiKey,
443
+ apiBaseUrl,
444
+ });
445
+
446
+ if (!result.valid) {
447
+ return res.status(402).json({
448
+ error: 'Invalid payment proof',
449
+ details: result.reason,
450
+ });
451
+ }
452
+
453
+ // Attach payment info to request for downstream use
454
+ req.x402Payment = result.payment;
455
+ next();
456
+ } catch (err) {
457
+ return res.status(500).json({ error: 'Payment verification failed', details: err.message });
458
+ }
459
+ };
460
+ };
461
+ }
462
+
463
+ /**
464
+ * Verify an x402 payment proof.
465
+ *
466
+ * Calls CoinPayPortal's facilitator API to validate the cryptographic
467
+ * signature and payment details in the X-PAYMENT header.
468
+ * Supports all payment schemes: EVM signatures, BTC/BCH tx proofs,
469
+ * Solana tx proofs, Lightning preimages, and Stripe payment intents.
470
+ *
471
+ * @param {string} paymentHeader - The X-PAYMENT header value (base64-encoded JSON)
472
+ * @param {Object} options
473
+ * @param {string} options.apiKey - CoinPayPortal API key
474
+ * @param {string} [options.apiBaseUrl='https://coinpayportal.com'] - API base URL
475
+ * @returns {Promise<{valid: boolean, payment?: Object, reason?: string}>}
476
+ */
477
+ export async function verifyX402Payment(paymentHeader, options = {}) {
478
+ const { apiKey, apiBaseUrl = 'https://coinpayportal.com' } = options;
479
+
480
+ if (!paymentHeader) {
481
+ return { valid: false, reason: 'Missing payment header' };
482
+ }
483
+
484
+ // Decode the payment header
485
+ let payment;
486
+ try {
487
+ const decoded = Buffer.from(paymentHeader, 'base64').toString('utf-8');
488
+ payment = JSON.parse(decoded);
489
+ } catch {
490
+ return { valid: false, reason: 'Invalid payment header encoding' };
491
+ }
492
+
493
+ // Validate required fields
494
+ if (!payment.scheme && !payment.signature && !payment.payload) {
495
+ return { valid: false, reason: 'Missing scheme, signature, or payload in payment proof' };
496
+ }
497
+
498
+ // Check expiry if present
499
+ if (payment.payload?.expiresAt) {
500
+ const expiresAt = typeof payment.payload.expiresAt === 'number'
501
+ ? new Date(payment.payload.expiresAt * 1000)
502
+ : new Date(payment.payload.expiresAt);
503
+ if (expiresAt < new Date()) {
504
+ return { valid: false, reason: 'Payment proof has expired' };
505
+ }
506
+ }
507
+
508
+ // Call CoinPayPortal facilitator to verify
509
+ try {
510
+ const response = await fetch(`${apiBaseUrl}/api/x402/verify`, {
511
+ method: 'POST',
512
+ headers: {
513
+ 'Content-Type': 'application/json',
514
+ ...(apiKey ? { 'X-API-Key': apiKey } : {}),
515
+ },
516
+ body: JSON.stringify({ payment }),
517
+ });
518
+
519
+ const data = await response.json();
520
+
521
+ if (!response.ok) {
522
+ return { valid: false, reason: data.error || 'Verification failed' };
523
+ }
524
+
525
+ return { valid: true, payment: data.payment };
526
+ } catch (err) {
527
+ return { valid: false, reason: `Facilitator error: ${err.message}` };
528
+ }
529
+ }
530
+
531
+ /**
532
+ * Settle an x402 payment on-chain (or via the appropriate payment rail).
533
+ *
534
+ * For crypto: claims the payment on-chain.
535
+ * For Lightning: confirms the preimage.
536
+ * For Stripe: captures the payment intent.
537
+ *
538
+ * @param {string} paymentHeader - The X-PAYMENT header value (base64-encoded JSON)
539
+ * @param {Object} options
540
+ * @param {string} options.apiKey - CoinPayPortal API key
541
+ * @param {string} [options.apiBaseUrl='https://coinpayportal.com'] - API base URL
542
+ * @returns {Promise<{settled: boolean, txHash?: string, network?: string, error?: string}>}
543
+ */
544
+ export async function settleX402Payment(paymentHeader, options = {}) {
545
+ const { apiKey, apiBaseUrl = 'https://coinpayportal.com' } = options;
546
+
547
+ if (!paymentHeader) {
548
+ return { settled: false, error: 'Missing payment header' };
549
+ }
550
+
551
+ let payment;
552
+ try {
553
+ const decoded = Buffer.from(paymentHeader, 'base64').toString('utf-8');
554
+ payment = JSON.parse(decoded);
555
+ } catch {
556
+ return { settled: false, error: 'Invalid payment header encoding' };
557
+ }
558
+
559
+ try {
560
+ const response = await fetch(`${apiBaseUrl}/api/x402/settle`, {
561
+ method: 'POST',
562
+ headers: {
563
+ 'Content-Type': 'application/json',
564
+ ...(apiKey ? { 'X-API-Key': apiKey } : {}),
565
+ },
566
+ body: JSON.stringify({ payment }),
567
+ });
568
+
569
+ const data = await response.json();
570
+
571
+ if (!response.ok) {
572
+ return { settled: false, error: data.error || 'Settlement failed' };
573
+ }
574
+
575
+ return {
576
+ settled: true,
577
+ txHash: data.txHash,
578
+ network: data.network,
579
+ asset: data.asset,
580
+ method: data.method,
581
+ };
582
+ } catch (err) {
583
+ return { settled: false, error: `Settlement error: ${err.message}` };
584
+ }
585
+ }