@dexterai/x402 1.4.1 → 1.5.1
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 +133 -1
- package/dist/adapters/index.d.cts +4 -4
- package/dist/adapters/index.d.ts +4 -4
- package/dist/client/index.cjs +184 -4
- package/dist/client/index.cjs.map +1 -1
- package/dist/client/index.d.cts +23 -5
- package/dist/client/index.d.ts +23 -5
- package/dist/client/index.js +184 -4
- package/dist/client/index.js.map +1 -1
- package/dist/{evm-ZDwQi4QL.d.ts → evm-71SZ7cjW.d.ts} +2 -2
- package/dist/{evm-BaoETN1Y.d.cts → evm-BYjwU6ZW.d.cts} +2 -2
- package/dist/react/index.cjs +325 -3
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +108 -4
- package/dist/react/index.d.ts +108 -4
- package/dist/react/index.js +324 -3
- package/dist/react/index.js.map +1 -1
- package/dist/server/index.cjs +253 -2
- package/dist/server/index.cjs.map +1 -1
- package/dist/server/index.d.cts +101 -3
- package/dist/server/index.d.ts +101 -3
- package/dist/server/index.js +240 -0
- package/dist/server/index.js.map +1 -1
- package/dist/{types-CQGDK_7X.d.cts → types--r7urkVI.d.cts} +1 -1
- package/dist/{types-DNx7-QUN.d.ts → types-BtpD4ULe.d.ts} +1 -1
- package/dist/{types-B7T6dZ-y.d.cts → types-CcVAaoro.d.cts} +64 -2
- package/dist/{types-B7T6dZ-y.d.ts → types-CcVAaoro.d.ts} +64 -2
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.js.map +1 -1
- package/dist/{x402-client-COrn-FQk.d.ts → x402-client-BxQWcK2Z.d.ts} +8 -1
- package/dist/{x402-client-DHmpVhEK.d.cts → x402-client-Dcm2FQ9f.d.cts} +8 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
|
|
25
25
|
x402 is a protocol for HTTP-native micropayments. When a server returns HTTP status `402 Payment Required`, it includes payment details in a `PAYMENT-REQUIRED` header. The client signs a payment transaction and retries the request with a `PAYMENT-SIGNATURE` header. The server verifies and settles the payment, then returns the protected content.
|
|
26
26
|
|
|
27
|
-
This SDK handles the entire flow automatically—you just call `fetch()` and payments happen transparently.
|
|
27
|
+
This SDK handles the entire flow automatically—you just call `fetch()` and payments happen transparently. With **Access Pass** mode, buyers pay once and get unlimited access for a time window—no per-request signing needed.
|
|
28
28
|
|
|
29
29
|
---
|
|
30
30
|
|
|
@@ -36,6 +36,8 @@ This SDK handles the entire flow automatically—you just call `fetch()` and pay
|
|
|
36
36
|
|
|
37
37
|
**Token-accurate LLM pricing.** Built-in [tiktoken](https://github.com/openai/tiktoken) support prices AI requests by actual token count. Works with OpenAI models out of the box, or bring your own rates for Anthropic, Gemini, Mistral, or local models.
|
|
38
38
|
|
|
39
|
+
**Access Pass.** Pay once, get unlimited access for a time window. Buyers connect a wallet, make one payment, and receive a JWT token that works like an API key—no per-request signing, no private keys in code. The Stripe replacement for crypto-native APIs.
|
|
40
|
+
|
|
39
41
|
**Full-stack.** Client SDK for browsers, server SDK for backends. React hooks, Express middleware patterns, facilitator client—everything you need.
|
|
40
42
|
|
|
41
43
|
**Multi-chain.** Solana and Base (Ethereum L2) with the same API. Add wallets for both and the SDK picks the right one automatically.
|
|
@@ -158,12 +160,18 @@ import { useX402Payment } from '@dexterai/x402/react';
|
|
|
158
160
|
// Server - Express middleware
|
|
159
161
|
import { x402Middleware } from '@dexterai/x402/server';
|
|
160
162
|
|
|
163
|
+
// Server - Access Pass (pay once, unlimited requests)
|
|
164
|
+
import { x402AccessPass } from '@dexterai/x402/server';
|
|
165
|
+
|
|
161
166
|
// Server - manual control
|
|
162
167
|
import { createX402Server } from '@dexterai/x402/server';
|
|
163
168
|
|
|
164
169
|
// Server - dynamic pricing
|
|
165
170
|
import { createDynamicPricing, createTokenPricing } from '@dexterai/x402/server';
|
|
166
171
|
|
|
172
|
+
// React - Access Pass hook
|
|
173
|
+
import { useAccessPass } from '@dexterai/x402/react';
|
|
174
|
+
|
|
167
175
|
// Chain adapters (advanced)
|
|
168
176
|
import { createSolanaAdapter, createEvmAdapter } from '@dexterai/x402/adapters';
|
|
169
177
|
|
|
@@ -221,6 +229,96 @@ Options:
|
|
|
221
229
|
- `facilitatorUrl` — Override facilitator (default: x402.dexter.cash)
|
|
222
230
|
- `verbose` — Enable debug logging
|
|
223
231
|
|
|
232
|
+
### Access Pass — Pay Once, Unlimited Requests
|
|
233
|
+
|
|
234
|
+
Replace API keys with time-limited access passes. Buyers make one payment and get a JWT token for unlimited requests during a time window.
|
|
235
|
+
|
|
236
|
+
**Server:**
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
import express from 'express';
|
|
240
|
+
import { x402AccessPass } from '@dexterai/x402/server';
|
|
241
|
+
|
|
242
|
+
const app = express();
|
|
243
|
+
|
|
244
|
+
// Protect all /api routes with access pass
|
|
245
|
+
app.use('/api', x402AccessPass({
|
|
246
|
+
payTo: 'YourSolanaAddress...',
|
|
247
|
+
tiers: {
|
|
248
|
+
'1h': '0.50', // $0.50 for 1 hour
|
|
249
|
+
'24h': '2.00', // $2.00 for 24 hours
|
|
250
|
+
},
|
|
251
|
+
ratePerHour: '0.50', // also accept custom durations
|
|
252
|
+
}));
|
|
253
|
+
|
|
254
|
+
app.get('/api/data', (req, res) => {
|
|
255
|
+
// Only runs with a valid access pass
|
|
256
|
+
res.json({ data: 'premium content' });
|
|
257
|
+
});
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
**Client (Node.js):**
|
|
261
|
+
|
|
262
|
+
```typescript
|
|
263
|
+
import { wrapFetch } from '@dexterai/x402/client';
|
|
264
|
+
|
|
265
|
+
const x402Fetch = wrapFetch(fetch, {
|
|
266
|
+
walletPrivateKey: process.env.SOLANA_PRIVATE_KEY,
|
|
267
|
+
accessPass: { preferTier: '1h', maxSpend: '1.00' },
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// First call: auto-purchases a 1-hour pass ($0.50 USDC)
|
|
271
|
+
const res1 = await x402Fetch('https://api.example.com/api/data');
|
|
272
|
+
|
|
273
|
+
// All subsequent calls for the next hour: uses cached JWT, zero payment
|
|
274
|
+
const res2 = await x402Fetch('https://api.example.com/api/data');
|
|
275
|
+
const res3 = await x402Fetch('https://api.example.com/api/data');
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
**React:**
|
|
279
|
+
|
|
280
|
+
```tsx
|
|
281
|
+
import { useAccessPass } from '@dexterai/x402/react';
|
|
282
|
+
|
|
283
|
+
function Dashboard() {
|
|
284
|
+
const { tiers, pass, isPassValid, purchasePass, fetch: apFetch } = useAccessPass({
|
|
285
|
+
wallets: { solana: solanaWallet },
|
|
286
|
+
resourceUrl: 'https://api.example.com',
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
return (
|
|
290
|
+
<div>
|
|
291
|
+
{!isPassValid && tiers?.map(t => (
|
|
292
|
+
<button key={t.id} onClick={() => purchasePass(t.id)}>
|
|
293
|
+
{t.label} — ${t.price}
|
|
294
|
+
</button>
|
|
295
|
+
))}
|
|
296
|
+
{isPassValid && <p>Pass active! {pass?.remainingSeconds}s remaining</p>}
|
|
297
|
+
<button onClick={() => apFetch('/api/data')}>Fetch Data</button>
|
|
298
|
+
</div>
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
**How it works:**
|
|
304
|
+
1. Client requests a protected endpoint → Server returns `402` with `X-ACCESS-PASS-TIERS` header
|
|
305
|
+
2. Client selects a tier and pays via x402 → Server verifies, settles, issues a JWT
|
|
306
|
+
3. Server returns `200` with `ACCESS-PASS` header containing the JWT
|
|
307
|
+
4. Client caches the JWT and includes it as `Authorization: Bearer <token>` on all subsequent requests
|
|
308
|
+
5. Server validates the JWT locally (no facilitator call) → instant response
|
|
309
|
+
|
|
310
|
+
Options:
|
|
311
|
+
- `payTo` — Address to receive payments
|
|
312
|
+
- `tiers` — Named duration tiers with prices (e.g., `{ '1h': '0.50' }`)
|
|
313
|
+
- `ratePerHour` — Rate for custom durations (buyer sends `?duration=<seconds>`)
|
|
314
|
+
- `network` — CAIP-2 network (default: Solana mainnet)
|
|
315
|
+
- `secret` — HMAC secret for JWT signing (auto-generated if not provided)
|
|
316
|
+
- `facilitatorUrl` — Override facilitator (default: x402.dexter.cash)
|
|
317
|
+
|
|
318
|
+
**[Live demo →](https://dexter.cash/access-pass)**
|
|
319
|
+
|
|
320
|
+
---
|
|
321
|
+
|
|
224
322
|
### Manual Server (Advanced)
|
|
225
323
|
|
|
226
324
|
For more control over the payment flow:
|
|
@@ -429,6 +527,18 @@ tiktoken's default encoding works well for most transformer models. Only use a c
|
|
|
429
527
|
| `maxAmountAtomic` | `string` | No | Maximum payment cap |
|
|
430
528
|
| `verbose` | `boolean` | No | Enable debug logging |
|
|
431
529
|
|
|
530
|
+
### `x402AccessPass(options)`
|
|
531
|
+
|
|
532
|
+
| Option | Type | Required | Description |
|
|
533
|
+
|--------|------|----------|-------------|
|
|
534
|
+
| `payTo` | `string` | Yes | Address to receive payments |
|
|
535
|
+
| `tiers` | `Record<string, string>` | One of `tiers` or `ratePerHour` | Named tiers (e.g., `{ '1h': '0.50' }`) |
|
|
536
|
+
| `ratePerHour` | `string` | One of `tiers` or `ratePerHour` | USD rate for custom durations |
|
|
537
|
+
| `network` | `string` | No | CAIP-2 network (default: Solana mainnet) |
|
|
538
|
+
| `secret` | `Buffer` | No | HMAC secret for JWT (auto-generated) |
|
|
539
|
+
| `facilitatorUrl` | `string` | No | Facilitator URL (default: x402.dexter.cash) |
|
|
540
|
+
| `verbose` | `boolean` | No | Enable debug logging |
|
|
541
|
+
|
|
432
542
|
### `useX402Payment(options)`
|
|
433
543
|
|
|
434
544
|
Returns:
|
|
@@ -444,6 +554,27 @@ Returns:
|
|
|
444
554
|
| `balances` | `Balance[]` | Token balances per chain |
|
|
445
555
|
| `refreshBalances` | `function` | Manual refresh |
|
|
446
556
|
| `reset` | `function` | Clear state |
|
|
557
|
+
| `accessPass` | `object?` | Active pass state (tier, expiresAt, remainingSeconds) |
|
|
558
|
+
|
|
559
|
+
### `useAccessPass(options)`
|
|
560
|
+
|
|
561
|
+
| Option | Type | Required | Description |
|
|
562
|
+
|--------|------|----------|-------------|
|
|
563
|
+
| `wallets` | `{ solana?, evm? }` | Yes | Multi-chain wallets |
|
|
564
|
+
| `resourceUrl` | `string` | Yes | The x402 resource base URL |
|
|
565
|
+
| `preferredNetwork` | `string` | No | Prefer this network |
|
|
566
|
+
| `autoConnect` | `boolean` | No | Auto-fetch tiers on mount (default: true) |
|
|
567
|
+
|
|
568
|
+
Returns:
|
|
569
|
+
|
|
570
|
+
| Property | Type | Description |
|
|
571
|
+
|----------|------|-------------|
|
|
572
|
+
| `tiers` | `AccessPassTier[]?` | Available tiers from server |
|
|
573
|
+
| `pass` | `object?` | Active pass (jwt, tier, expiresAt, remainingSeconds) |
|
|
574
|
+
| `isPassValid` | `boolean` | Whether pass is active and not expired |
|
|
575
|
+
| `purchasePass` | `function` | Buy a pass for a tier or custom duration |
|
|
576
|
+
| `isPurchasing` | `boolean` | Purchase in progress |
|
|
577
|
+
| `fetch` | `function` | Fetch with auto pass inclusion |
|
|
447
578
|
|
|
448
579
|
---
|
|
449
580
|
|
|
@@ -466,5 +597,6 @@ MIT — see [LICENSE](./LICENSE)
|
|
|
466
597
|
<p align="center">
|
|
467
598
|
<a href="https://x402.dexter.cash">Dexter Facilitator</a> ·
|
|
468
599
|
<a href="https://dexter.cash/sdk">Live Demo</a> ·
|
|
600
|
+
<a href="https://dexter.cash/access-pass">Access Pass Demo</a> ·
|
|
469
601
|
<a href="https://dexter.cash/onboard">Become a Seller</a>
|
|
470
602
|
</p>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { C as ChainAdapter } from '../types
|
|
2
|
-
export { A as AdapterConfig, B as BalanceInfo, G as GenericWallet, S as SignedTransaction, W as WalletSet } from '../types
|
|
3
|
-
export { A as ARBITRUM_ONE, B as BASE_MAINNET, h as BASE_SEPOLIA, j as ETHEREUM_MAINNET, E as EvmAdapter, k as EvmWallet, d as SOLANA_DEVNET, S as SOLANA_MAINNET, e as SOLANA_TESTNET, b as SolanaAdapter, f as SolanaWallet, a as createEvmAdapter, c as createSolanaAdapter, g as isEvmWallet, i as isSolanaWallet } from '../evm-
|
|
4
|
-
import '../types-
|
|
1
|
+
import { C as ChainAdapter } from '../types--r7urkVI.cjs';
|
|
2
|
+
export { A as AdapterConfig, B as BalanceInfo, G as GenericWallet, S as SignedTransaction, W as WalletSet } from '../types--r7urkVI.cjs';
|
|
3
|
+
export { A as ARBITRUM_ONE, B as BASE_MAINNET, h as BASE_SEPOLIA, j as ETHEREUM_MAINNET, E as EvmAdapter, k as EvmWallet, d as SOLANA_DEVNET, S as SOLANA_MAINNET, e as SOLANA_TESTNET, b as SolanaAdapter, f as SolanaWallet, a as createEvmAdapter, c as createSolanaAdapter, g as isEvmWallet, i as isSolanaWallet } from '../evm-BYjwU6ZW.cjs';
|
|
4
|
+
import '../types-CcVAaoro.cjs';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Create all default adapters
|
package/dist/adapters/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { C as ChainAdapter } from '../types-
|
|
2
|
-
export { A as AdapterConfig, B as BalanceInfo, G as GenericWallet, S as SignedTransaction, W as WalletSet } from '../types-
|
|
3
|
-
export { A as ARBITRUM_ONE, B as BASE_MAINNET, h as BASE_SEPOLIA, j as ETHEREUM_MAINNET, E as EvmAdapter, k as EvmWallet, d as SOLANA_DEVNET, S as SOLANA_MAINNET, e as SOLANA_TESTNET, b as SolanaAdapter, f as SolanaWallet, a as createEvmAdapter, c as createSolanaAdapter, g as isEvmWallet, i as isSolanaWallet } from '../evm-
|
|
4
|
-
import '../types-
|
|
1
|
+
import { C as ChainAdapter } from '../types-BtpD4ULe.js';
|
|
2
|
+
export { A as AdapterConfig, B as BalanceInfo, G as GenericWallet, S as SignedTransaction, W as WalletSet } from '../types-BtpD4ULe.js';
|
|
3
|
+
export { A as ARBITRUM_ONE, B as BASE_MAINNET, h as BASE_SEPOLIA, j as ETHEREUM_MAINNET, E as EvmAdapter, k as EvmWallet, d as SOLANA_DEVNET, S as SOLANA_MAINNET, e as SOLANA_TESTNET, b as SolanaAdapter, f as SolanaWallet, a as createEvmAdapter, c as createSolanaAdapter, g as isEvmWallet, i as isSolanaWallet } from '../evm-71SZ7cjW.js';
|
|
4
|
+
import '../types-CcVAaoro.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Create all default adapters
|
package/dist/client/index.cjs
CHANGED
|
@@ -614,10 +614,41 @@ function createX402Client(config) {
|
|
|
614
614
|
rpcUrls = {},
|
|
615
615
|
maxAmountAtomic,
|
|
616
616
|
fetch: customFetch = globalThis.fetch,
|
|
617
|
-
verbose = false
|
|
617
|
+
verbose = false,
|
|
618
|
+
accessPass: accessPassConfig
|
|
618
619
|
} = config;
|
|
619
620
|
const log = verbose ? console.log.bind(console, "[x402]") : () => {
|
|
620
621
|
};
|
|
622
|
+
const passCache = /* @__PURE__ */ new Map();
|
|
623
|
+
function getCachedPass(url) {
|
|
624
|
+
try {
|
|
625
|
+
const host = new URL(url).host;
|
|
626
|
+
const cached = passCache.get(host);
|
|
627
|
+
if (cached && cached.expiresAt > Date.now() / 1e3 + 10) {
|
|
628
|
+
return cached.jwt;
|
|
629
|
+
}
|
|
630
|
+
if (cached) {
|
|
631
|
+
passCache.delete(host);
|
|
632
|
+
}
|
|
633
|
+
} catch {
|
|
634
|
+
}
|
|
635
|
+
return null;
|
|
636
|
+
}
|
|
637
|
+
function cachePass(url, jwt) {
|
|
638
|
+
try {
|
|
639
|
+
const host = new URL(url).host;
|
|
640
|
+
const parts = jwt.split(".");
|
|
641
|
+
if (parts.length === 3) {
|
|
642
|
+
const payload = JSON.parse(atob(parts[1].replace(/-/g, "+").replace(/_/g, "/")));
|
|
643
|
+
if (payload.exp) {
|
|
644
|
+
passCache.set(host, { jwt, expiresAt: payload.exp });
|
|
645
|
+
log("Access pass cached for", host, "| expires:", new Date(payload.exp * 1e3).toISOString());
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
} catch {
|
|
649
|
+
log("Failed to cache access pass");
|
|
650
|
+
}
|
|
651
|
+
}
|
|
621
652
|
const wallets = walletSet || {};
|
|
622
653
|
if (legacyWallet && !wallets.solana && isSolanaWallet(legacyWallet)) {
|
|
623
654
|
wallets.solana = legacyWallet;
|
|
@@ -652,13 +683,160 @@ function createX402Client(config) {
|
|
|
652
683
|
function getRpcUrl(network, adapter) {
|
|
653
684
|
return rpcUrls[network] || adapter.getDefaultRpcUrl(network);
|
|
654
685
|
}
|
|
686
|
+
async function purchaseAccessPass(input, init, originalResponse, passInfo, url) {
|
|
687
|
+
let tierQuery = "";
|
|
688
|
+
if (accessPassConfig?.preferTier && passInfo.tiers) {
|
|
689
|
+
const match2 = passInfo.tiers.find((t) => t.id === accessPassConfig.preferTier);
|
|
690
|
+
if (match2) {
|
|
691
|
+
if (accessPassConfig.maxSpend && parseFloat(match2.price) > parseFloat(accessPassConfig.maxSpend)) {
|
|
692
|
+
throw new X402Error(
|
|
693
|
+
"access_pass_exceeds_max_spend",
|
|
694
|
+
`Access pass tier "${match2.id}" costs $${match2.price}, exceeds max spend $${accessPassConfig.maxSpend}`
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
tierQuery = `tier=${match2.id}`;
|
|
698
|
+
}
|
|
699
|
+
} else if (accessPassConfig?.preferDuration && passInfo.ratePerHour) {
|
|
700
|
+
tierQuery = `duration=${accessPassConfig.preferDuration}`;
|
|
701
|
+
} else if (passInfo.tiers && passInfo.tiers.length > 0) {
|
|
702
|
+
const cheapest = passInfo.tiers[0];
|
|
703
|
+
if (accessPassConfig?.maxSpend && parseFloat(cheapest.price) > parseFloat(accessPassConfig.maxSpend)) {
|
|
704
|
+
throw new X402Error(
|
|
705
|
+
"access_pass_exceeds_max_spend",
|
|
706
|
+
`Cheapest access pass costs $${cheapest.price}, exceeds max spend $${accessPassConfig?.maxSpend}`
|
|
707
|
+
);
|
|
708
|
+
}
|
|
709
|
+
tierQuery = `tier=${cheapest.id}`;
|
|
710
|
+
}
|
|
711
|
+
const passUrl = tierQuery ? url.includes("?") ? `${url}&${tierQuery}` : `${url}?${tierQuery}` : url;
|
|
712
|
+
log("Purchasing access pass:", tierQuery || "default tier");
|
|
713
|
+
const paymentRequiredHeader = originalResponse.headers.get("PAYMENT-REQUIRED");
|
|
714
|
+
if (!paymentRequiredHeader) return null;
|
|
715
|
+
let requirements;
|
|
716
|
+
try {
|
|
717
|
+
requirements = JSON.parse(atob(paymentRequiredHeader));
|
|
718
|
+
} catch {
|
|
719
|
+
return null;
|
|
720
|
+
}
|
|
721
|
+
const match = findPaymentOption(requirements.accepts);
|
|
722
|
+
if (!match) return null;
|
|
723
|
+
const { accept, adapter, wallet } = match;
|
|
724
|
+
if (adapter.name === "Solana" && !accept.extra?.feePayer) return null;
|
|
725
|
+
const USDC_MINTS = [
|
|
726
|
+
"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
|
|
727
|
+
"4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
|
|
728
|
+
"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
729
|
+
"0x036CbD53842c5426634e7929541eC2318f3dCF7e"
|
|
730
|
+
];
|
|
731
|
+
const decimals = accept.extra?.decimals ?? (USDC_MINTS.includes(accept.asset) ? 6 : void 0);
|
|
732
|
+
if (typeof decimals !== "number") return null;
|
|
733
|
+
const paymentAmount = accept.amount || accept.maxAmountRequired;
|
|
734
|
+
if (!paymentAmount) return null;
|
|
735
|
+
const rpcUrl = getRpcUrl(accept.network, adapter);
|
|
736
|
+
const balance = await adapter.getBalance(accept, wallet, rpcUrl);
|
|
737
|
+
const requiredAmount = Number(paymentAmount) / Math.pow(10, decimals);
|
|
738
|
+
if (balance < requiredAmount) {
|
|
739
|
+
throw new X402Error(
|
|
740
|
+
"insufficient_balance",
|
|
741
|
+
`Insufficient balance for access pass. Have $${balance.toFixed(4)}, need $${requiredAmount.toFixed(4)}`
|
|
742
|
+
);
|
|
743
|
+
}
|
|
744
|
+
const signedTx = await adapter.buildTransaction(accept, wallet, rpcUrl);
|
|
745
|
+
let payload;
|
|
746
|
+
if (adapter.name === "EVM") {
|
|
747
|
+
payload = JSON.parse(signedTx.serialized);
|
|
748
|
+
} else {
|
|
749
|
+
payload = { transaction: signedTx.serialized };
|
|
750
|
+
}
|
|
751
|
+
const originalUrl = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
752
|
+
let resolvedResource = requirements.resource;
|
|
753
|
+
if (typeof requirements.resource === "string") {
|
|
754
|
+
try {
|
|
755
|
+
resolvedResource = new URL(requirements.resource, originalUrl).toString();
|
|
756
|
+
} catch {
|
|
757
|
+
}
|
|
758
|
+
} else if (requirements.resource && typeof requirements.resource === "object" && "url" in requirements.resource) {
|
|
759
|
+
const rObj = requirements.resource;
|
|
760
|
+
try {
|
|
761
|
+
resolvedResource = { ...rObj, url: new URL(rObj.url, originalUrl).toString() };
|
|
762
|
+
} catch {
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
const paymentSignature = {
|
|
766
|
+
x402Version: accept.x402Version ?? 2,
|
|
767
|
+
resource: resolvedResource,
|
|
768
|
+
accepted: accept,
|
|
769
|
+
payload
|
|
770
|
+
};
|
|
771
|
+
const paymentSignatureHeader = btoa(JSON.stringify(paymentSignature));
|
|
772
|
+
const passResponse = await customFetch(passUrl, {
|
|
773
|
+
...init,
|
|
774
|
+
method: "POST",
|
|
775
|
+
headers: {
|
|
776
|
+
...init?.headers || {},
|
|
777
|
+
"Content-Type": "application/json",
|
|
778
|
+
"PAYMENT-SIGNATURE": paymentSignatureHeader
|
|
779
|
+
}
|
|
780
|
+
});
|
|
781
|
+
if (!passResponse.ok) {
|
|
782
|
+
log("Pass purchase failed:", passResponse.status);
|
|
783
|
+
return null;
|
|
784
|
+
}
|
|
785
|
+
const accessPassJwt = passResponse.headers.get("ACCESS-PASS");
|
|
786
|
+
if (!accessPassJwt) {
|
|
787
|
+
return passResponse;
|
|
788
|
+
}
|
|
789
|
+
cachePass(url, accessPassJwt);
|
|
790
|
+
log("Access pass purchased and cached");
|
|
791
|
+
const retryResponse = await customFetch(input, {
|
|
792
|
+
...init,
|
|
793
|
+
headers: {
|
|
794
|
+
...init?.headers || {},
|
|
795
|
+
"Authorization": `Bearer ${accessPassJwt}`
|
|
796
|
+
}
|
|
797
|
+
});
|
|
798
|
+
return retryResponse;
|
|
799
|
+
}
|
|
655
800
|
async function x402Fetch(input, init) {
|
|
656
|
-
|
|
801
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
802
|
+
log("Making request:", url);
|
|
803
|
+
if (accessPassConfig) {
|
|
804
|
+
const cachedJwt = getCachedPass(url);
|
|
805
|
+
if (cachedJwt) {
|
|
806
|
+
log("Using cached access pass");
|
|
807
|
+
const passResponse = await customFetch(input, {
|
|
808
|
+
...init,
|
|
809
|
+
headers: {
|
|
810
|
+
...init?.headers || {},
|
|
811
|
+
"Authorization": `Bearer ${cachedJwt}`
|
|
812
|
+
}
|
|
813
|
+
});
|
|
814
|
+
if (passResponse.status !== 401 && passResponse.status !== 402) {
|
|
815
|
+
return passResponse;
|
|
816
|
+
}
|
|
817
|
+
log("Cached pass rejected (status", passResponse.status, "), purchasing new pass");
|
|
818
|
+
try {
|
|
819
|
+
passCache.delete(new URL(url).host);
|
|
820
|
+
} catch {
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
}
|
|
657
824
|
const response = await customFetch(input, init);
|
|
658
825
|
if (response.status !== 402) {
|
|
659
826
|
return response;
|
|
660
827
|
}
|
|
661
828
|
log("Received 402 Payment Required");
|
|
829
|
+
const passTiersHeader = response.headers.get("X-ACCESS-PASS-TIERS");
|
|
830
|
+
if (accessPassConfig && passTiersHeader) {
|
|
831
|
+
log("Server offers access passes, purchasing...");
|
|
832
|
+
try {
|
|
833
|
+
const passInfo = JSON.parse(atob(passTiersHeader));
|
|
834
|
+
const passResponse = await purchaseAccessPass(input, init, response, passInfo, url);
|
|
835
|
+
if (passResponse) return passResponse;
|
|
836
|
+
} catch (e) {
|
|
837
|
+
log("Access pass purchase failed, falling back to per-request payment:", e);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
662
840
|
const paymentRequiredHeader = response.headers.get("PAYMENT-REQUIRED");
|
|
663
841
|
if (!paymentRequiredHeader) {
|
|
664
842
|
throw new X402Error(
|
|
@@ -875,7 +1053,8 @@ function wrapFetch(fetchImpl, options) {
|
|
|
875
1053
|
// facilitatorUrl is reserved for future use when we add facilitator selection
|
|
876
1054
|
rpcUrls,
|
|
877
1055
|
maxAmountAtomic,
|
|
878
|
-
verbose
|
|
1056
|
+
verbose,
|
|
1057
|
+
accessPass
|
|
879
1058
|
} = options;
|
|
880
1059
|
if (!walletPrivateKey && !evmPrivateKey) {
|
|
881
1060
|
throw new Error("At least one wallet private key is required (walletPrivateKey or evmPrivateKey)");
|
|
@@ -893,7 +1072,8 @@ function wrapFetch(fetchImpl, options) {
|
|
|
893
1072
|
rpcUrls,
|
|
894
1073
|
maxAmountAtomic,
|
|
895
1074
|
fetch: fetchImpl,
|
|
896
|
-
verbose
|
|
1075
|
+
verbose,
|
|
1076
|
+
accessPass
|
|
897
1077
|
};
|
|
898
1078
|
const client = createX402Client(clientConfig);
|
|
899
1079
|
return client.fetch.bind(client);
|