apinow-sdk 0.12.5 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -7,6 +7,7 @@ A TypeScript SDK for interacting with ApiNow endpoints, supporting Ethereum and
7
7
  - **Automatic x402 Payments**: Intercepts `402` responses to handle payment flows automatically.
8
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
9
  - **Flexible Pricing**: Supports endpoints that require a fixed token amount or a USD equivalent.
10
+ - **Endpoint Discovery**: Includes a `search` method to find endpoints semantically.
10
11
  - **Configurable Payment**: Prioritize which tokens you prefer to pay with.
11
12
  - **Multi-chain support**: Works with Ethereum and Base.
12
13
  - **Node.js Environment**: Designed to work in a Node.js environment.
@@ -19,33 +20,31 @@ npm install apinow-sdk
19
20
  yarn add apinow-sdk
20
21
  ```
21
22
 
22
- ## Usage
23
+ ## Quick Example
23
24
 
24
25
  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.
25
26
 
26
27
  ```typescript
27
28
  import apiNow from 'apinow-sdk';
28
29
 
29
- // The API endpoint you want to interact with.
30
- const ENDPOINT_URL = 'https://apinow.fun/api/endpoints/your-endpoint';
31
-
32
30
  // Your private key, securely stored (e.g., in an environment variable).
33
- const YOUR_WALLET_PRIVATE_KEY = '0x...';
31
+ const YOUR_WALLET_PRIVATE_KEY = process.env.USER_PRIVATE_KEY;
34
32
 
