apinow-sdk 0.12.3 → 0.12.4

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/README.md CHANGED
@@ -1,14 +1,15 @@
1
1
  # ApiNow SDK
2
2
 
3
- A TypeScript SDK for interacting with ApiNow endpoints, supporting Ethereum and Base chains. Designed to work in Node.js, browsers, and edge environments like Cloudflare Workers.
3
+ A TypeScript SDK for interacting with ApiNow endpoints, supporting Ethereum and Base chains. This SDK simplifies payments by automatically handling `402 Payment Required` responses, including on-the-fly token swaps.
4
4
 
5
5
  ## Features
6
6
 
7
- - Multi-chain support (Ethereum, Base)
8
- - Native and ERC20 token transfers
9
- - Environment Agnostic: Uses global `fetch` for broad compatibility.
10
- - Optional RPC URL: Uses public RPCs by default, allows override.
11
- - TypeScript types for better development experience.
7
+ - **Automatic x402 Payments**: Intercepts `402` responses to handle payment flows automatically.
8
+ - **On-the-fly Token Swaps**: If you don't have the required payment token, the SDK can swap a common asset (like ETH, WETH, or USDC) to make the payment, powered by 0x.
9
+ - **Flexible Pricing**: Supports endpoints that require a fixed token amount or a USD equivalent.
10
+ - **Configurable Payment**: Prioritize which tokens you prefer to pay with.
11
+ - **Multi-chain support**: Works with Ethereum and Base.
12
+ - **Node.js Environment**: Designed to work in a Node.js environment.
12
13
 
13
14
  ## Installation
14
15
 
@@ -20,147 +21,132 @@ yarn add apinow-sdk
20
21
 
21
22
  ## Usage
22
23
 
23
- ### Basic Example (Using Default RPCs)
24
+ The primary way to use the SDK is with the `execute` method. It's a single call that handles all the complexity of API payments for you.
24
25
 
25
26
  ```typescript
26
27
  import apiNow from 'apinow-sdk';
27
28
 
29
+ // The API endpoint you want to interact with.
28
30
  const ENDPOINT_URL = 'https://apinow.fun/api/endpoints/your-endpoint';
31
+
32
+ // Your private key, securely stored (e.g., in an environment variable).
29
33
  const YOUR_WALLET_PRIVATE_KEY = '0x...';
30
34
 
31
- // 1. Get endpoint info (payment details)
32
- const info = await apiNow.info(ENDPOINT_URL);
33
- // info will contain: { requiredAmount, walletAddress, httpMethod, tokenAddress, chain }
34
- // Note: chain will be 'eth' or 'base'
35
- console.log('Payment required:', info);
36
-
37
- // 2. Send payment and get the API response in one step
38
- try {
39
- const response = await apiNow.infoBuyResponse(
40
- ENDPOINT_URL,
41
- YOUR_WALLET_PRIVATE_KEY
42
- // Optional: Add RPC URL override here if needed
43
- // Optional: Add options like { fastMode: true } here (Note: fastMode currently has no effect)
44
- );
45
- console.log('API Response:', response);
46
- } catch (error) {
47
- console.error('Operation failed:', error);
35
+ async function main() {
36
+ try {
37
+ // The `execute` method handles everything automatically.
38
+ // If the API requires a payment (402), the SDK will:
39
+ // 1. Find the best token you hold to pay with.
40
+ // 2. If needed, swap a common asset (like ETH or USDC) to the required token.
41
+ // 3. Send the payment transaction.
42
+ // 4. Retry the original request with proof of payment.
43
+ const response = await apiNow.execute(
44
+ ENDPOINT_URL,
45
+ YOUR_WALLET_PRIVATE_KEY,
46
+ { // Optional: request options
47
+ method: 'POST',
48
+ data: { query: 'your-data' }
49
+ }
50
+ );
51
+
52
+ console.log('API Response:', response);
53
+ } catch (error) {
54
+ console.error('Operation failed:', error);
55
+ }
48
56
  }
57
+
58
+ main();
49
59
  ```
50
60
 
51
- ### Providing a Custom RPC URL
61
+ ## How It Works: Automatic Payments
52
62
 
53
- If you need to use a specific RPC node (e.g., for Base):
63
+ When you call `execute`, the SDK makes a request to the endpoint. If the server responds with a `402 Payment Required` status, the SDK automatically performs the following steps:
54
64
 
55
- ```typescript
56
- const CUSTOM_RPC_URL = 'https://mainnet.base.org';
65
+ 1. **Parses Payment Options**: The `402` response contains a list of accepted payment options. This can be a single token (fixed price) or multiple tokens (USD equivalent price, e.g., "$5 of USDC" or "$5 of ETH").
66
+ 2. **Checks Balances**: It checks your wallet balance for each of the accepted payment tokens.
67
+ 3. **Prioritizes Payment**: It attempts to pay using your tokens in a preferred order (default: `['USDC', 'WETH', 'ETH']`).
68
+ 4. **Swaps if Needed**: If you don't have any of the *required* tokens, the SDK will try to swap one of your preferred assets for the required one. For example, it can swap your USDC to pay with a different required token.
69
+ 5. **Pays and Retries**: Once the payment transaction is sent, the SDK automatically retries the original API request, now with proof of payment.
57
70
 
58
- const response = await apiNow.infoBuyResponse(
59
- ENDPOINT_URL,
60
- YOUR_WALLET_PRIVATE_KEY,
61
- CUSTOM_RPC_URL // Provide the RPC URL here
62
- );
63
- ```
71
+ ## Configuration
64
72
 
65
- ### Fast Mode
73
+ You can customize the behavior of the `execute` method with the `opts` and `paymentConfig` parameters.
66
74
 
67
- *Note: `fastMode` is currently included for potential future use but does not affect Ethereum/Base transaction confirmation behavior in this version.*
75
+ ### Request Options (`opts`)
68
76
 
