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 +19 -136
- package/dist/index.d.ts +33 -49
- package/dist/index.js +65 -689
- package/package.json +16 -13
- package/dist/server.d.ts +0 -2
- package/dist/server.js +0 -90
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
|
-
##
|
|
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 =
|
|
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
|
-
//
|
|
40
|
-
//
|
|
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
|
-
|
|
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: {
|
|
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.
|
|
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.
|
|
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
|
-
### `
|
|
114
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
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
|
-
|
|
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.
|
|
4
|
-
"description": "
|
|
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
|
-
"
|
|
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
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
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
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
|