35
33
  async function main() {
36
34
  try {
37
35
  // 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.
36
+ // If the API requires a payment (402), the SDK will find the best
37
+ // token you hold, swap if necessary, send the payment, and retry
38
+ // the original request with proof of payment.
43
39
  const response = await apiNow.execute(
44
- ENDPOINT_URL,
40
+ 'https://apinow.fun/api/endpoints/apinowfun/translate-TRANSLATE',
45
41
  YOUR_WALLET_PRIVATE_KEY,
46
42
  { // Optional: request options
47
43
  method: 'POST',
48
- data: { query: 'your-data' }
44
+ data: {
45
+ text: 'Hello world',
46
+ selectedLanguage: 'es'
47
+ }
49
48
  }
50
49
  );
51
50
 
@@ -58,100 +57,28 @@ async function main() {
58
57
  main();
59
58
  ```
60
59
 
60
+ For a complete, runnable example, see [`example.js`](./example.js).
61
+
61
62
  ## How It Works: Automatic Payments
62
63
 
63
64
  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:
64
65
 
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
+ 1. **Parses Payment Options**: The `402` response contains a list of accepted payment options.
66
67
  2. **Checks Balances**: It checks your wallet balance for each of the accepted payment tokens.
67
68
  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
+ 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.
69
70
  5. **Pays and Retries**: Once the payment transaction is sent, the SDK automatically retries the original API request, now with proof of payment.
70
71
 
71
- ## Configuration
72
-
73
- You can customize the behavior of the `execute` method with the `opts` and `paymentConfig` parameters.
74
-
75
- ### Request Options (`opts`)
76
-
77
- Passed as the third argument to `execute`. This corresponds to `TxResponseOptions`.
78
-
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.
81
-
82
- ### Payment Configuration (`paymentConfig`)
83
-
84
- Passed as the fourth argument to `execute`. This corresponds to `X402PaymentConfig`.
85
-
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.
87
-
88
- ```typescript
89
- await apiNow.execute(
90
- ENDPOINT_URL,
91
- YOUR_WALLET_PRIVATE_KEY,
92
- { method: 'POST', data: { /* ... */ } }, // opts
93
- { preferredTokens: ['DAI', 'USDC'] } // paymentConfig
94
- );
95
- ```
96
-
97
- ## Legacy Flow (Backward Compatibility)
98
-
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(
103
- ENDPOINT_URL,
104
- YOUR_WALLET_PRIVATE_KEY
105
- );
106
- ```
107
-
108
72
  ## API Reference
109
73
 
110
74
  ### `execute(endpoint, privateKey, opts?, paymentConfig?)`
111
75
  Handles a request and its potential payment in a single, automatic call. This is the recommended method.
112
76
 
113
- ### `infoBuyResponse(endpoint, privateKey, rpcUrl?, opts?)`
114
- (Legacy) Combines `info`, `buy`, and `txResponse` into a single call.
115
-
116
- ### `info(endpoint)`
117
- (Legacy) Gets payment requirement information from an endpoint.
118
-
119
- ### `buy(walletAddress, amount, privateKey, chain, ...)`
120
- (Legacy) Sends a payment transaction.
121
-
122
- ### `txResponse(endpoint, txHash, opts?)`
123
- (Legacy) Fetches the API response after a payment has been made manually.
77
+ ### `search(params, privateKey, paymentConfig?)`
78
+ Performs a semantic search for endpoints.
124
79
 
125
- ## Types
126
-
127
- ```typescript
128
- // Response from a 402 error
129
- interface X402PaymentInfo {
130
- challenge: string;
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;
142
- }
143
-
144
- // Configuration for payments
145
- interface X402PaymentConfig {
146
- preferredTokens?: string[];
147
- }
148
-
149
- // Options for the API request itself
150
- interface TxResponseOptions {
151
- method?: string;
152
- data?: any;
153
- }
154
- ```
80
+ ### `info(endpointUrl)`
81
+ Retrieves public, detailed information about an endpoint.
155
82
 
156
83
  ## Default RPC URLs
157
84
 
@@ -179,47 +106,3 @@ It is NOT directly compatible with browsers or edge environments that do not pro
179
106
  ## License
180
107
 
181
108
  MIT
182
-
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,51 +1,35 @@
1
- import { Wallet } from 'ethers';
2
- interface TxResponseOptions {
3
- method?: string;
4
- data?: any;
5
- signature?: string;
1
+ export interface CallOptions {
2
+ method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
3
+ body?: Record<string, any>;
4
+ headers?: Record<string, string>;
6
5
  }
7
- interface InfoResponse {
8
- requiredAmount: string;
9
- walletAddress: string;
10
- httpMethod: string;
11
- tokenAddress?: string;
12
- chain: 'eth' | 'base';
13
- decimals?: number;
6
+ export interface ApinowConfig {
7
+ privateKey: `0x${string}`;
8
+ baseUrl?: string;
14
9
  }
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>;
37
- declare class ApiNow {
38
- private handlers;
39
- info(endpoint: string): Promise<InfoResponse>;
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>;
43
- txResponse(endpoint: string, txHash: string, opts?: TxResponseOptions): Promise<any>;
44
- infoBuyResponse(endpoint: string, userWalletPrivateKey: string, rpcUrl?: string, opts?: TxResponseOptions & {
45
- fastMode?: boolean;
46
- }): Promise<any>;
47
- }
48
- declare const apiNow: ApiNow;
49
- export default apiNow;
50
- export { get0xSwapQuote };
51
- export type { InfoResponse, TxResponseOptions, X402PaymentInfo, X402PaymentOption, X402PaymentConfig };
10
+ export declare function createClient(config: ApinowConfig): {
11
+ wallet: `0x${string}`;
12
+ /**
13
+ * Call any APINow endpoint. Handles x402 payment automatically.
14
+ *
15
+ * @example
16
+ * const data = await apinow.call('/api/endpoints/apinowfun/translate', {
17
+ * method: 'POST',
18
+ * body: { text: 'Hello world', targetLanguage: 'es' },
19
+ * });
20
+ */
21
+ call(endpoint: string, opts?: CallOptions): Promise<any>;
22
+ /**
23
+ * Semantic search across all APINow endpoints.
24
+ */
25
+ search(query: string, limit?: number): Promise<any>;
26
+ /**
27
+ * Get public endpoint info (free, no payment).
28
+ */
29
+ info(namespace: string, endpointName: string): Promise<any>;
30
+ /**
31
+ * The underlying x402-wrapped fetch, for advanced use.
32
+ */
33
+ fetch: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
34
+ };
35
+ export default createClient;
package/dist/index.js CHANGED
@@ -1,691 +1,67 @@
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
4
- // Default RPC URLs
5
- const DEFAULT_ETH_RPC = 'https://rpc.ankr.com/eth';
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
- };
20
- // --- Helper function for fetch (Keep this) ---
21
- async function fetchJson(url, options) {
22
- console.error(`fetchJson (using node-fetch): Called with URL: ${url}`);
23
- // Safer logging for options to avoid mutating Uint8Array body
24
- if (options) {
25
- const { body, ...optionsWithoutBody } = options; // Destructure to separate body for logging
26
- console.error(`fetchJson (using node-fetch): Called with options (metadata):`, JSON.stringify(optionsWithoutBody, null, 2));
27
- if (options.headers) { // Log headers from original options
28
- const headers = options.headers;
29
- const contentLengthHeader = headers['Content-Length'] || headers['content-length'];
30
- console.error(`fetchJson (using node-fetch): Content-Length in options.headers before fetch: ${contentLengthHeader}`);
31
- }
32
- if (body) {
33
- // For node-fetch, string or Buffer bodies are common.
34
- // If we are passing a string, its length in bytes is what Content-Length should be.
35
- // If it's a Buffer, Buffer.length.
36
- if (typeof body === 'string') {
37
- console.error(`fetchJson (using node-fetch): Body in options is string, length: ${body.length}. Snippet (first ~100 chars): ${body.substring(0, 100)}`);
38
- }
39
- else if (body instanceof Buffer) {
40
- console.error(`fetchJson (using node-fetch): Body in options is Buffer, length: ${body.length}. Snippet (first ~100 bytes as hex): ${body.slice(0, 100).toString('hex')}`);
41
- }
42
- else if (body instanceof Uint8Array) {
43
- // This case should ideally not be hit if txResponse prepares a string or Buffer
44
- console.error(`fetchJson (using node-fetch): Body in options is Uint8Array, length: ${body.length}.`);
45
- }
46
- else {
47
- try {
48
- console.error(`fetchJson (using node-fetch): Body in options before fetch (first 500 chars): ${String(body).substring(0, 500)}`);
49
- }
50
- catch (e) {
51
- console.error(`fetchJson (using node-fetch): Body in options before fetch: (Could not be easily stringified for logging)`);
52
- }
53
- }
54
- }
55
- else {
56
- console.error(`fetchJson (using node-fetch): No body in options.`);
57
- }
58
- }
59
- else {
60
- console.error(`fetchJson (using node-fetch): Called with no options.`);
61
- }
62
- let response;
63
- try {
64
- // @ts-ignore options might not perfectly match node-fetch's expected type if RequestInit from lib.dom.d.ts is too different
65
- response = await fetch(url, options);
66
- }
67
- catch (networkError) {
68
- let requestBodySummary = "No body provided";
69
- if (options?.body) {
70
- if (typeof options.body === 'string') {
71
- requestBodySummary = `String body (len ${options.body.length}): ${options.body.substring(0, 100)}...`;
72
- }
73
- else if (options.body instanceof Buffer) {
74
- requestBodySummary = `Buffer body (len ${options.body.length})`;
75
- }
76
- else if (options.body instanceof Uint8Array) { // Should not happen with current txResponse
77
- requestBodySummary = `Uint8Array body (len ${options.body.length})`;
78
- }
79
- else {
80
- requestBodySummary = `Body of type ${options.body?.constructor?.name || 'unknown'}`;
81
- }
82
- }
83
- const errorMessage = `Network/fetch error for URL: ${url}, Method: ${options?.method || 'GET'}, Request: ${requestBodySummary}. Original error: ${networkError instanceof Error ? networkError.message : String(networkError)}`;
84
- console.error("fetchJson (using node-fetch): Fetch execution error - ", errorMessage);
85
- throw new Error(errorMessage);
86
- }
87
- console.error(`fetchJson (using node-fetch): Response status: ${response.status}, ok: ${response.ok}`);
88
- if (!response.ok) {
89
- const errorBodyText = await response.text();
90
- console.error(`fetchJson (using node-fetch): Error response body for ${url} (status ${response.status}): ${errorBodyText}`);
91
- let requestBodySummary = "No body provided";
92
- if (options?.body) {
93
- if (typeof options.body === 'string') {
94
- requestBodySummary = `String body (len ${options.body.length}): ${options.body.substring(0, 100)}...`;
95
- }
96
- else if (options.body instanceof Buffer) {
97
- requestBodySummary = `Buffer body (len ${options.body.length})`;
98
- }
99
- else if (options.body instanceof Uint8Array) { // Should not happen with current txResponse
100
- requestBodySummary = `Uint8Array body (len ${options.body.length})`;
101
- }
102
- else {
103
- requestBodySummary = `Body of type ${options.body?.constructor?.name || 'unknown'}`;
104
- }
105
- }
106
- const detailedErrorMessage = `HTTP error ${response.status} for URL: ${url}, Method: ${options?.method || 'GET'}, Request: ${requestBodySummary}. Response: ${errorBodyText}`;
107
- throw new Error(detailedErrorMessage);
108
- }
109
- const responseData = await response.json();
110
- console.error(`fetchJson: Successfully fetched and parsed JSON (first 500 chars): ${JSON.stringify(responseData).substring(0, 500)}`);
111
- return responseData;
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 ${challenge}:${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
- }
450
- // --- Helper function for RPC calls (Keep this) ---
451
- async function sendJsonRpc(rpcUrl, method, params) {
452
- const response = await fetch(rpcUrl, {
453
- method: 'POST',
454
- headers: {
455
- 'Content-Type': 'application/json',
1
+ import { x402Client, wrapFetchWithPayment } from '@x402/fetch';
2
+ import { registerExactEvmScheme } from '@x402/evm/exact/client';
3
+ import { privateKeyToAccount } from 'viem/accounts';
4
+ const APINOW_BASE = 'https://apinow.fun';
5
+ // ─── SDK ───
6
+ export function createClient(config) {
7
+ const { privateKey, baseUrl = APINOW_BASE } = config;
8
+ const account = privateKeyToAccount(privateKey);
9
+ const client = new x402Client();
10
+ registerExactEvmScheme(client, { signer: account });
11
+ const fetchWithPayment = wrapFetchWithPayment(fetch, client);
12
+ return {
13
+ wallet: account.address,
14
+ /**
15
+ * Call any APINow endpoint. Handles x402 payment automatically.
16
+ *
17
+ * @example
18
+ * const data = await apinow.call('/api/endpoints/apinowfun/translate', {
19
+ * method: 'POST',
20
+ * body: { text: 'Hello world', targetLanguage: 'es' },
21
+ * });
22
+ */
23
+ async call(endpoint, opts = {}) {
24
+ const url = endpoint.startsWith('http') ? endpoint : `${baseUrl}${endpoint}`;
25
+ const method = opts.method || 'POST';
26
+ const fetchOpts = {
27
+ method,
28
+ headers: {
29
+ 'Content-Type': 'application/json',
30
+ ...opts.headers,
31
+ },
32
+ };
33
+ if (opts.body && method !== 'GET') {
34
+ fetchOpts.body = JSON.stringify(opts.body);
35
+ }
36
+ const res = await fetchWithPayment(url, fetchOpts);
37
+ if (!res.ok) {
38
+ const text = await res.text();
39
+ throw new Error(`APINow ${res.status}: ${text}`);
40
+ }
41
+ return res.json();
456
42
  },
457
- body: JSON.stringify({
458
- jsonrpc: '2.0',
459
- id: 1, // Simple static ID
460
- method: method,
461
- params: params,
462
- }),
463
- });
464
- if (!response.ok) {
465
- const errorBody = await response.text();
466
- throw new Error(`RPC error ${response.status} for method ${method}: ${errorBody}`);
467
- }
468
- const jsonResponse = await response.json();
469
- if (jsonResponse.error) {
470
- throw new Error(`RPC error for method ${method}: ${JSON.stringify(jsonResponse.error)}`);
471
- }
472
- return jsonResponse.result;
473
- }
474
- class EthereumHandler {
475
- async buy(walletAddress, amount, userWalletPrivateKey, chain, rpcUrl, tokenAddress) {
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);
479
- if (!walletAddress || !isAddress(walletAddress)) {
480
- throw new Error('Invalid recipient wallet address');
481
- }
482
- try {
483
- let txRequest;
484
- if (tokenAddress) {
485
- if (!isAddress(tokenAddress)) {
486
- throw new Error('Invalid token address');
487
- }
488
- const abi = ["function transfer(address to, uint256 amount)"];
489
- const iface = new ethers.Interface(abi);
490
- const data = iface.encodeFunctionData("transfer", [walletAddress, amount]);
491
- txRequest = {
492
- to: tokenAddress,
493
- data: data,
494
- value: 0
495
- };
496
- }
497
- else {
498
- txRequest = {
499
- to: walletAddress,
500
- value: amount,
501
- };
502
- }
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;
508
- }
509
- catch (error) {
510
- console.error('Detailed ETH error:', error);
511
- throw new Error(`Ethereum transaction failed: ${error instanceof Error ? error.message : String(error)}`);
512
- }
513
- }
514
- }
515
- class ApiNow {
516
- constructor() {
517
- this.handlers = {
518
- eth: new EthereumHandler(),
519
- base: new EthereumHandler()
520
- };
521
- }
522
- async info(endpoint) {
523
- if (!endpoint || typeof endpoint !== 'string') {
524
- throw new Error('Invalid endpoint URL');
525
- }
526
- try {
527
- return await fetchJson(endpoint);
528
- }
529
- catch (error) {
530
- console.error(`Failed to fetch info from ${endpoint}:`, error);
531
- throw new Error(`Could not get endpoint info: ${error instanceof Error ? error.message : String(error)}`);
532
- }
533
- }
534
- async buy(walletAddress, amount, userWalletPrivateKey, chain, rpcUrl, tokenAddress, fastMode) {
535
- const handler = this.handlers[chain];
536
- if (!handler) {
537
- throw new Error(`Unsupported chain: ${chain}`);
538
- }
539
- if (amount <= 0n) {
540
- throw new Error('Amount must be positive.');
541
- }
542
- return handler.buy(walletAddress, amount, userWalletPrivateKey, chain, rpcUrl, tokenAddress);
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
- }
573
- async txResponse(endpoint, txHash, opts = {}) {
574
- console.error(`txResponse: Called with endpoint: ${endpoint}, txHash: ${txHash}, opts:`, JSON.stringify(opts, null, 2));
575
- if (!endpoint || typeof endpoint !== 'string') {
576
- console.error('txResponse: Invalid endpoint URL received.');
577
- throw new Error('Invalid endpoint URL');
578
- }
579
- if (!txHash || typeof txHash !== 'string') {
580
- console.error('txResponse: Invalid transaction hash received.');
581
- throw new Error('Invalid transaction hash');
582
- }
583
- const url = new URL(endpoint);
584
- // Add txHash as a query parameter
585
- url.searchParams.append('txHash', txHash);
586
- console.error(`txResponse: Constructed URL (with txHash query param) for fetch: ${url.toString()}`);
587
- // Determine method reliably
588
- const method = (opts.method || 'GET').toUpperCase();
589
- console.error(`txResponse: Preparing ${method} request to ${endpoint}`);
590
- const fetchOptions = {
591
- method: method,
592
- headers: {
593
- 'Content-Type': 'application/json',
594
- 'Accept': '*/*',
595
- 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36',
596
- 'X-Transaction-Hash': txHash
597
- },
598
- // body is set conditionally below
599
- };
600
- if (opts.signature) {
601
- fetchOptions.headers['X-Signature'] = opts.signature;
602
- console.error(`txResponse: Included signature in X-Signature header.`);
603
- }
604
- if (method === 'GET' || method === 'HEAD') {
605
- // --- GET/HEAD: Append data as query params ---
606
- if (opts.data && typeof opts.data === 'object' && Object.keys(opts.data).length > 0) {
607
- console.error(`txResponse: Appending data as query params for GET request:`, opts.data);
608
- // Convert potential non-string values in opts.data to strings for URLSearchParams
609
- const paramsData = {};
610
- for (const key in opts.data) {
611
- if (Object.prototype.hasOwnProperty.call(opts.data, key)) {
612
- paramsData[key] = String(opts.data[key]);
613
- }
614
- }
615
- const params = new URLSearchParams(paramsData);
616
- params.forEach((value, key) => {
617
- url.searchParams.append(key, value);
618
- });
619
- }
620
- // Ensure no body is sent for GET/HEAD
621
- delete fetchOptions.body; // Or just don't set it
622
- }
623
- else {
624
- // --- POST/PUT/etc.: Set data as body ---
625
- if (opts.data) {
626
- console.error(`txResponse: Setting data as body for ${method} request (to be used with node-fetch):`, opts.data);
627
- const requestBodyString = JSON.stringify(opts.data);
628
- fetchOptions.body = requestBodyString; // Use the string as the body for node-fetch
629
- // Let node-fetch set the Content-Length automatically for string bodies
630
- // const bodyBytes = new TextEncoder().encode(requestBodyString);
631
- // (fetchOptions.headers as Record<string, string>)['Content-Length'] = String(bodyBytes.length);
632
- }
633
- }
634
- try {
635
- console.error(`txResponse: About to call fetchJson. Final URL: ${url.toString()}, Final fetchOptions:`, JSON.stringify(fetchOptions, null, 2));
636
- // Use the potentially modified URL and fetchOptions
637
- const result = await fetchJson(url.toString(), fetchOptions);
638
- console.error('txResponse: Successfully received response from fetchJson (first 500 chars): ', JSON.stringify(result).substring(0, 500));
639
- return result;
640
- }
641
- catch (error) {
642
- console.error(`txResponse: Error during fetchJson call from ${url.toString()} for tx ${txHash} using method ${method}:`, error);
643
- throw new Error(`Could not get transaction response: ${error instanceof Error ? error.message : String(error)}`);
644
- }
645
- }
646
- async infoBuyResponse(endpoint, userWalletPrivateKey, rpcUrl, opts = {}) {
647
- console.error(`Starting infoBuyResponse for endpoint: ${endpoint}`);
648
- const info = await this.info(endpoint);
649
- console.error("Received info:", info);
650
- const { requiredAmount, walletAddress, chain, tokenAddress, decimals } = info;
651
- if (!chain || !this.handlers[chain]) {
652
- throw new Error(`Unsupported chain specified by endpoint: ${chain}`);
653
- }
654
- let amountBigInt;
655
- try {
656
- // Use info.decimals if available, otherwise default to 18 (for ETH)
657
- const parseDecimals = (tokenAddress && decimals !== undefined) ? decimals : 18;
658
- amountBigInt = parseUnits(requiredAmount, parseDecimals);
659
- if (amountBigInt <= 0n) {
660
- throw new Error('Required amount must be positive.');
661
- }
662
- }
663
- catch (e) {
664
- throw new Error(`Invalid requiredAmount format or value: ${requiredAmount}. Could not parse with ${(tokenAddress && decimals !== undefined) ? decimals : 18} decimals.`);
665
- }
666
- console.error(`Attempting payment: Chain=${chain}, To=${walletAddress}, Amount=${amountBigInt.toString()}, Token=${tokenAddress || 'Native'}`);
667
- const txHash = await this.buy(walletAddress, amountBigInt, userWalletPrivateKey, chain, rpcUrl, tokenAddress, opts.fastMode);
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}`);
672
- if (!opts.fastMode) {
673
- await new Promise(resolve => setTimeout(resolve, 3000));
674
- }
675
- else {
676
- await new Promise(resolve => setTimeout(resolve, 500));
677
- }
678
- console.error(`Fetching response for tx: ${txHash}`);
679
- // Create specific options for txResponse
680
- const txResponseOpts = {
681
- method: info.httpMethod || 'POST', // Use the method from info, default to POST
682
- data: opts.data, // Pass the original data payload intended for the API
683
- signature: signature
684
- };
685
- // Call txResponse with the tailored options
686
- return this.txResponse(endpoint, txHash, txResponseOpts);
687
- }
43
+ /**
44
+ * Semantic search across all APINow endpoints.
45
+ */
46
+ async search(query, limit = 10) {
47
+ return this.call(`${baseUrl}/api/endpoints/apinowfun/endpoint-search`, {
48
+ method: 'POST',
49
+ body: { query, limit },
50
+ });
51
+ },
52
+ /**
53
+ * Get public endpoint info (free, no payment).
54
+ */
55
+ async info(namespace, endpointName) {
56
+ const res = await fetch(`${baseUrl}/api/endpoints/${namespace}/${endpointName}/details`);
57
+ if (!res.ok)
58
+ throw new Error(`Failed to fetch info: ${res.status}`);
59
+ return res.json();
60
+ },
61
+ /**
62
+ * The underlying x402-wrapped fetch, for advanced use.
63
+ */
64
+ fetch: fetchWithPayment,
65
+ };
688
66
  }
689
- const apiNow = new ApiNow();
690
- export default apiNow;
691
- export { get0xSwapQuote }; // Export for testing
67
+ export default createClient;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "apinow-sdk",
3
- "version": "0.12.5",
4
- "description": "ApiNow SDK · The endpoint vending machine",
3
+ "version": "0.13.0",
4
+ "description": "Pay-per-call API SDK for APINow.fun wraps x402 so you don't have to",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "type": "module",
@@ -10,29 +10,32 @@
10
10
  ],
11
11
  "scripts": {
12
12
  "build": "tsc",
13
- "test": "jest",
14
13
  "prepare": "npm run build"
15
14
  },
16
15
  "keywords": [
17
16
  "api",
17
+ "x402",
18
+ "pay-per-call",
18
19
  "ethereum",
19
- "web3"
20
+ "base",
21
+ "web3",
22
+ "ai",
23
+ "llm"
20
24
  ],
21
25
  "author": "ApiNow.fun",
22
26
  "license": "MIT",
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "https://github.com/1dolinski/apinow"
30
+ },
31
+ "homepage": "https://apinow.fun",
23
32
  "dependencies": {
24
- "apinow-sdk": "^0.11.19",
25
- "body-parser": "^2.2.0",
26
- "ethers": "^6.13.5",
27
- "node-fetch": "^3.3.2"
33
+ "@x402/fetch": "^2.3.0",
34
+ "@x402/evm": "^2.3.1",
35
+ "viem": "^2.38.3"
28
36
  },
29
37
  "devDependencies": {
30
- "@types/express": "^5.0.2",
31
- "@types/jest": "^29.5.12",
32
38
  "@types/node": "^20.11.24",
33
- "jest": "^29.7.0",
34
- "ts-jest": "^29.1.2",
35
- "ts-node": "^10.9.2",
36
39
  "typescript": "^5.8.3"
37
40
  }
38
41
  }
package/dist/server.d.ts DELETED
@@ -1,2 +0,0 @@
1
- declare const app: import("express-serve-static-core").Express;
2
- export default app;
package/dist/server.js DELETED
@@ -1,90 +0,0 @@
1
- import express from 'express';
2
- import bodyParser from 'body-parser';
3
- import apiNow from './index'; // Assuming your compiled ApiNow class is exported as default from index.js
4
- const app = express();
5
- const port = process.env.PORT || 3000;
6
- app.use(bodyParser.json());
7
- // Middleware for cleaner async route handling
8
- const asyncHandler = (fn) => (req, res, next) => {
9
- Promise.resolve(fn(req, res, next)).catch(next);
10
- };
11
- // Endpoint to get API info
12
- app.get('/info', asyncHandler(async (req, res, next) => {
13
- const endpoint = req.query.endpoint;
14
- if (!endpoint) {
15
- res.status(400).send({ error: 'Missing endpoint query parameter' });
16
- return;
17
- }
18
- try {
19
- const info = await apiNow.info(endpoint);
20
- res.json(info);
21
- }
22
- catch (error) {
23
- console.error(`Error in /info for endpoint ${endpoint}:`, error);
24
- // Pass to error handling middleware or directly send response
25
- res.status(500).send({ error: error.message || 'Failed to get info' });
26
- }
27
- }));
28
- // Endpoint to buy (initiate payment)
29
- app.post('/buy', asyncHandler(async (req, res, next) => {
30
- const { walletAddress, amount, // Expecting amount as string to be converted to bigint
31
- userWalletPrivateKey, chain, rpcUrl, tokenAddress, fastMode } = req.body;
32
- if (!walletAddress || !amount || !userWalletPrivateKey || !chain) {
33
- res.status(400).send({ error: 'Missing required fields: walletAddress, amount, userWalletPrivateKey, chain' });
34
- return;
35
- }
36
- try {
37
- const amountBigInt = BigInt(amount);
38
- if (amountBigInt <= 0n) {
39
- res.status(400).send({ error: 'Amount must be a positive value.' });
40
- return;
41
- }
42
- const txHash = await apiNow.buy(walletAddress, amountBigInt, userWalletPrivateKey, chain, rpcUrl, tokenAddress, fastMode);
43
- res.json({ txHash });
44
- }
45
- catch (error) {
46
- console.error(`Error in /buy:`, error);
47
- res.status(500).send({ error: error.message || 'Failed to process buy request' });
48
- }
49
- }));
50
- // Endpoint to get transaction response
51
- app.post('/tx-response', asyncHandler(async (req, res, next) => {
52
- const { endpoint, txHash, opts } = req.body;
53
- if (!endpoint || !txHash) {
54
- res.status(400).send({ error: 'Missing required fields: endpoint, txHash' });
55
- return;
56
- }
57
- try {
58
- const response = await apiNow.txResponse(endpoint, txHash, opts || {});
59
- res.json(response);
60
- }
61
- catch (error) {
62
- console.error(`Error in /tx-response for ${txHash} at ${endpoint}:`, error);
63
- res.status(500).send({ error: error.message || 'Failed to get transaction response' });
64
- }
65
- }));
66
- // Endpoint for the combined info, buy, and response flow
67
- app.post('/info-buy-response', asyncHandler(async (req, res, next) => {
68
- const { endpoint, userWalletPrivateKey, rpcUrl, opts // This includes TxResponseOptions and fastMode
69
- } = req.body;
70
- if (!endpoint || !userWalletPrivateKey) {
71
- res.status(400).send({ error: 'Missing required fields: endpoint, userWalletPrivateKey' });
72
- return;
73
- }
74
- try {
75
- const response = await apiNow.infoBuyResponse(endpoint, userWalletPrivateKey, rpcUrl, opts || {});
76
- res.json(response);
77
- }
78
- catch (error) {
79
- console.error(`Error in /info-buy-response for endpoint ${endpoint}:`, error);
80
- res.status(500).send({ error: error.message || 'Failed to process info-buy-response request' });
81
- }
82
- }));
83
- app.listen(port, () => {
84
- console.log(`ApiNow HTTP server running on http://localhost:${port}`);
85
- });
86
- // Optional: Add a root endpoint for basic health check or info
87
- app.get('/', (req, res) => {
88
- res.send('ApiNow HTTP Wrapper is running!');
89
- });
90
- export default app; // Optional: export app for testing or other programmatic uses