69
- ```typescript
70
- const response = await apiNow.infoBuyResponse(
71
- ENDPOINT_URL,
72
- YOUR_WALLET_PRIVATE_KEY,
73
- undefined, // Use default RPC
74
- { fastMode: true } // Option included, but no effect yet
75
- );
76
- ```
77
-
78
- ### Manual Payment (Separate Steps)
77
+ Passed as the third argument to `execute`. This corresponds to `TxResponseOptions`.
79
78
 
80
- ```typescript
81
- import apiNow from 'apinow-sdk';
82
- import { ethers } from 'ethers';
79
+ - `method`: The HTTP method for your request (e.g., `'GET'`, `'POST'`). Defaults to `'GET'`.
80
+ - `data`: The payload for your request. For `POST` requests, this is the JSON body. For `GET`, it's converted to query parameters.
83
81
 
84
- const ENDPOINT_URL = 'https://apinow.fun/api/endpoints/your-endpoint';
85
- const YOUR_WALLET_PRIVATE_KEY = '0x...';
86
- const YOUR_CUSTOM_RPC_URL = 'https://your-node.com'; // Optional
82
+ ### Payment Configuration (`paymentConfig`)
87
83
 
88
- // 1. Get Info
89
- const info = await apiNow.info(ENDPOINT_URL);
90
- const { requiredAmount, walletAddress, chain, tokenAddress } = info;
84
+ Passed as the fourth argument to `execute`. This corresponds to `X402PaymentConfig`.
91
85
 
92
- // Convert requiredAmount (string) to bigint (wei)
93
- const amountBigInt = BigInt(requiredAmount);
86
+ - `preferredTokens`: An array of token symbols (e.g., `['USDC', 'WETH']`) that you prefer to pay with. The SDK will check your balance of these tokens first.
94
87
 
95
- // 2. Send Payment
96
- const txHash = await apiNow.buy(
97
- walletAddress,
98
- amountBigInt,
88
+ ```typescript
89
+ await apiNow.execute(
90
+ ENDPOINT_URL,
99
91
  YOUR_WALLET_PRIVATE_KEY,
100
- chain, // 'eth' or 'base'
101
- YOUR_CUSTOM_RPC_URL, // Optional: override RPC
102
- tokenAddress // Optional: specify ERC20 token if required
103
- // fastMode param exists but is unused by handler
92
+ { method: 'POST', data: { /* ... */ } }, // opts
93
+ { preferredTokens: ['DAI', 'USDC'] } // paymentConfig
104
94
  );
105
- console.log(`Payment sent: ${txHash}`);
95
+ ```
106
96
 
107
- // 3. Get API Response (add delay as needed)
108
- await new Promise(resolve => setTimeout(resolve, 5000)); // Example 5s delay
97
+ ## Legacy Flow (Backward Compatibility)
109
98
 
110
- const apiResponse = await apiNow.txResponse(
99
+ For backward compatibility, the `infoBuyResponse` method is still available. It performs a less sophisticated multi-step payment process.
100
+
101
+ ```typescript
102
+ const response = await apiNow.infoBuyResponse(
111
103
  ENDPOINT_URL,
112
- txHash
104
+ YOUR_WALLET_PRIVATE_KEY
113
105
  );
114
- console.log('API Response:', apiResponse);
115
106
  ```
116
107
 
117
108
  ## API Reference
118
109
 
119
- ### `info(endpoint: string): Promise<InfoResponse>`
120
-
121
- Gets payment requirement information about an ApiNow endpoint.
122
-
123
- ### `buy(walletAddress: string, amount: bigint, userWalletPrivateKey: string, chain: 'eth' | 'base', rpcUrl?: string, tokenAddress?: string, fastMode?: boolean): Promise<string>`
124
-
125
- Sends the required payment transaction to the specified address.
126
- - `amount`: The required amount in the smallest unit (wei).
127
- - `userWalletPrivateKey`: The private key of the wallet sending the funds.
128
- - `chain`: The blockchain target ('eth' or 'base').
129
- - `rpcUrl` (Optional): Overrides the default public RPC URL.
130
- - `tokenAddress` (Optional): The contract address if paying with an ERC20 token.
131
- - `fastMode` (Optional): Parameter exists but currently has no effect.
132
-
133
- Returns the transaction hash.
134
-
135
- ### `txResponse(endpoint: string, txHash: string, opts?: TxResponseOptions): Promise<any>`
110
+ ### `execute(endpoint, privateKey, opts?, paymentConfig?)`
111
+ Handles a request and its potential payment in a single, automatic call. This is the recommended method.
136
112
 
137
- Fetches the final API response from the endpoint after a successful payment.
138
- - `txHash`: The hash of the payment transaction.
139
- - `opts` (Optional): Options like `{ method: 'POST', data: {...} }`.
113
+ ### `infoBuyResponse(endpoint, privateKey, rpcUrl?, opts?)`
114
+ (Legacy) Combines `info`, `buy`, and `txResponse` into a single call.
140
115
 
141
- Returns the endpoint's API response.
116
+ ### `info(endpoint)`
117
+ (Legacy) Gets payment requirement information from an endpoint.
142
118
 
143
- ### `infoBuyResponse(endpoint: string, userWalletPrivateKey: string, rpcUrl?: string, opts?: TxResponseOptions & { fastMode?: boolean }): Promise<any>`
119
+ ### `buy(walletAddress, amount, privateKey, chain, ...)`
120
+ (Legacy) Sends a payment transaction.
144
121
 
145
- Combines `info`, `buy`, and `txResponse` into a single call.
146
- - `userWalletPrivateKey`: The private key of the wallet sending the funds.
147
- - `rpcUrl` (Optional): Overrides the default public RPC URL for the payment.
148
- - `opts` (Optional): Contains `fastMode` boolean (currently no effect) and any `TxResponseOptions`.
149
-
150
- Returns the final API response.
122
+ ### `txResponse(endpoint, txHash, opts?)`
123
+ (Legacy) Fetches the API response after a payment has been made manually.
151
124
 
152
125
  ## Types
153
126
 
154
127
  ```typescript
155
- // Defined in the SDK
156
- interface InfoResponse {
157
- requiredAmount: string; // Amount in wei (string)
158
- walletAddress: string;
159
- httpMethod: string;
160
- tokenAddress?: string; // ERC20 address
128
+ // Response from a 402 error
129
+ interface X402PaymentInfo {
130
+ challenge: string;
161
131
  chain: 'eth' | 'base';
132
+ recipientAddress: string;
133
+ options: X402PaymentOption[];
134
+ }
135
+
136
+ // A single way to pay
137
+ interface X402PaymentOption {
138
+ tokenAddress: string;
139
+ symbol: string;
140
+ amount: string;
141
+ decimals: number;
162
142
  }
163
143
 
144
+ // Configuration for payments
145
+ interface X402PaymentConfig {
146
+ preferredTokens?: string[];
147
+ }
148
+
149
+ // Options for the API request itself
164
150
  interface TxResponseOptions {
165
151
  method?: string;
166
152
  data?: any;
@@ -172,46 +158,68 @@ interface TxResponseOptions {
172
158
  - **Ethereum:** `https://rpc.ankr.com/eth`
173
159
  - **Base:** `https://mainnet.base.org`
174
160
 
175
- You can override these by providing the `rpcUrl` parameter to `buy` or `infoBuyResponse`.
176
-
177
161
  ## Error Handling
178
162
 
179
163
  The SDK throws descriptive errors for:
180
164
  - Invalid endpoint URLs or configurations.
181
165
  - RPC communication errors.
182
166
  - Transaction signing or sending failures.
183
- - Insufficient funds or token allowances.
184
- - Failures during API response fetching (`txResponse`).
167
+ - Insufficient funds or failure to find a valid swap.
168
+ - Failures during API response fetching.
185
169
 
186
170
  Wrap calls in `try...catch` blocks for robust error handling.
187
171
 
188
172
  ## Compatibility
189
173
 
190
- This SDK uses the standard Web `fetch` API and avoids Node.js-specific modules, making it compatible with:
191
- - Node.js (v18+ recommended for global fetch)
192
- - Browsers (modern)
193
- - Edge environments (Cloudflare Workers, Vercel Edge Functions, etc.)
174
+ This SDK uses `node-fetch`, making it compatible with:
175
+ - Node.js (v18+ recommended)
176
+
177
+ It is NOT directly compatible with browsers or edge environments that do not provide a Node.js-compatible `fetch` API.
194
178
 
195
179
  ## License
196
180
 
197
181
  MIT
198
182
 
199
- Copyright (c) 2024 Your Name
200
-
201
- Permission is hereby granted, free of charge, to any person obtaining a copy
202
- of this software and associated documentation files (the "Software"), to deal
203
- in the Software without restriction, including without limitation the rights
204
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
205
- copies of the Software, and to permit persons to whom the Software is
206
- furnished to do so, subject to the following conditions:
207
-
208
- The above copyright notice and permission notice shall be included in all
209
- copies or substantial portions of the Software.
210
-
211
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
212
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
213
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
214
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
215
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
216
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
217
- SOFTWARE.
183
+ ## Examples
184
+
185
+ This project includes a test server and a test runner to demonstrate various payment scenarios.
186
+
187
+ 1. **Create a `.env` file** in the root of the project and add your wallet's private key:
188
+ ```
189
+ PRIVATE_KEY=your_private_key_here
190
+ ```
191
+
192
+ 2. **Install dependencies:**
193
+ ```bash
194
+ npm install
195
+ ```
196
+
197
+ 3. **Start the test server:**
198
+ The test server simulates an API that requires different types of payments.
199
+ ```bash
200
+ node test/test-server.js
201
+ ```
202
+
203
+ 4. **Run the test runner:**
204
+ In a separate terminal, run the test runner to execute a series of transactions against the test server.
205
+ ```bash
206
+ node test/test-runner.js
207
+ ```
208
+
209
+ This will demonstrate:
210
+ - Paying with USDC
211
+ - Paying with a custom ERC20 token
212
+ - Paying with a token priced in USDC
213
+ - Fallback token payments
214
+ - Handling various error conditions
215
+
216
+ ## Local Development
217
+
218
+ 1. **Build the project:**
219
+ ```bash
220
+ npm run build
221
+ ```
222
+
223
+ This will compile the TypeScript source files into JavaScript in the `dist` directory.
224
+
225
+ ## Contributing
package/dist/index.d.ts CHANGED
@@ -1,6 +1,8 @@
1
+ import { Wallet } from 'ethers';
1
2
  interface TxResponseOptions {
2
3
  method?: string;
3
4
  data?: any;
5
+ signature?: string;
4
6
  }
5
7
  interface InfoResponse {
6
8
  requiredAmount: string;
@@ -10,10 +12,34 @@ interface InfoResponse {
10
12
  chain: 'eth' | 'base';
11
13
  decimals?: number;
12
14
  }
15
+ interface X402PaymentOption {
16
+ tokenAddress: string;
17
+ symbol: string;
18
+ amount?: string;
19
+ usdAmount?: string;
20
+ decimals: number;
21
+ }
22
+ interface X402PaymentInfo {
23
+ challenge: string;
24
+ chain: 'eth' | 'base';
25
+ recipientAddress: string;
26
+ options: X402PaymentOption[];
27
+ }
28
+ interface X402PaymentConfig {
29
+ preferredTokens?: string[];
30
+ swapFromAssets?: {
31
+ symbol: string;
32
+ address: string;
33
+ }[];
34
+ slippagePercentage?: string;
35
+ }
36
+ declare function get0xSwapQuote(buyToken: string, sellToken: string, buyAmount: bigint, buyTokenDecimals: number, takerAddress: string, chain: 'eth' | 'base', slippagePercentage?: string): Promise<any>;
13
37
  declare class ApiNow {
14
38
  private handlers;
15
39
  info(endpoint: string): Promise<InfoResponse>;
16
40
  buy(walletAddress: string, amount: bigint, userWalletPrivateKey: string, chain: 'eth' | 'base', rpcUrl?: string, tokenAddress?: string, fastMode?: boolean): Promise<string>;
41
+ execute(endpoint: string, userWalletPrivateKey: string, opts?: TxResponseOptions, paymentConfig?: X402PaymentConfig): Promise<any>;
42
+ approve(wallet: Wallet, tokenAddress: string, spenderAddress: string, amount: bigint): Promise<string>;
17
43
  txResponse(endpoint: string, txHash: string, opts?: TxResponseOptions): Promise<any>;
18
44
  infoBuyResponse(endpoint: string, userWalletPrivateKey: string, rpcUrl?: string, opts?: TxResponseOptions & {
19
45
  fastMode?: boolean;
@@ -21,4 +47,5 @@ declare class ApiNow {
21
47
  }
22
48
  declare const apiNow: ApiNow;
23
49
  export default apiNow;
24
- export type { InfoResponse, TxResponseOptions };
50
+ export { get0xSwapQuote };
51
+ export type { InfoResponse, TxResponseOptions, X402PaymentInfo, X402PaymentOption, X402PaymentConfig };
package/dist/index.js CHANGED
@@ -1,8 +1,22 @@
1
- import { ethers, Wallet, parseUnits, isAddress } from 'ethers';
2
- import fetch from 'node-fetch'; // Import node-fetch
1
+ console.log('[sdk] SDK v1.1 loaded. This version includes header parsing and extensive logging.');
2
+ import { ethers, Contract, Wallet, JsonRpcProvider, parseUnits, isAddress } from 'ethers';
3
+ import fetch from 'node-fetch'; // Import node-fetch and its RequestInit
3
4
  // Default RPC URLs
4
5
  const DEFAULT_ETH_RPC = 'https://rpc.ankr.com/eth';
5
6
  const DEFAULT_BASE_RPC = 'https://mainnet.base.org';
7
+ const NATIVE_TOKEN_ADDRESS = '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee';
8
+ const CHAIN_CONFIG = {
9
+ '1': {
10
+ USDC: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
11
+ WETH: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
12
+ ETH: NATIVE_TOKEN_ADDRESS
13
+ },
14
+ '8453': {
15
+ USDC: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
16
+ WETH: '0x4200000000000000000000000000000000000006',
17
+ ETH: NATIVE_TOKEN_ADDRESS
18
+ }
19
+ };
6
20
  // --- Helper function for fetch (Keep this) ---
7
21
  async function fetchJson(url, options) {
8
22
  console.error(`fetchJson (using node-fetch): Called with URL: ${url}`);
@@ -96,6 +110,343 @@ async function fetchJson(url, options) {
96
110
  console.error(`fetchJson: Successfully fetched and parsed JSON (first 500 chars): ${JSON.stringify(responseData).substring(0, 500)}`);
97
111
  return responseData;
98
112
  }
113
+ // Helper to check ERC20 token balance
114
+ async function getTokenBalance(provider, ownerAddress, tokenAddress) {
115
+ const abi = ["function balanceOf(address owner) view returns (uint256)"];
116
+ const contract = new Contract(tokenAddress, abi, provider);
117
+ try {
118
+ const balance = await contract.balanceOf(ownerAddress);
119
+ return balance;
120
+ }
121
+ catch (e) {
122
+ console.error(`[sdk] Could not get balance for token ${tokenAddress} (this is expected for invalid tokens in fallback tests). Error: ${e instanceof Error ? e.message : String(e)}`);
123
+ return 0n; // Return 0 if the token address is invalid or another error occurs
124
+ }
125
+ }
126
+ // Helper to check ERC20 token allowance
127
+ async function checkAllowance(provider, ownerAddress, tokenAddress, spenderAddress) {
128
+ const abi = ["function allowance(address owner, address spender) view returns (uint256)"];
129
+ const contract = new Contract(tokenAddress, abi, provider);
130
+ const allowance = await contract.allowance(ownerAddress, spenderAddress);
131
+ return allowance;
132
+ }
133
+ // New helper for x402 flow
134
+ async function fetchWithX402(url, options, api, // Pass in the ApiNow instance
135
+ userWalletPrivateKey, paymentConfig = {}) {
136
+ const originalResponse = await fetch(url, options);
137
+ if (originalResponse.status !== 402) {
138
+ if (!originalResponse.ok) {
139
+ const errorBody = await originalResponse.text();
140
+ throw new Error(`HTTP Error ${originalResponse.status}: ${errorBody}`);
141
+ }
142
+ return originalResponse.json();
143
+ }
144
+ console.error("402 Payment Required. Handling payment...");
145
+ // --- FIX START: Parse the www-authenticate header instead of the body ---
146
+ const wwwAuthHeader = originalResponse.headers.get('www-authenticate');
147
+ console.log('[sdk] Raw www-authenticate header:', wwwAuthHeader);
148
+ if (!wwwAuthHeader) {
149
+ throw new Error('402 response is missing the www-authenticate header.');
150
+ }
151
+ const l402Match = wwwAuthHeader.match(/L402="([^"]+)"/);
152
+ console.log('[sdk] Regex match for L402 token:', l402Match);
153
+ if (!l402Match || !l402Match[1]) {
154
+ throw new Error('Could not parse L402 token from www-authenticate header.');
155
+ }
156
+ const l402Token = l402Match[1];
157
+ console.log('[sdk] Extracted L402 Base64 token:', l402Token);
158
+ let paymentInfo;
159
+ try {
160
+ const decodedToken = Buffer.from(l402Token, 'base64').toString('utf8');
161
+ console.log('[sdk] Decoded token (JSON string):', decodedToken);
162
+ const parsedToken = JSON.parse(decodedToken);
163
+ console.log('[sdk] Parsed token (JavaScript object):', JSON.stringify(parsedToken, null, 2));
164
+ // Ensure the parsed token matches the X402PaymentInfo interface
165
+ if (typeof parsedToken === 'object' && parsedToken !== null && 'challenge' in parsedToken && 'chain' in parsedToken && 'recipientAddress' in parsedToken && 'options' in parsedToken && Array.isArray(parsedToken.options)) {
166
+ paymentInfo = parsedToken;
167
+ }
168
+ else {
169
+ console.error('[sdk] Parsed token failed validation. Keys:', Object.keys(parsedToken));
170
+ throw new Error('Parsed L402 token is not in the expected X402PaymentInfo format (missing challenge, chain, recipientAddress, or options).');
171
+ }
172
+ }
173
+ catch (e) {
174
+ console.error('[sdk] Error during token decoding/parsing:', e);
175
+ throw new Error(`Failed to decode or parse L402 token: ${e instanceof Error ? e.message : String(e)}`);
176
+ }
177
+ // --- FIX END ---
178
+ // --- NEW FIX START: Select RPC URL based on the parsed chain ---
179
+ const rpcUrl = paymentInfo.chain === 'base' ? DEFAULT_BASE_RPC : DEFAULT_ETH_RPC;
180
+ const provider = new JsonRpcProvider(rpcUrl);
181
+ const wallet = new Wallet(userWalletPrivateKey, provider);
182
+ console.log(`[sdk] Using RPC URL for chain "${paymentInfo.chain}": ${rpcUrl}`);
183
+ // --- NEW FIX END ---
184
+ const { challenge, chain, recipientAddress, options: paymentOptions } = paymentInfo;
185
+ let txHash;
186
+ // Define a preference order for payment tokens.
187
+ const preferredTokens = paymentConfig.preferredTokens || ['USDC', 'WETH']; // Default preference
188
+ const sortedOptions = [...paymentOptions].sort((a, b) => {
189
+ const aIndex = preferredTokens.indexOf(a.symbol);
190
+ const bIndex = preferredTokens.indexOf(b.symbol);
191
+ if (aIndex === -1 && bIndex === -1)
192
+ return 0;
193
+ if (aIndex === -1)
194
+ return 1;
195
+ if (bIndex === -1)
196
+ return -1;
197
+ return aIndex - bIndex;
198
+ });
199
+ const nativeSymbol = chain === 'base' ? 'ETH' : 'ETH'; // Could be more specific
200
+ const nativeOption = sortedOptions.find(o => o.symbol === nativeSymbol);
201
+ if (nativeOption) {
202
+ const nativeIndex = sortedOptions.indexOf(nativeOption);
203
+ sortedOptions.splice(nativeIndex, 1);
204
+ sortedOptions.push(nativeOption); // Always try native token last unless specified
205
+ }
206
+ for (const option of sortedOptions) {
207
+ const { tokenAddress, amount, decimals, symbol, usdAmount } = option;
208
+ let requiredAmount;
209
+ if (usdAmount) {
210
+ console.log(`[sdk] Option requires a USD value of ${usdAmount}. Fetching price for ${symbol}...`);
211
+ const tokenPriceInUsd = await getUsdcPriceForToken(tokenAddress, chain, decimals);
212
+ const requiredTokens = parseFloat(usdAmount) / tokenPriceInUsd;
213
+ console.log(`[sdk] Current price of ${symbol} is ~$${tokenPriceInUsd.toFixed(4)}. Required tokens: ${requiredTokens}`);
214
+ requiredAmount = parseUnits(requiredTokens.toString(), decimals);
215
+ }
216
+ else if (amount) {
217
+ requiredAmount = parseUnits(amount, decimals);
218
+ }
219
+ else {
220
+ console.warn(`[sdk] Skipping payment option for ${symbol} because it has no 'amount' or 'usdAmount'.`);
221
+ continue;
222
+ }
223
+ console.log(`[sdk] Checking balance for ${symbol} (${tokenAddress}). Required: ${requiredAmount.toString()}`);
224
+ let balance;
225
+ if (tokenAddress.toLowerCase() === '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee') {
226
+ balance = await provider.getBalance(wallet.address);
227
+ }
228
+ else {
229
+ balance = await getTokenBalance(provider, wallet.address, tokenAddress);
230
+ }
231
+ console.log(`[sdk] Found balance for ${symbol}: ${balance.toString()}`);
232
+ if (balance >= requiredAmount) {
233
+ console.log(`[sdk] Sufficient balance found for ${symbol}. Proceeding with payment.`);
234
+ txHash = await api.buy(recipientAddress, requiredAmount, userWalletPrivateKey, chain, rpcUrl, tokenAddress);
235
+ break;
236
+ }
237
+ }
238
+ if (!txHash) {
239
+ // --- Try to swap ---
240
+ console.log("No direct payment option possible. Attempting to find a swap.");
241
+ const chainId = chain === 'base' ? '8453' : '1';
242
+ const defaultSwapAssets = [
243
+ { symbol: 'USDC', address: CHAIN_CONFIG[chainId].USDC },
244
+ { symbol: 'WETH', address: CHAIN_CONFIG[chainId].WETH },
245
+ { symbol: 'ETH', address: CHAIN_CONFIG[chainId].ETH }
246
+ ];
247
+ const swapHierarchy = paymentConfig.swapFromAssets || defaultSwapAssets;
248
+ for (const targetOption of sortedOptions) {
249
+ for (const sourceAsset of swapHierarchy) {
250
+ try {
251
+ const quote = await get0xSwapQuote(targetOption.tokenAddress, sourceAsset.address,
252
+ // --- FIX for USD amounts in swaps ---
253
+ targetOption.usdAmount ?
254
+ (await (async () => {
255
+ console.log(`[sdk] Swap target requires a USD value of ${targetOption.usdAmount}. Fetching price for ${targetOption.symbol}...`);
256
+ const tokenPriceInUsd = await getUsdcPriceForToken(targetOption.tokenAddress, chain, targetOption.decimals);
257
+ const requiredTokens = parseFloat(targetOption.usdAmount) / tokenPriceInUsd;
258
+ console.log(`[sdk] Swap target ${targetOption.symbol} price is ~$${tokenPriceInUsd.toFixed(4)}. Required tokens for swap: ${requiredTokens}`);
259
+ return parseUnits(requiredTokens.toFixed(18), targetOption.decimals); // Use high precision for swap amount
260
+ })()) :
261
+ parseUnits(targetOption.amount, targetOption.decimals), // Use non-null assertion for amount
262
+ // --- End FIX ---
263
+ targetOption.decimals, wallet.address, chain, paymentConfig.slippagePercentage);
264
+ const buyAmount = BigInt(quote.buyAmount);
265
+ let cost;
266
+ let sourceBalance;
267
+ if (sourceAsset.address === NATIVE_TOKEN_ADDRESS) {
268
+ if (!quote.transaction || !quote.transaction.gasPrice || !quote.transaction.gas) {
269
+ throw new Error('0x quote response for native asset swap is missing transaction details (gas/gasPrice).');
270
+ }
271
+ cost = BigInt(quote.sellAmount) + (BigInt(quote.transaction.gasPrice) * BigInt(quote.transaction.gas)); // Correctly reference gasPrice and gas
272
+ sourceBalance = await provider.getBalance(wallet.address);
273
+ }
274
+ else {
275
+ cost = BigInt(quote.sellAmount);
276
+ sourceBalance = await getTokenBalance(provider, wallet.address, sourceAsset.address);
277
+ // Check allowance for the 0x spender contract and approve if necessary
278
+ const allowance = await checkAllowance(provider, wallet.address, sourceAsset.address, quote.allowanceTarget);
279
+ if (allowance < cost) {
280
+ console.log(`Insufficient allowance for ${sourceAsset.symbol}. Approving ${quote.allowanceTarget} now...`);
281
+ const approveTxHash = await api.approve(wallet, sourceAsset.address, quote.allowanceTarget, cost);
282
+ console.log(`Approval transaction sent: ${approveTxHash}. Waiting for confirmation...`);
283
+ const approveReceipt = await provider.waitForTransaction(approveTxHash);
284
+ if (!approveReceipt || approveReceipt.status === 0) {
285
+ throw new Error(`Approval transaction for ${sourceAsset.symbol} failed.`);
286
+ }
287
+ console.log('✅ Approval confirmed.');
288
+ }
289
+ }
290
+ if (sourceBalance >= cost) {
291
+ console.log(`Found affordable swap: ${sourceAsset.symbol} -> ${targetOption.symbol}.`);
292
+ console.log('--- Full 0x Quote Response ---');
293
+ console.log(JSON.stringify(quote, null, 2));
294
+ console.log('-----------------------------');
295
+ // Execute Swap
296
+ const swapTx = {
297
+ to: quote.transaction.to,
298
+ data: quote.transaction.data,
299
+ value: quote.transaction.value,
300
+ gasPrice: quote.transaction.gasPrice,
301
+ gasLimit: quote.transaction.gas, // Add gasLimit from quote
302
+ nonce: await provider.getTransactionCount(wallet.address, 'latest'),
303
+ chainId: parseInt(chainId),
304
+ };
305
+ console.log("Sending swap transaction...");
306
+ const signedSwapTx = await wallet.signTransaction(swapTx);
307
+ const swapTxHash = await provider.send('eth_sendRawTransaction', [signedSwapTx]);
308
+ console.log(`Swap transaction sent: ${swapTxHash}. Waiting for confirmation...`);
309
+ const swapReceipt = await provider.waitForTransaction(swapTxHash);
310
+ if (!swapReceipt || swapReceipt.status === 0) {
311
+ throw new Error(`Swap transaction failed: ${swapTxHash}`);
312
+ }
313
+ console.log('Swap transaction confirmed.');
314
+ // Execute Payment
315
+ console.log(`Swap successful. Proceeding with final payment.`);
316
+ txHash = await api.buy(recipientAddress, buyAmount, // Use the amount from the swap quote
317
+ userWalletPrivateKey, chain, rpcUrl, targetOption.tokenAddress);
318
+ break; // Exit the source asset loop
319
+ }
320
+ }
321
+ catch (error) {
322
+ console.error(`Could not get swap quote for ${sourceAsset.symbol} -> ${targetOption.symbol}:`, error instanceof Error ? error.message : String(error));
323
+ continue; // Try next source asset
324
+ }
325
+ }
326
+ if (txHash)
327
+ break; // Exit the target option loop
328
+ }
329
+ if (!txHash) {
330
+ throw new Error("Could not find a valid payment or swap option.");
331
+ }
332
+ }
333
+ console.error(`Payment transaction sent: ${txHash}. Waiting for confirmation...`);
334
+ const paymentReceipt = await provider.waitForTransaction(txHash);
335
+ if (!paymentReceipt || paymentReceipt.status === 0) {
336
+ throw new Error(`Final payment transaction failed: ${txHash}`);
337
+ }
338
+ console.log('Final payment transaction confirmed.');
339
+ const signature = await wallet.signMessage(challenge);
340
+ const retryOptions = { ...options };
341
+ retryOptions.headers['Authorization'] = `X402 ${txHash}:${signature}`;
342
+ console.error(`Retrying request to ${url} with payment proof.`);
343
+ const finalResponse = await fetch(url, retryOptions);
344
+ if (!finalResponse.ok) {
345
+ const errorBody = await finalResponse.text();
346
+ throw new Error(`API request failed after payment: ${errorBody}`);
347
+ }
348
+ return finalResponse.json();
349
+ }
350
+ async function get0xSwapQuote(buyToken, sellToken, buyAmount, buyTokenDecimals, takerAddress, chain, slippagePercentage = '0.01' // Default 1% slippage
351
+ ) {
352
+ const apiUrl = `https://api.0x.org`;
353
+ const chainId = chain === 'base' ? '8453' : '1';
354
+ const apiKey = process.env.ZERO_X_API_KEY;
355
+ if (!apiKey) {
356
+ throw new Error('ZERO_X_API_KEY is not set in the .env file. Please get a free key from https://dashboard.0x.org/apps');
357
+ }
358
+ const headers = {
359
+ '0x-api-key': apiKey,
360
+ '0x-version': 'v2'
361
+ };
362
+ // Step 1: Get a spot price by making a "reverse" lookup.
363
+ // We sell a nominal amount of the buyToken to get a price in terms of the sellToken.
364
+ const nominalSellAmount = parseUnits('1', buyTokenDecimals);
365
+ const priceParams = new URLSearchParams({
366
+ chainId: chainId,
367
+ buyToken: sellToken, // Swapped for reverse lookup
368
+ sellToken: buyToken, // Swapped for reverse lookup
369
+ sellAmount: nominalSellAmount.toString(),
370
+ taker: takerAddress,
371
+ }).toString();
372
+ const priceUrl = `${apiUrl}/swap/permit2/price?${priceParams}`;
373
+ console.log(`[sdk] Fetching 0x spot price with reverse lookup: ${priceUrl}`);
374
+ const priceResponse = await fetch(priceUrl, { headers });
375
+ if (!priceResponse.ok) {
376
+ const errorBody = await priceResponse.text();
377
+ console.error(`[sdk] Full 0x API spot price error response:`, errorBody);
378
+ throw new Error(`Failed to get 0x spot price: ${errorBody}`);
379
+ }
380
+ const priceData = (await priceResponse.json());
381
+ if (!priceData.buyAmount || !priceData.sellAmount || BigInt(priceData.sellAmount) === 0n) {
382
+ throw new Error('Could not determine a valid price for the swap. This might be due to low liquidity.');
383
+ }
384
+ // Step 2: Calculate the required sellAmount based on the reverse price quote.
385
+ // The reverse price gives us a ratio of sellToken per buyToken.
386
+ // sellAmount = buyAmount * (priceData.buyAmount / priceData.sellAmount)
387
+ const estimatedSellAmount = (buyAmount * BigInt(priceData.buyAmount)) / BigInt(priceData.sellAmount);
388
+ // Add a slippage buffer to the sell amount to ensure the trade goes through.
389
+ const slippageFactor = 1 + parseFloat(slippagePercentage);
390
+ const finalSellAmount = BigInt(Math.ceil(Number(estimatedSellAmount) * slippageFactor));
391
+ // Step 3: Get the firm quote with the estimated sellAmount.
392
+ const quoteParams = new URLSearchParams({
393
+ chainId: chainId,
394
+ buyToken: buyToken,
395
+ sellToken: sellToken,
396
+ sellAmount: finalSellAmount.toString(),
397
+ taker: takerAddress,
398
+ }).toString();
399
+ const quoteUrl = `${apiUrl}/swap/permit2/quote?${quoteParams}`;
400
+ console.error(`Fetching 0x firm quote: ${quoteUrl}`);
401
+ const response = await fetch(quoteUrl, { headers });
402
+ if (!response.ok) {
403
+ const errorBody = await response.text();
404
+ console.error(`[sdk] Full 0x API quote error response for ${quoteUrl}:`, errorBody);
405
+ throw new Error(`Failed to get 0x quote: ${errorBody}`);
406
+ }
407
+ return response.json();
408
+ }
409
+ // --- New helper to get the USDC price of a token ---
410
+ async function getUsdcPriceForToken(tokenAddress, chain, tokenDecimals) {
411
+ if (tokenAddress.toLowerCase() === CHAIN_CONFIG[chain === 'base' ? '8453' : '1'].USDC.toLowerCase()) {
412
+ return 1.0; // USDC is always 1.0 USD
413
+ }
414
+ const apiUrl = `https://api.0x.org`;
415
+ const chainId = chain === 'base' ? '8453' : '1';
416
+ const apiKey = process.env.ZERO_X_API_KEY;
417
+ if (!apiKey) {
418
+ throw new Error('ZERO_X_API_KEY is not set in the .env file for price lookup.');
419
+ }
420
+ const headers = {
421
+ '0x-api-key': apiKey,
422
+ '0x-version': 'v2'
423
+ };
424
+ const nominalSellAmount = parseUnits('1', tokenDecimals).toString();
425
+ // We're buying USDC by selling one full unit of the token to find its price.
426
+ const priceParams = new URLSearchParams({
427
+ chainId: chainId,
428
+ buyToken: CHAIN_CONFIG[chainId].USDC,
429
+ sellToken: tokenAddress,
430
+ sellAmount: nominalSellAmount,
431
+ }).toString();
432
+ const priceUrl = `${apiUrl}/swap/permit2/price?${priceParams}`;
433
+ console.log(`[sdk] Fetching USDC price for ${tokenAddress} via 0x: ${priceUrl}`);
434
+ const priceResponse = await fetch(priceUrl, { headers });
435
+ if (!priceResponse.ok) {
436
+ const errorBody = await priceResponse.text();
437
+ throw new Error(`Failed to get 0x price for ${tokenAddress}: ${errorBody}`);
438
+ }
439
+ const priceData = (await priceResponse.json());
440
+ if (!priceData.buyAmount) {
441
+ throw new Error(`Invalid price response from 0x API: ${JSON.stringify(priceData)}`);
442
+ }
443
+ // The buyAmount is the amount of USDC (6 decimals) we get for 1 full unit of the sellToken.
444
+ const price = parseFloat(ethers.formatUnits(priceData.buyAmount, 6));
445
+ if (isNaN(price) || price <= 0) {
446
+ throw new Error(`Could not determine a valid price from 0x API response.`);
447
+ }
448
+ return price;
449
+ }
99
450
  // --- Helper function for RPC calls (Keep this) ---
100
451
  async function sendJsonRpc(rpcUrl, method, params) {
101
452
  const response = await fetch(rpcUrl, {
@@ -123,14 +474,12 @@ async function sendJsonRpc(rpcUrl, method, params) {
123
474
  class EthereumHandler {
124
475
  async buy(walletAddress, amount, userWalletPrivateKey, chain, rpcUrl, tokenAddress) {
125
476
  const rpc = rpcUrl || (chain === 'base' ? DEFAULT_BASE_RPC : DEFAULT_ETH_RPC);
477
+ const provider = new JsonRpcProvider(rpc);
478
+ const wallet = new Wallet(userWalletPrivateKey, provider);
126
479
  if (!walletAddress || !isAddress(walletAddress)) {
127
480
  throw new Error('Invalid recipient wallet address');
128
481
  }
129
- const wallet = new Wallet(userWalletPrivateKey);
130
- const senderAddress = wallet.address;
131
482
  try {
132
- const nonce = await sendJsonRpc(rpc, 'eth_getTransactionCount', [senderAddress, 'latest']);
133
- const feeData = await sendJsonRpc(rpc, 'eth_gasPrice', []);
134
483
  let txRequest;
135
484
  if (tokenAddress) {
136
485
  if (!isAddress(tokenAddress)) {
@@ -141,27 +490,21 @@ class EthereumHandler {
141
490
  const data = iface.encodeFunctionData("transfer", [walletAddress, amount]);
142
491
  txRequest = {
143
492
  to: tokenAddress,
144
- nonce: parseInt(nonce, 16),
145
- gasPrice: feeData,
146
- gasLimit: 100000,
147
493
  data: data,
148
- chainId: (await sendJsonRpc(rpc, 'eth_chainId', [])),
494
+ value: 0
149
495
  };
150
496
  }
151
497
  else {
152
498
  txRequest = {
153
499
  to: walletAddress,
154
500
  value: amount,
155
- nonce: parseInt(nonce, 16),
156
- gasPrice: feeData,
157
- gasLimit: 21000,
158
- chainId: (await sendJsonRpc(rpc, 'eth_chainId', [])),
159
501
  };
160
502
  }
161
- const signedTx = await wallet.signTransaction(txRequest);
162
- const txHash = await sendJsonRpc(rpc, 'eth_sendRawTransaction', [signedTx]);
163
- console.error(`Transaction sent: ${txHash}. Confirmation check not implemented.`);
164
- return txHash;
503
+ const txResponse = await wallet.sendTransaction(txRequest);
504
+ console.error(`Transaction sent: ${txResponse.hash}. Waiting for confirmation...`);
505
+ await txResponse.wait();
506
+ console.error(`Transaction confirmed: ${txResponse.hash}`);
507
+ return txResponse.hash;
165
508
  }
166
509
  catch (error) {
167
510
  console.error('Detailed ETH error:', error);
@@ -198,6 +541,35 @@ class ApiNow {
198
541
  }
199
542
  return handler.buy(walletAddress, amount, userWalletPrivateKey, chain, rpcUrl, tokenAddress);
200
543
  }
544
+ async execute(endpoint, userWalletPrivateKey, opts = {}, paymentConfig = {}) {
545
+ console.error(`Executing request for endpoint: ${endpoint}`);
546
+ const url = new URL(endpoint);
547
+ const method = (opts.method || 'GET').toUpperCase();
548
+ const fetchOptions = {
549
+ method: method,
550
+ headers: {
551
+ 'Content-Type': 'application/json',
552
+ 'Accept': '*/*',
553
+ },
554
+ };
555
+ if (opts.data) {
556
+ if (method === 'GET' || method === 'HEAD') {
557
+ const params = new URLSearchParams(opts.data);
558
+ params.forEach((value, key) => url.searchParams.append(key, value));
559
+ }
560
+ else {
561
+ fetchOptions.body = JSON.stringify(opts.data);
562
+ }
563
+ }
564
+ return fetchWithX402(url.toString(), fetchOptions, this, userWalletPrivateKey, paymentConfig);
565
+ }
566
+ async approve(wallet, tokenAddress, spenderAddress, amount) {
567
+ const abi = ["function approve(address spender, uint256 amount)"];
568
+ const contract = new Contract(tokenAddress, abi, wallet);
569
+ const tx = await contract.approve(spenderAddress, amount);
570
+ await tx.wait();
571
+ return tx.hash;
572
+ }
201
573
  async txResponse(endpoint, txHash, opts = {}) {
202
574
  console.error(`txResponse: Called with endpoint: ${endpoint}, txHash: ${txHash}, opts:`, JSON.stringify(opts, null, 2));
203
575
  if (!endpoint || typeof endpoint !== 'string') {
@@ -225,6 +597,10 @@ class ApiNow {
225
597
  },
226
598
  // body is set conditionally below
227
599
  };
600
+ if (opts.signature) {
601
+ fetchOptions.headers['X-Signature'] = opts.signature;
602
+ console.error(`txResponse: Included signature in X-Signature header.`);
603
+ }
228
604
  if (method === 'GET' || method === 'HEAD') {
229
605
  // --- GET/HEAD: Append data as query params ---
230
606
  if (opts.data && typeof opts.data === 'object' && Object.keys(opts.data).length > 0) {
@@ -290,6 +666,9 @@ class ApiNow {
290
666
  console.error(`Attempting payment: Chain=${chain}, To=${walletAddress}, Amount=${amountBigInt.toString()}, Token=${tokenAddress || 'Native'}`);
291
667
  const txHash = await this.buy(walletAddress, amountBigInt, userWalletPrivateKey, chain, rpcUrl, tokenAddress, opts.fastMode);
292
668
  console.error(`Transaction sent: ${txHash}`);
669
+ const wallet = new Wallet(userWalletPrivateKey);
670
+ const signature = await wallet.signMessage(txHash);
671
+ console.error(`Generated signature for txHash ${txHash}: ${signature}`);
293
672
  if (!opts.fastMode) {
294
673
  await new Promise(resolve => setTimeout(resolve, 3000));
295
674
  }
@@ -300,7 +679,8 @@ class ApiNow {
300
679
  // Create specific options for txResponse
301
680
  const txResponseOpts = {
302
681
  method: info.httpMethod || 'POST', // Use the method from info, default to POST
303
- data: opts.data // Pass the original data payload intended for the API
682
+ data: opts.data, // Pass the original data payload intended for the API
683
+ signature: signature
304
684
  };
305
685
  // Call txResponse with the tailored options
306
686
  return this.txResponse(endpoint, txHash, txResponseOpts);
@@ -308,3 +688,4 @@ class ApiNow {
308
688
  }
309
689
  const apiNow = new ApiNow();
310
690
  export default apiNow;
691
+ export { get0xSwapQuote }; // Export for testing
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apinow-sdk",
3
- "version": "0.12.3",
3
+ "version": "0.12.4",
4
4
  "description": "ApiNow SDK · The endpoint vending machine",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -24,7 +24,6 @@
24
24
  "apinow-sdk": "^0.11.19",
25
25
  "body-parser": "^2.2.0",
26
26
  "ethers": "^6.13.5",
27
- "express": "^5.1.0",
28
27
  "node-fetch": "^3.3.2"
29
28
  },
30
29
  "devDependencies": {