@dexterai/x402 3.8.0 → 3.9.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 +310 -703
- package/dist/adapters/index.cjs +1 -1
- package/dist/adapters/index.d.cts +2 -2
- package/dist/adapters/index.d.ts +2 -2
- package/dist/adapters/index.js +1 -1
- package/dist/batch-settlement/index.d.cts +3 -3
- package/dist/batch-settlement/index.d.ts +3 -3
- package/dist/batch-settlement/seller/index.d.cts +4 -4
- package/dist/batch-settlement/seller/index.d.ts +4 -4
- package/dist/client/index.cjs +1 -1
- package/dist/client/index.d.cts +307 -237
- package/dist/client/index.d.ts +307 -237
- package/dist/client/index.js +1 -1
- package/dist/react/index.cjs +1 -1
- package/dist/react/index.d.cts +12 -4
- package/dist/react/index.d.ts +12 -4
- package/dist/react/index.js +1 -1
- package/dist/server/index.cjs +4 -4
- package/dist/server/index.d.cts +62 -4
- package/dist/server/index.d.ts +62 -4
- package/dist/server/index.js +4 -4
- package/dist/{types-XG8QvfyL.d.cts → types-C8lyIOmX.d.cts} +1 -1
- package/dist/{types-DllrEG_Z.d.ts → types-CTl7yVq6.d.ts} +1 -1
- package/dist/{types-pOwQlGEV.d.cts → types-CiPcPs0w.d.cts} +1 -1
- package/dist/{types-DDBPREEu.d.ts → types-D9VMq7In.d.ts} +1 -1
- package/dist/{types-htvWHuW3.d.cts → types-RxdlGPaG.d.cts} +122 -1
- package/dist/{types-htvWHuW3.d.ts → types-RxdlGPaG.d.ts} +122 -1
- package/dist/{sponsored-access-D96FgkQK.d.ts → x402-client-CHrU2Bs6.d.cts} +114 -63
- package/dist/{sponsored-access-D7H-womP.d.cts → x402-client-CzseAnIt.d.ts} +114 -63
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,14 +5,13 @@
|
|
|
5
5
|
<h1 align="center">@dexterai/x402</h1>
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
|
-
<strong>
|
|
8
|
+
<strong>HTTP-native micropayments for agents. Solana and the major EVM chains.</strong>
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
11
|
<p align="center">
|
|
12
12
|
<a href="https://www.npmjs.com/package/@dexterai/x402"><img src="https://img.shields.io/npm/v/@dexterai/x402.svg" alt="npm"></a>
|
|
13
13
|
<a href="https://nodejs.org"><img src="https://img.shields.io/badge/node-%3E=18-brightgreen.svg" alt="Node"></a>
|
|
14
14
|
<a href="https://dexter.cash/sdk"><img src="https://img.shields.io/badge/Live_Demo-dexter.cash%2Fsdk-blueviolet" alt="Live Demo"></a>
|
|
15
|
-
<a href="https://dexter.cash/opendexter"><img src="https://img.shields.io/badge/Marketplace-5%2C000%2B_APIs-success" alt="Marketplace"></a>
|
|
16
15
|
</p>
|
|
17
16
|
|
|
18
17
|
<p align="center">
|
|
@@ -23,206 +22,142 @@
|
|
|
23
22
|
|
|
24
23
|
## What is x402?
|
|
25
24
|
|
|
26
|
-
x402 is
|
|
25
|
+
x402 is HTTP's missing payment protocol. A server returns `402 Payment Required` with a `PAYMENT-REQUIRED` header describing what it wants paid; the client signs a payment, retries with `PAYMENT-SIGNATURE`, and gets the resource.
|
|
27
26
|
|
|
28
|
-
|
|
27
|
+
The audience this is built for in 2026 is **agents**: Claude, ChatGPT, Cursor, and the rest, making paid HTTP calls on behalf of humans. This SDK is the buyer side and the seller side, with USDC on Solana and the major EVM chains, behind a single API.
|
|
29
28
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
## Why This SDK?
|
|
33
|
-
|
|
34
|
-
**Monetize any API in minutes.** Add payments to your server in ~10 lines. Clients pay automatically—no checkout pages, no subscriptions, no invoices. Just HTTP.
|
|
35
|
-
|
|
36
|
-
**Dynamic pricing.** Charge based on usage: characters, tokens, records, pixels, API calls—whatever makes sense. Price scales with input, not fixed rates.
|
|
37
|
-
|
|
38
|
-
**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.
|
|
39
|
-
|
|
40
|
-
**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.
|
|
41
|
-
|
|
42
|
-
**Full-stack.** Client SDK for browsers, server SDK for backends. React hooks, Express middleware patterns, facilitator client—everything you need.
|
|
43
|
-
|
|
44
|
-
**Multi-chain.** Solana plus seven EVM chains (Base, Polygon, Arbitrum, Optimism, Avalanche, BSC, SKALE Base) with the same API. Add wallets for both families and the SDK picks the right one automatically.
|
|
45
|
-
|
|
46
|
-
**Works out of the box.** Built-in RPC proxy, pre-flight balance checks, automatic retry on 402. Uses the [Dexter facilitator](https://x402.dexter.cash) by default—Solana's most feature-rich x402 facilitator.
|
|
29
|
+
You call `payAndFetch()` on the client. You add `x402Middleware()` on the server. Payments happen.
|
|
47
30
|
|
|
48
31
|
---
|
|
49
32
|
|
|
50
|
-
##
|
|
51
|
-
|
|
52
|
-
When someone pays for your API through the Dexter facilitator, your endpoint is **automatically discovered and listed** in the [OpenDexter Marketplace](https://dexter.cash/opendexter) — a searchable directory of 5,000+ paid APIs used by AI agents.
|
|
53
|
-
|
|
54
|
-
No registration step needed. The flow:
|
|
55
|
-
|
|
56
|
-
1. You add `x402Middleware` to your endpoint (see Quick Start below)
|
|
57
|
-
2. An agent pays for your API → the facilitator processes the settlement
|
|
58
|
-
3. Your endpoint is auto-discovered, AI-named, and quality-verified
|
|
59
|
-
4. Agents find it via `x402_search` in any MCP client (ChatGPT, Claude, Cursor, etc.)
|
|
60
|
-
|
|
61
|
-
Quality-verified endpoints (score 75+) get promoted in search results. The verification bot tests your endpoint automatically — no action required on your part.
|
|
62
|
-
|
|
63
|
-
---
|
|
64
|
-
|
|
65
|
-
## Quick Start
|
|
66
|
-
|
|
67
|
-
### Install
|
|
33
|
+
## Quick start
|
|
68
34
|
|
|
69
35
|
```bash
|
|
70
36
|
npm install @dexterai/x402
|
|
71
37
|
```
|
|
72
38
|
|
|
73
|
-
###
|
|
74
|
-
|
|
75
|
-
The simplest way to make x402 payments from scripts:
|
|
39
|
+
### Pay for a resource (Node.js, any chain)
|
|
76
40
|
|
|
77
41
|
```typescript
|
|
78
|
-
import {
|
|
79
|
-
|
|
80
|
-
// Solana
|
|
81
|
-
const x402Fetch = wrapFetch(fetch, {
|
|
82
|
-
walletPrivateKey: process.env.SOLANA_PRIVATE_KEY,
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
// EVM (Base, Polygon, Arbitrum, Optimism, Avalanche, SKALE)
|
|
86
|
-
const x402Fetch = wrapFetch(fetch, {
|
|
87
|
-
evmPrivateKey: process.env.EVM_PRIVATE_KEY, // requires: npm install viem
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
// Both — SDK picks the chain with balance
|
|
91
|
-
const x402Fetch = wrapFetch(fetch, {
|
|
92
|
-
walletPrivateKey: process.env.SOLANA_PRIVATE_KEY,
|
|
93
|
-
evmPrivateKey: process.env.EVM_PRIVATE_KEY,
|
|
94
|
-
});
|
|
42
|
+
import { payAndFetch, createKeypairWallet, createEvmKeypairWallet } from '@dexterai/x402/client';
|
|
95
43
|
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
Check the payment receipt:
|
|
44
|
+
const solana = await createKeypairWallet(process.env.SOLANA_PRIVATE_KEY);
|
|
45
|
+
const evm = await createEvmKeypairWallet(process.env.EVM_PRIVATE_KEY); // requires: npm install viem
|
|
101
46
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
47
|
+
const result = await payAndFetch(
|
|
48
|
+
'https://api.example.com/protected',
|
|
49
|
+
{ method: 'GET' },
|
|
50
|
+
{ solana, evm },
|
|
51
|
+
{},
|
|
52
|
+
);
|
|
107
53
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
console.log(
|
|
54
|
+
if (result.ok && result.paid) {
|
|
55
|
+
const data = await result.response.json();
|
|
56
|
+
console.log(`Paid ${result.amountPaid} on ${result.network.bare}, tx ${result.txSignature}`);
|
|
57
|
+
} else if (result.ok && !result.paid) {
|
|
58
|
+
// Endpoint didn't demand payment; response came through unchanged.
|
|
59
|
+
const data = await result.response.json();
|
|
60
|
+
} else {
|
|
61
|
+
console.error(result.reason, result.detail);
|
|
111
62
|
}
|
|
112
63
|
```
|
|
113
64
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
```typescript
|
|
117
|
-
import { createX402Client } from '@dexterai/x402/client';
|
|
118
|
-
|
|
119
|
-
const client = createX402Client({
|
|
120
|
-
wallets: {
|
|
121
|
-
solana: solanaWallet,
|
|
122
|
-
evm: evmWallet,
|
|
123
|
-
},
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
// That's it. 402 responses are handled automatically.
|
|
127
|
-
const response = await client.fetch('https://api.example.com/protected');
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
RPC URLs are optional—the SDK uses Dexter's RPC proxy by default. Override if needed:
|
|
131
|
-
|
|
132
|
-
```typescript
|
|
133
|
-
const client = createX402Client({
|
|
134
|
-
wallets: { solana: solanaWallet },
|
|
135
|
-
rpcUrls: {
|
|
136
|
-
'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp': 'https://your-rpc.com',
|
|
137
|
-
},
|
|
138
|
-
});
|
|
139
|
-
```
|
|
65
|
+
`payAndFetch` is version-agnostic (handles x402 v1 and v2 transparently) and returns a discriminated `PayResult`. The `ok: true` branch is further split by `paid: true | false`, so a free 200 response is distinguishable from an actually-paid one. No throws for expected failures.
|
|
140
66
|
|
|
141
|
-
### React
|
|
67
|
+
### Pay for a resource (Browser, React)
|
|
142
68
|
|
|
143
|
-
|
|
69
|
+
`useX402Payment` accepts wallets from your existing providers (`@solana/wallet-adapter-react`, `wagmi`) and exposes a `fetch` that pays automatically.
|
|
144
70
|
|
|
145
71
|
```tsx
|
|
146
72
|
import { useX402Payment } from '@dexterai/x402/react';
|
|
147
|
-
import { useWallet } from '@solana/wallet-adapter-react';
|
|
148
|
-
import { useAccount } from 'wagmi';
|
|
73
|
+
import { useWallet } from '@solana/wallet-adapter-react';
|
|
74
|
+
import { useAccount } from 'wagmi';
|
|
149
75
|
|
|
150
|
-
function PayButton() {
|
|
151
|
-
// Get wallets from your existing providers
|
|
76
|
+
function PayButton({ url }: { url: string }) {
|
|
152
77
|
const solanaWallet = useWallet();
|
|
153
78
|
const evmWallet = useAccount();
|
|
154
79
|
|
|
155
80
|
const { fetch, isLoading, balances, transactionUrl } = useX402Payment({
|
|
156
|
-
wallets: {
|
|
157
|
-
solana: solanaWallet, // Pass directly - SDK handles the interface
|
|
158
|
-
evm: evmWallet,
|
|
159
|
-
},
|
|
81
|
+
wallets: { solana: solanaWallet, evm: evmWallet },
|
|
160
82
|
});
|
|
161
83
|
|
|
162
84
|
return (
|
|
163
85
|
<div>
|
|
164
86
|
<p>Balance: ${balances[0]?.balance.toFixed(2)}</p>
|
|
165
|
-
<button
|
|
166
|
-
|
|
167
|
-
disabled={isLoading || !solanaWallet.connected}
|
|
168
|
-
>
|
|
169
|
-
{isLoading ? 'Paying...' : 'Pay'}
|
|
87
|
+
<button onClick={() => fetch(url)} disabled={isLoading}>
|
|
88
|
+
{isLoading ? 'Paying…' : 'Pay'}
|
|
170
89
|
</button>
|
|
171
|
-
{transactionUrl && <a href={transactionUrl}>View
|
|
90
|
+
{transactionUrl && <a href={transactionUrl}>View transaction</a>}
|
|
172
91
|
</div>
|
|
173
92
|
);
|
|
174
93
|
}
|
|
175
94
|
```
|
|
176
95
|
|
|
96
|
+
### Protect an endpoint (server)
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
import express from 'express';
|
|
100
|
+
import { x402Middleware } from '@dexterai/x402/server';
|
|
101
|
+
|
|
102
|
+
const app = express();
|
|
103
|
+
|
|
104
|
+
app.get(
|
|
105
|
+
'/api/protected',
|
|
106
|
+
x402Middleware({
|
|
107
|
+
payTo: 'YourReceivingAddress',
|
|
108
|
+
amount: '0.01', // $0.01 USDC
|
|
109
|
+
network: 'eip155:8453', // Base. Pass an array for multi-chain.
|
|
110
|
+
}),
|
|
111
|
+
(req, res) => res.json({ data: 'protected content' }),
|
|
112
|
+
);
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
The handler only runs after a successful payment. Pass `network` as an array to accept across multiple chains; the buyer picks the chain they have balance on.
|
|
116
|
+
|
|
117
|
+
### Reading the receipt
|
|
118
|
+
|
|
119
|
+
`getPaymentReceipt(response)` returns the settled-payment info attached to any paid response (whether the payment came from `payAndFetch`, the legacy `wrapFetch`, or the React hook).
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
import { payAndFetch, getPaymentReceipt } from '@dexterai/x402/client';
|
|
123
|
+
|
|
124
|
+
const result = await payAndFetch(url, { method: 'GET' }, wallets, {});
|
|
125
|
+
if (result.ok && result.paid) {
|
|
126
|
+
const receipt = getPaymentReceipt(result.response);
|
|
127
|
+
console.log('tx:', receipt?.transaction, 'on', receipt?.network);
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
177
131
|
---
|
|
178
132
|
|
|
179
133
|
## Batch settlement (EVM)
|
|
180
134
|
|
|
181
|
-
Batch settlement lets a buyer pre-fund an escrow channel once, make many
|
|
182
|
-
**discrete** paid API calls against it with cheap off-chain vouchers, and then
|
|
183
|
-
close the channel — the seller's many charges are batched into a handful of
|
|
184
|
-
on-chain transactions instead of one per call. It amortizes gas across
|
|
185
|
-
high-frequency discrete purchasing.
|
|
135
|
+
Batch settlement lets a buyer pre-fund an escrow channel once, make many **discrete** paid API calls against it with cheap off-chain vouchers, and then close the channel. The seller's many charges are batched into a handful of on-chain transactions instead of one per call. It amortizes gas across high-frequency discrete purchasing.
|
|
186
136
|
|
|
187
|
-
It is **not** a streaming primitive
|
|
188
|
-
(Base, Arbitrum, Polygon). The buyer never needs a gas token: every step
|
|
189
|
-
(deposit, voucher, claim, settle, refund) is signature-based; the Dexter
|
|
190
|
-
facilitator submits the transactions and pays the gas.
|
|
137
|
+
It is **not** a streaming primitive; it batches discrete purchases. EVM only (Base, Arbitrum, Polygon). The buyer never needs a gas token: every step (deposit, voucher, claim, settle, refund) is signature-based; the Dexter facilitator submits the transactions and pays the gas.
|
|
191
138
|
|
|
192
139
|
### Buyer
|
|
193
140
|
|
|
194
141
|
```ts
|
|
195
142
|
import { openBatchChannel } from '@dexterai/x402/batch-settlement';
|
|
196
143
|
|
|
197
|
-
// Open a channel — signs an escrow deposit authorization (no gas needed).
|
|
198
144
|
const channel = await openBatchChannel({
|
|
199
145
|
wallet: evmWallet, // any { address, signTypedData }
|
|
200
146
|
network: 'eip155:8453', // Base
|
|
201
147
|
deposit: '0.30', // USDC escrowed for this channel
|
|
202
148
|
});
|
|
203
149
|
|
|
204
|
-
// Each call signs a cumulative voucher against the channel.
|
|
205
150
|
const a = await channel.fetch('https://api.example.com/v1/data');
|
|
206
151
|
const b = await channel.fetch('https://api.example.com/v1/data');
|
|
207
152
|
|
|
208
153
|
console.log(channel.state); // { deposited: '0.3', spent: '0.16', remaining: '0.14' }
|
|
209
154
|
|
|
210
|
-
// Done buying — signal you're finished with the channel.
|
|
211
155
|
const { closed } = await channel.close();
|
|
212
|
-
// closed === true. This is an intent signal, not a settlement: it does not
|
|
213
|
-
// move funds. The seller's settlement (below) claims what you spent and
|
|
214
|
-
// refunds the rest of your escrow automatically on the normal path.
|
|
215
156
|
```
|
|
216
157
|
|
|
217
|
-
Each `openBatchChannel` call opens a
|
|
218
|
-
channel-config salt is generated, so a buyer can hold several independent
|
|
219
|
-
channels with the same seller over time. The salt is exposed as
|
|
220
|
-
`channel.salt` — persist it if you will later need to resume that exact
|
|
221
|
-
channel.
|
|
158
|
+
Each `openBatchChannel` call opens a new channel: a fresh random channel-config salt is generated, so a buyer can hold several independent channels with the same seller over time. The salt is exposed as `channel.salt`; persist it if you will later need to resume that exact channel.
|
|
222
159
|
|
|
223
|
-
|
|
224
|
-
the wallet, network, and the **salt** of the channel being resumed (the
|
|
225
|
-
`channelId` alone is not enough — it cannot be reversed to a salt):
|
|
160
|
+
Resume after a process restart with the wallet, network, and the channel's salt:
|
|
226
161
|
|
|
227
162
|
```ts
|
|
228
163
|
import { resumeBatchChannel } from '@dexterai/x402/batch-settlement';
|
|
@@ -230,89 +165,182 @@ import { resumeBatchChannel } from '@dexterai/x402/batch-settlement';
|
|
|
230
165
|
const channel = await resumeBatchChannel({
|
|
231
166
|
wallet: evmWallet,
|
|
232
167
|
network: 'eip155:8453',
|
|
233
|
-
salt: savedSalt,
|
|
168
|
+
salt: savedSalt,
|
|
234
169
|
});
|
|
235
170
|
```
|
|
236
171
|
|
|
237
|
-
Channel state auto-persists (localStorage in the browser, a file under
|
|
238
|
-
`~/.dexter-x402/channels` in Node); the resumed channel's accounting is
|
|
239
|
-
recovered from storage, or from on-chain state if storage was lost. Pass a
|
|
240
|
-
custom `store` (any `ChannelStore`) to persist elsewhere. To deterministically
|
|
241
|
-
reopen a specific channel instead of getting a fresh one, pass an explicit
|
|
242
|
-
`salt` to `openBatchChannel`.
|
|
172
|
+
Channel state auto-persists (localStorage in the browser, a file under `~/.dexter-x402/channels` in Node); the resumed channel's accounting is recovered from storage, or from on-chain state if storage was lost.
|
|
243
173
|
|
|
244
|
-
#### Escape hatch
|
|
174
|
+
#### Escape hatch: `forceWithdraw()` / `finalizeWithdraw()`
|
|
245
175
|
|
|
246
|
-
If the seller never settles, the buyer can reclaim unspent escrow directly via
|
|
247
|
-
the channel contract's timed withdrawal:
|
|
176
|
+
If the seller never settles, the buyer can reclaim unspent escrow directly via the channel contract's timed withdrawal:
|
|
248
177
|
|
|
249
178
|
```ts
|
|
250
|
-
// 1. Start the on-chain timed withdrawal.
|
|
251
179
|
await channel.forceWithdraw();
|
|
252
|
-
|
|
253
|
-
// 2. After the channel's withdraw delay elapses, finalize it.
|
|
180
|
+
// after the channel's withdraw delay elapses
|
|
254
181
|
await channel.finalizeWithdraw();
|
|
255
182
|
```
|
|
256
183
|
|
|
257
|
-
|
|
258
|
-
the seller's settlement returns unspent escrow on the standard path. Unlike
|
|
259
|
-
every other batch-settlement step, the escape hatch **costs the buyer gas**: it
|
|
260
|
-
submits transactions itself, so the buyer's wallet must be transaction-capable
|
|
261
|
-
(it must expose a `sendTransaction` method). A signature-only wallet cannot use
|
|
262
|
-
the escape hatch.
|
|
184
|
+
Last-resort safety net; normal operation never needs it. Unlike every other batch-settlement step, the escape hatch costs the buyer gas: the wallet must expose a `sendTransaction` method.
|
|
263
185
|
|
|
264
186
|
### Seller
|
|
265
187
|
|
|
266
|
-
`createBatchSettlementSeller(config)` returns
|
|
267
|
-
Express request handler — mount it directly. It accepts batch-settlement
|
|
268
|
-
payments (each incoming voucher is verified and persisted to channel storage)
|
|
269
|
-
and collects them (claim → settle → refund). Dexter operates the delegate
|
|
270
|
-
authorizer, so the seller manages no signing key:
|
|
188
|
+
`createBatchSettlementSeller(config)` returns an Express request handler. Mount it directly; it accepts vouchers, persists them, and settles in the background. Dexter operates the delegate authorizer, so the seller manages no signing key.
|
|
271
189
|
|
|
272
190
|
```ts
|
|
273
191
|
import { createBatchSettlementSeller } from '@dexterai/x402/batch-settlement/seller';
|
|
274
192
|
|
|
275
193
|
const seller = createBatchSettlementSeller({
|
|
276
194
|
payTo: '0xYourReceivingAddress',
|
|
277
|
-
network: 'eip155:8453',
|
|
278
|
-
price: '0.08',
|
|
195
|
+
network: 'eip155:8453',
|
|
196
|
+
price: '0.08',
|
|
279
197
|
});
|
|
280
198
|
|
|
281
|
-
// The seller object IS the Express handler — mount it directly.
|
|
282
199
|
app.use('/api/data', seller);
|
|
283
200
|
|
|
284
|
-
// Auto-settlement runs in the background by default. On shutdown, stop the
|
|
285
|
-
// seller — this flushes a final settle so no collected vouchers are lost.
|
|
286
201
|
process.on('SIGTERM', async () => {
|
|
287
|
-
await seller.stop();
|
|
202
|
+
await seller.stop(); // flushes a final settle so no vouchers are lost
|
|
288
203
|
});
|
|
289
204
|
```
|
|
290
205
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
206
|
+
Mounting via `x402Middleware` also works. With `scheme: 'batch-settlement'` it returns the same callable seller object, so you keep the `.stop()` / `.closeAll()` / `.closeChannel()` handle.
|
|
207
|
+
|
|
208
|
+
---
|
|
294
209
|
|
|
295
|
-
|
|
296
|
-
returns the same callable seller object, so you still get a `.stop()` /
|
|
297
|
-
`.closeAll()` / `.closeChannel()` handle:
|
|
210
|
+
## Discovery (bazaar extension)
|
|
298
211
|
|
|
299
|
-
|
|
212
|
+
Shipped in 3.8.0. The bazaar extension makes any `x402Middleware`-protected route discoverable through the official x402 bazaar spec, so agents browsing a bazaar-compliant indexer find your endpoint by capability, not by URL.
|
|
213
|
+
|
|
214
|
+
The 402 response carries a spec-compliant `extensions.bazaar` block describing the route's inputs, output schema, and template path. Discovery indexers read it and surface your endpoint in agent-facing catalogs.
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
import {
|
|
218
|
+
x402Middleware,
|
|
219
|
+
bazaarExtension,
|
|
220
|
+
declareDiscoveryExtension,
|
|
221
|
+
} from '@dexterai/x402/server';
|
|
222
|
+
|
|
223
|
+
app.post(
|
|
224
|
+
'/v1/translate',
|
|
225
|
+
x402Middleware({
|
|
226
|
+
payTo: '...',
|
|
227
|
+
amount: '0.02',
|
|
228
|
+
network: 'eip155:8453',
|
|
229
|
+
extensions: [bazaarExtension()],
|
|
230
|
+
declarations: {
|
|
231
|
+
...declareDiscoveryExtension({
|
|
232
|
+
method: 'POST',
|
|
233
|
+
bodyType: 'json',
|
|
234
|
+
inputSchema: {
|
|
235
|
+
properties: {
|
|
236
|
+
text: { type: 'string', description: 'Source text' },
|
|
237
|
+
targetLang: { type: 'string', description: 'ISO 639-1 code' },
|
|
238
|
+
},
|
|
239
|
+
required: ['text', 'targetLang'],
|
|
240
|
+
},
|
|
241
|
+
output: {
|
|
242
|
+
example: { translation: 'Bonjour' },
|
|
243
|
+
},
|
|
244
|
+
}),
|
|
245
|
+
},
|
|
246
|
+
}),
|
|
247
|
+
(req, res) => res.json({ translation: translate(req.body) }),
|
|
248
|
+
);
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
`extensions` is opt-in: middleware without an `extensions` array emits a 402 byte-identical to pre-3.8.0 behavior. `method` may be omitted from `declareDiscoveryExtension`; the extension stamps the actual request method at 402 time.
|
|
252
|
+
|
|
253
|
+
Failure isolation: if an extension throws, it's caught, logged, and skipped. The 402 still goes out, just without that key. The payment path is never affected.
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
## Sponsored Access (Instinct ad network)
|
|
258
|
+
|
|
259
|
+
This is how MCP agents (Claude, ChatGPT, Cursor) see your sponsored placements. When an agent pays for an API through Dexter's facilitator, a matched recommendation can be injected into the settlement receipt; the agent's LLM reads it and may call the suggested resource next. Both blockchain transactions become proof of the conversion.
|
|
260
|
+
|
|
261
|
+
The buyer-side helpers are wired into every MCP `fetch` tool in the Dexter ecosystem, plus the human-facing receipt UI on x402gle. If you're shipping an x402 endpoint, sponsored access is how you reach the agents already using paid APIs.
|
|
262
|
+
|
|
263
|
+
### Seller: enable recommendation injection
|
|
264
|
+
|
|
265
|
+
```typescript
|
|
300
266
|
import { x402Middleware } from '@dexterai/x402/server';
|
|
301
267
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
268
|
+
app.get(
|
|
269
|
+
'/api/data',
|
|
270
|
+
x402Middleware({
|
|
271
|
+
payTo: '...',
|
|
272
|
+
amount: '0.01',
|
|
273
|
+
sponsoredAccess: true, // injects _x402_sponsored into JSON responses
|
|
274
|
+
}),
|
|
275
|
+
(req, res) => res.json({ data: 'content' }),
|
|
276
|
+
);
|
|
277
|
+
// Response: { _x402_sponsored: [{ resourceUrl, description, sponsor }], data: 'content' }
|
|
278
|
+
```
|
|
308
279
|
|
|
309
|
-
|
|
310
|
-
|
|
280
|
+
For custom placement (where in the body the recommendation appears, conversion logging, etc.), pass an object instead of `true`:
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
sponsoredAccess: {
|
|
284
|
+
inject: (body, recs) => ({ ...body, related_tools: recs }),
|
|
285
|
+
onMatch: (recs, settlement) => log(`matched ${recs.length} for tx ${settlement.transaction}`),
|
|
286
|
+
},
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### Buyer: read recommendations off a paid response
|
|
290
|
+
|
|
291
|
+
```typescript
|
|
292
|
+
import {
|
|
293
|
+
payAndFetch,
|
|
294
|
+
getSponsoredRecommendations,
|
|
295
|
+
fireImpressionBeacon,
|
|
296
|
+
} from '@dexterai/x402/client';
|
|
297
|
+
|
|
298
|
+
const result = await payAndFetch(url, { method: 'GET' }, wallets, {});
|
|
299
|
+
if (result.ok && result.paid) {
|
|
300
|
+
const recs = getSponsoredRecommendations(result.response);
|
|
301
|
+
if (recs) {
|
|
302
|
+
for (const rec of recs) {
|
|
303
|
+
console.log(`${rec.sponsor}: ${rec.description} (${rec.resourceUrl})`);
|
|
304
|
+
}
|
|
305
|
+
await fireImpressionBeacon(result.response);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
311
308
|
```
|
|
312
309
|
|
|
310
|
+
### React: recommendations in the hook
|
|
311
|
+
|
|
312
|
+
```tsx
|
|
313
|
+
import { useX402Payment } from '@dexterai/x402/react';
|
|
314
|
+
|
|
315
|
+
function PayButton() {
|
|
316
|
+
const { fetch, isLoading, sponsoredRecommendations } = useX402Payment({ wallets });
|
|
317
|
+
|
|
318
|
+
return (
|
|
319
|
+
<div>
|
|
320
|
+
<button onClick={() => fetch(url)} disabled={isLoading}>Pay</button>
|
|
321
|
+
{sponsoredRecommendations?.map((rec, i) => (
|
|
322
|
+
<a key={i} href={rec.resourceUrl}>{rec.sponsor}: {rec.description}</a>
|
|
323
|
+
))}
|
|
324
|
+
</div>
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### Advertise
|
|
330
|
+
|
|
331
|
+
Campaign creation is x402-gated at `x402ads.io`. Your wallet is your identity. Full advertiser guide at [docs.dexter.cash/docs/sponsored-access/for-advertisers](https://docs.dexter.cash/docs/sponsored-access/for-advertisers).
|
|
332
|
+
|
|
313
333
|
---
|
|
314
334
|
|
|
315
|
-
##
|
|
335
|
+
## Auto-listing in OpenDexter
|
|
336
|
+
|
|
337
|
+
When an agent pays for your API through the Dexter facilitator, your endpoint is auto-discovered, AI-named, and quality-tested. Quality-verified endpoints surface in `x402_search` results across MCP clients (ChatGPT, Claude, Cursor). No registration step.
|
|
338
|
+
|
|
339
|
+
Browse the live catalog at [dexter.cash/opendexter](https://dexter.cash/opendexter).
|
|
340
|
+
|
|
341
|
+
---
|
|
342
|
+
|
|
343
|
+
## Supported networks
|
|
316
344
|
|
|
317
345
|
All networks supported by the [Dexter facilitator](https://x402.dexter.cash/supported). USDC on every chain.
|
|
318
346
|
|
|
@@ -338,25 +366,9 @@ All networks supported by the [Dexter facilitator](https://x402.dexter.cash/supp
|
|
|
338
366
|
| Base Sepolia | `eip155:84532` |
|
|
339
367
|
| SKALE Base Sepolia | `eip155:324705682` |
|
|
340
368
|
|
|
341
|
-
|
|
369
|
+
Multi-chain endpoints accept payments on any chain in the list. The buyer picks:
|
|
342
370
|
|
|
343
371
|
```typescript
|
|
344
|
-
// Same address across all EVM chains
|
|
345
|
-
app.get('/api/data', x402Middleware({
|
|
346
|
-
payTo: '0xYourAddress',
|
|
347
|
-
amount: '0.01',
|
|
348
|
-
network: [
|
|
349
|
-
'eip155:8453', // Base
|
|
350
|
-
'eip155:137', // Polygon
|
|
351
|
-
'eip155:42161', // Arbitrum
|
|
352
|
-
'eip155:10', // Optimism
|
|
353
|
-
'eip155:43114', // Avalanche
|
|
354
|
-
'eip155:56', // BSC
|
|
355
|
-
'eip155:1187947933', // SKALE Base (zero gas)
|
|
356
|
-
],
|
|
357
|
-
}));
|
|
358
|
-
|
|
359
|
-
// Different addresses per chain family
|
|
360
372
|
app.get('/api/data', x402Middleware({
|
|
361
373
|
payTo: {
|
|
362
374
|
'solana:*': 'YourSolanaAddress...',
|
|
@@ -378,40 +390,32 @@ app.get('/api/data', x402Middleware({
|
|
|
378
390
|
|
|
379
391
|
---
|
|
380
392
|
|
|
381
|
-
## Package
|
|
393
|
+
## Package exports
|
|
382
394
|
|
|
383
395
|
```typescript
|
|
384
|
-
// Client
|
|
385
|
-
import {
|
|
396
|
+
// Client: canonical entrypoint
|
|
397
|
+
import { payAndFetch, createKeypairWallet, createEvmKeypairWallet, getPaymentReceipt } from '@dexterai/x402/client';
|
|
386
398
|
|
|
387
|
-
// Client
|
|
388
|
-
import {
|
|
399
|
+
// Client: sponsored access reader
|
|
400
|
+
import { getSponsoredRecommendations, fireImpressionBeacon } from '@dexterai/x402/client';
|
|
389
401
|
|
|
390
|
-
//
|
|
391
|
-
import { openBatchChannel } from '@dexterai/x402/batch-settlement';
|
|
392
|
-
|
|
393
|
-
// Batch settlement - EVM escrow channel seller runtime
|
|
394
|
-
import { createBatchSettlementSeller } from '@dexterai/x402/batch-settlement/seller';
|
|
395
|
-
|
|
396
|
-
// React hook
|
|
402
|
+
// React
|
|
397
403
|
import { useX402Payment } from '@dexterai/x402/react';
|
|
398
404
|
|
|
399
|
-
// Server
|
|
405
|
+
// Server: middleware
|
|
400
406
|
import { x402Middleware } from '@dexterai/x402/server';
|
|
401
407
|
|
|
402
|
-
// Server
|
|
403
|
-
import {
|
|
408
|
+
// Server: discovery (bazaar extension)
|
|
409
|
+
import { bazaarExtension, declareDiscoveryExtension } from '@dexterai/x402/server';
|
|
404
410
|
|
|
405
|
-
// Server
|
|
411
|
+
// Server: manual control
|
|
406
412
|
import { createX402Server } from '@dexterai/x402/server';
|
|
407
413
|
|
|
408
|
-
//
|
|
409
|
-
import {
|
|
410
|
-
|
|
411
|
-
// React - Access Pass hook
|
|
412
|
-
import { useAccessPass } from '@dexterai/x402/react';
|
|
414
|
+
// Batch settlement
|
|
415
|
+
import { openBatchChannel, resumeBatchChannel } from '@dexterai/x402/batch-settlement';
|
|
416
|
+
import { createBatchSettlementSeller } from '@dexterai/x402/batch-settlement/seller';
|
|
413
417
|
|
|
414
|
-
//
|
|
418
|
+
// Adapters (advanced)
|
|
415
419
|
import { createSolanaAdapter, createEvmAdapter } from '@dexterai/x402/adapters';
|
|
416
420
|
|
|
417
421
|
// Utilities
|
|
@@ -425,142 +429,17 @@ import { toAtomicUnits, fromAtomicUnits } from '@dexterai/x402/utils';
|
|
|
425
429
|
```typescript
|
|
426
430
|
import { toAtomicUnits, fromAtomicUnits } from '@dexterai/x402/utils';
|
|
427
431
|
|
|
428
|
-
|
|
429
|
-
toAtomicUnits(
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
// Convert atomic units back to dollars (for display)
|
|
433
|
-
fromAtomicUnits('50000', 6); // 0.05
|
|
434
|
-
fromAtomicUnits(1500000n, 6); // 1.5
|
|
432
|
+
toAtomicUnits(0.05, 6); // '50000'
|
|
433
|
+
toAtomicUnits(1.50, 6); // '1500000'
|
|
434
|
+
fromAtomicUnits('50000', 6); // 0.05
|
|
435
|
+
fromAtomicUnits(1500000n, 6); // 1.5
|
|
435
436
|
```
|
|
436
437
|
|
|
437
438
|
---
|
|
438
439
|
|
|
439
|
-
##
|
|
440
|
-
|
|
441
|
-
### Express Middleware — NEW!
|
|
442
|
-
|
|
443
|
-
One-liner payment protection for any Express endpoint:
|
|
444
|
-
|
|
445
|
-
```typescript
|
|
446
|
-
import express from 'express';
|
|
447
|
-
import { x402Middleware } from '@dexterai/x402/server';
|
|
448
|
-
|
|
449
|
-
const app = express();
|
|
450
|
-
|
|
451
|
-
app.get('/api/protected',
|
|
452
|
-
x402Middleware({
|
|
453
|
-
payTo: 'YourSolanaAddress...',
|
|
454
|
-
amount: '0.01', // $0.01 USD
|
|
455
|
-
}),
|
|
456
|
-
(req, res) => {
|
|
457
|
-
// This only runs after successful payment
|
|
458
|
-
res.json({ data: 'protected content' });
|
|
459
|
-
}
|
|
460
|
-
);
|
|
461
|
-
```
|
|
462
|
-
|
|
463
|
-
Options:
|
|
464
|
-
- `payTo` — Address to receive payments
|
|
465
|
-
- `amount` — Price in USD (e.g., `'0.01'` for 1 cent)
|
|
466
|
-
- `network` — CAIP-2 network (default: Solana mainnet)
|
|
467
|
-
- `description` — Human-readable description
|
|
468
|
-
- `facilitatorUrl` — Override facilitator (default: x402.dexter.cash)
|
|
469
|
-
- `verbose` — Enable debug logging
|
|
470
|
-
|
|
471
|
-
### Access Pass — Pay Once, Unlimited Requests
|
|
440
|
+
## Manual server (advanced)
|
|
472
441
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
**Server:**
|
|
476
|
-
|
|
477
|
-
```typescript
|
|
478
|
-
import express from 'express';
|
|
479
|
-
import { x402AccessPass } from '@dexterai/x402/server';
|
|
480
|
-
|
|
481
|
-
const app = express();
|
|
482
|
-
|
|
483
|
-
// Protect all /api routes with access pass
|
|
484
|
-
app.use('/api', x402AccessPass({
|
|
485
|
-
payTo: 'YourSolanaAddress...',
|
|
486
|
-
tiers: {
|
|
487
|
-
'1h': '0.50', // $0.50 for 1 hour
|
|
488
|
-
'24h': '2.00', // $2.00 for 24 hours
|
|
489
|
-
},
|
|
490
|
-
ratePerHour: '0.50', // also accept custom durations
|
|
491
|
-
}));
|
|
492
|
-
|
|
493
|
-
app.get('/api/data', (req, res) => {
|
|
494
|
-
// Only runs with a valid access pass
|
|
495
|
-
res.json({ data: 'premium content' });
|
|
496
|
-
});
|
|
497
|
-
```
|
|
498
|
-
|
|
499
|
-
**Client (Node.js):**
|
|
500
|
-
|
|
501
|
-
```typescript
|
|
502
|
-
import { wrapFetch } from '@dexterai/x402/client';
|
|
503
|
-
|
|
504
|
-
const x402Fetch = wrapFetch(fetch, {
|
|
505
|
-
walletPrivateKey: process.env.SOLANA_PRIVATE_KEY,
|
|
506
|
-
accessPass: { preferTier: '1h', maxSpend: '1.00' },
|
|
507
|
-
});
|
|
508
|
-
|
|
509
|
-
// First call: auto-purchases a 1-hour pass ($0.50 USDC)
|
|
510
|
-
const res1 = await x402Fetch('https://api.example.com/api/data');
|
|
511
|
-
|
|
512
|
-
// All subsequent calls for the next hour: uses cached JWT, zero payment
|
|
513
|
-
const res2 = await x402Fetch('https://api.example.com/api/data');
|
|
514
|
-
const res3 = await x402Fetch('https://api.example.com/api/data');
|
|
515
|
-
```
|
|
516
|
-
|
|
517
|
-
**React:**
|
|
518
|
-
|
|
519
|
-
```tsx
|
|
520
|
-
import { useAccessPass } from '@dexterai/x402/react';
|
|
521
|
-
|
|
522
|
-
function Dashboard() {
|
|
523
|
-
const { tiers, pass, isPassValid, purchasePass, fetch: apFetch } = useAccessPass({
|
|
524
|
-
wallets: { solana: solanaWallet },
|
|
525
|
-
resourceUrl: 'https://api.example.com',
|
|
526
|
-
});
|
|
527
|
-
|
|
528
|
-
return (
|
|
529
|
-
<div>
|
|
530
|
-
{!isPassValid && tiers?.map(t => (
|
|
531
|
-
<button key={t.id} onClick={() => purchasePass(t.id)}>
|
|
532
|
-
{t.label} — ${t.price}
|
|
533
|
-
</button>
|
|
534
|
-
))}
|
|
535
|
-
{isPassValid && <p>Pass active! {pass?.remainingSeconds}s remaining</p>}
|
|
536
|
-
<button onClick={() => apFetch('/api/data')}>Fetch Data</button>
|
|
537
|
-
</div>
|
|
538
|
-
);
|
|
539
|
-
}
|
|
540
|
-
```
|
|
541
|
-
|
|
542
|
-
**How it works:**
|
|
543
|
-
1. Client requests a protected endpoint → Server returns `402` with `X-ACCESS-PASS-TIERS` header
|
|
544
|
-
2. Client selects a tier and pays via x402 → Server verifies, settles, issues a JWT
|
|
545
|
-
3. Server returns `200` with `ACCESS-PASS` header containing the JWT
|
|
546
|
-
4. Client caches the JWT and includes it as `Authorization: Bearer <token>` on all subsequent requests
|
|
547
|
-
5. Server validates the JWT locally (no facilitator call) → instant response
|
|
548
|
-
|
|
549
|
-
Options:
|
|
550
|
-
- `payTo` — Address to receive payments
|
|
551
|
-
- `tiers` — Named duration tiers with prices (e.g., `{ '1h': '0.50' }`)
|
|
552
|
-
- `ratePerHour` — Rate for custom durations (buyer sends `?duration=<seconds>`)
|
|
553
|
-
- `network` — CAIP-2 network (default: Solana mainnet)
|
|
554
|
-
- `secret` — HMAC secret for JWT signing (auto-generated if not provided)
|
|
555
|
-
- `facilitatorUrl` — Override facilitator (default: x402.dexter.cash)
|
|
556
|
-
|
|
557
|
-
**[Live demo →](https://dexter.cash/access-pass)**
|
|
558
|
-
|
|
559
|
-
---
|
|
560
|
-
|
|
561
|
-
### Manual Server (Advanced)
|
|
562
|
-
|
|
563
|
-
For more control over the payment flow:
|
|
442
|
+
For full control over the payment flow without `x402Middleware`:
|
|
564
443
|
|
|
565
444
|
```typescript
|
|
566
445
|
import { createX402Server } from '@dexterai/x402/server';
|
|
@@ -570,7 +449,6 @@ const server = createX402Server({
|
|
|
570
449
|
network: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp',
|
|
571
450
|
});
|
|
572
451
|
|
|
573
|
-
// In your route handler
|
|
574
452
|
app.post('/protected', async (req, res) => {
|
|
575
453
|
const paymentSig = req.headers['payment-signature'];
|
|
576
454
|
|
|
@@ -588,392 +466,121 @@ app.post('/protected', async (req, res) => {
|
|
|
588
466
|
return res.status(402).json({ error: result.errorReason });
|
|
589
467
|
}
|
|
590
468
|
|
|
591
|
-
res.json({ data: '
|
|
469
|
+
res.json({ data: 'protected content' });
|
|
592
470
|
});
|
|
593
471
|
```
|
|
594
472
|
|
|
595
473
|
---
|
|
596
474
|
|
|
597
|
-
##
|
|
475
|
+
## Legacy capabilities
|
|
598
476
|
|
|
599
|
-
|
|
477
|
+
Several v1-era helpers ship with `@deprecated` markers in 3.9. They keep working. The markers exist to steer new code at the canonical paths. Each has a JSDoc pointing at its migration target.
|
|
600
478
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
-
|
|
479
|
+
| Symbol | Migration target |
|
|
480
|
+
|---|---|
|
|
481
|
+
| `wrapFetch` (`@dexterai/x402/client`) | `payAndFetch` (version-agnostic, discriminated return type) |
|
|
482
|
+
| `createX402Client` (`@dexterai/x402/client`) | `payAndFetch` |
|
|
483
|
+
| `x402AccessPass`, `useAccessPass` | No replacement. Per-request `x402Middleware` + `payAndFetch` covers the same usage pattern. |
|
|
484
|
+
| `createDynamicPricing`, `createTokenPricing`, `MODEL_PRICING` | Price requests in your handler (use your model provider's live API for LLM cases) and pass the amount to `x402Middleware`. The v1 character-based and tiktoken-based helpers were stopgaps before x402 v2 dynamic pricing landed. |
|
|
485
|
+
| `stripePayTo` | No replacement in the SDK. Integrate Stripe at your application layer if needed. |
|
|
486
|
+
| `x402BrowserSupport` | No replacement. Build a custom paywall page if you need one. |
|
|
606
487
|
|
|
607
|
-
|
|
608
|
-
import { createX402Server, createDynamicPricing } from '@dexterai/x402/server';
|
|
609
|
-
|
|
610
|
-
const server = createX402Server({ payTo: '...', network: '...' });
|
|
611
|
-
const pricing = createDynamicPricing({
|
|
612
|
-
unitSize: 1000, // chars per unit
|
|
613
|
-
ratePerUnit: 0.01, // $0.01 per unit
|
|
614
|
-
minUsd: 0.01, // floor
|
|
615
|
-
maxUsd: 10.00, // ceiling
|
|
616
|
-
});
|
|
617
|
-
|
|
618
|
-
app.post('/api/llm', async (req, res) => {
|
|
619
|
-
const { prompt } = req.body;
|
|
620
|
-
const paymentSig = req.headers['payment-signature'];
|
|
621
|
-
|
|
622
|
-
if (!paymentSig) {
|
|
623
|
-
const quote = pricing.calculate(prompt);
|
|
624
|
-
const requirements = await server.buildRequirements({
|
|
625
|
-
amountAtomic: quote.amountAtomic,
|
|
626
|
-
resourceUrl: req.originalUrl,
|
|
627
|
-
});
|
|
628
|
-
res.setHeader('PAYMENT-REQUIRED', server.encodeRequirements(requirements));
|
|
629
|
-
res.setHeader('X-Quote-Hash', quote.quoteHash);
|
|
630
|
-
return res.status(402).json({ usdAmount: quote.usdAmount });
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
// Validate quote hasn't changed (prevents prompt manipulation)
|
|
634
|
-
const quoteHash = req.headers['x-quote-hash'];
|
|
635
|
-
if (!pricing.validateQuote(prompt, quoteHash)) {
|
|
636
|
-
return res.status(400).json({ error: 'Prompt changed, re-quote required' });
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
const result = await server.settlePayment(paymentSig);
|
|
640
|
-
if (!result.success) return res.status(402).json({ error: result.errorReason });
|
|
641
|
-
|
|
642
|
-
const response = await runLLM(prompt);
|
|
643
|
-
res.json(response);
|
|
644
|
-
});
|
|
645
|
-
```
|
|
646
|
-
|
|
647
|
-
The client SDK automatically forwards `X-Quote-Hash` on retry.
|
|
488
|
+
Removal release is TBD. Decided after we see how 3.9 deprecation warnings land with real consumers. None of these will be removed in 3.x.
|
|
648
489
|
|
|
649
490
|
---
|
|
650
491
|
|
|
651
|
-
##
|
|
652
|
-
|
|
653
|
-
**Accurate token-based pricing for LLMs.** Uses tiktoken for token counting. Supports OpenAI models out of the box, plus custom rates for Anthropic, Gemini, Mistral, or any model.
|
|
492
|
+
## API reference
|
|
654
493
|
|
|
655
|
-
|
|
656
|
-
import { createX402Server, createTokenPricing, MODEL_PRICING } from '@dexterai/x402/server';
|
|
494
|
+
### `payAndFetch(url, init, wallets, opts) → Promise<PayResult>`
|
|
657
495
|
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
496
|
+
| Argument | Type | Description |
|
|
497
|
+
|---|---|---|
|
|
498
|
+
| `url` | `string` | Endpoint to fetch |
|
|
499
|
+
| `init` | `RequestInit` | Standard fetch init. Body must be a string. |
|
|
500
|
+
| `wallets` | `WalletSet` | `{ solana?, evm? }`. The SDK picks the chain by what the merchant accepts and what you can pay |
|
|
501
|
+
| `opts` | `PayAndFetchOptions` | `maxAmountAtomic`, `timeoutMs`, `solanaRpcUrl` |
|
|
664
502
|
|
|
665
|
-
|
|
666
|
-
const { prompt, systemPrompt } = req.body;
|
|
667
|
-
const paymentSig = req.headers['payment-signature'];
|
|
668
|
-
|
|
669
|
-
if (!paymentSig) {
|
|
670
|
-
const quote = pricing.calculate(prompt, systemPrompt);
|
|
671
|
-
const requirements = await server.buildRequirements({
|
|
672
|
-
amountAtomic: quote.amountAtomic,
|
|
673
|
-
resourceUrl: req.originalUrl,
|
|
674
|
-
description: `${quote.model}: ${quote.inputTokens.toLocaleString()} tokens`,
|
|
675
|
-
});
|
|
676
|
-
res.setHeader('PAYMENT-REQUIRED', server.encodeRequirements(requirements));
|
|
677
|
-
res.setHeader('X-Quote-Hash', quote.quoteHash);
|
|
678
|
-
return res.status(402).json({
|
|
679
|
-
inputTokens: quote.inputTokens,
|
|
680
|
-
usdAmount: quote.usdAmount,
|
|
681
|
-
model: quote.model,
|
|
682
|
-
tier: quote.tier,
|
|
683
|
-
});
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
// Validate quote hasn't changed
|
|
687
|
-
const quoteHash = req.headers['x-quote-hash'];
|
|
688
|
-
if (!pricing.validateQuote(prompt, quoteHash)) {
|
|
689
|
-
return res.status(400).json({ error: 'Prompt changed, re-quote required' });
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
const result = await server.settlePayment(paymentSig);
|
|
693
|
-
if (!result.success) return res.status(402).json({ error: result.errorReason });
|
|
694
|
-
|
|
695
|
-
const response = await openai.chat.completions.create({
|
|
696
|
-
model: pricing.config.model,
|
|
697
|
-
messages: [{ role: 'user', content: prompt }],
|
|
698
|
-
max_completion_tokens: pricing.modelInfo.maxTokens,
|
|
699
|
-
});
|
|
700
|
-
|
|
701
|
-
res.json({
|
|
702
|
-
response: response.choices[0].message.content,
|
|
703
|
-
transaction: result.transaction,
|
|
704
|
-
});
|
|
705
|
-
});
|
|
706
|
-
```
|
|
707
|
-
|
|
708
|
-
### Available Models
|
|
503
|
+
`PayResult` is a discriminated union. Narrow on `ok` first, then on `paid`:
|
|
709
504
|
|
|
710
505
|
```typescript
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
//
|
|
714
|
-
|
|
715
|
-
//
|
|
716
|
-
|
|
717
|
-
//
|
|
718
|
-
|
|
719
|
-
//
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
**Supported tiers:** `fast`, `standard`, `reasoning`, `premium`, `custom`
|
|
723
|
-
|
|
724
|
-
### Custom Models (Anthropic, Gemini, etc.)
|
|
725
|
-
|
|
726
|
-
Not using OpenAI? Pass your own rates:
|
|
727
|
-
|
|
728
|
-
```typescript
|
|
729
|
-
// Anthropic Claude
|
|
730
|
-
const pricing = createTokenPricing({
|
|
731
|
-
model: 'claude-3-sonnet',
|
|
732
|
-
inputRate: 3.0, // $3.00 per 1M input tokens
|
|
733
|
-
outputRate: 15.0, // $15.00 per 1M output tokens
|
|
734
|
-
maxTokens: 4096,
|
|
735
|
-
});
|
|
736
|
-
|
|
737
|
-
// Google Gemini
|
|
738
|
-
const pricing = createTokenPricing({
|
|
739
|
-
model: 'gemini-1.5-pro',
|
|
740
|
-
inputRate: 1.25,
|
|
741
|
-
outputRate: 5.0,
|
|
742
|
-
});
|
|
743
|
-
|
|
744
|
-
// Custom/local model with custom tokenizer
|
|
745
|
-
const pricing = createTokenPricing({
|
|
746
|
-
model: 'llama-3-70b',
|
|
747
|
-
inputRate: 0.50,
|
|
748
|
-
tokenizer: (text) => llamaTokenizer.encode(text).length,
|
|
749
|
-
});
|
|
750
|
-
```
|
|
751
|
-
|
|
752
|
-
tiktoken's default encoding works well for most transformer models. Only use a custom tokenizer if your model has significantly different tokenization.
|
|
753
|
-
|
|
754
|
-
---
|
|
755
|
-
|
|
756
|
-
## Sponsored Access (Ads for Agents)
|
|
757
|
-
|
|
758
|
-
Sponsored Access delivers targeted resource recommendations through x402 payments. When an agent pays for an API, the facilitator can inject a recommendation for a related tool in the settlement receipt. The agent sees the recommendation and can call it immediately -- the subsequent call is tracked as a conversion with both blockchain transaction hashes as proof.
|
|
759
|
-
|
|
760
|
-
### Server — Enable Recommendation Injection
|
|
761
|
-
|
|
762
|
-
Add `sponsoredAccess: true` to your middleware config. This reads `extensions["sponsored-access"]` from the facilitator's settlement response and injects `_x402_sponsored` into the JSON response body so the agent's LLM can see the recommendations (headers are invisible to LLMs).
|
|
763
|
-
|
|
764
|
-
```typescript
|
|
765
|
-
import { x402Middleware } from '@dexterai/x402/server';
|
|
766
|
-
|
|
767
|
-
// Default injection: adds _x402_sponsored field to JSON response
|
|
768
|
-
app.get('/api/data',
|
|
769
|
-
x402Middleware({
|
|
770
|
-
payTo: '...', amount: '0.01',
|
|
771
|
-
sponsoredAccess: true,
|
|
772
|
-
}),
|
|
773
|
-
(req, res) => res.json({ data: 'content' })
|
|
774
|
-
);
|
|
775
|
-
// Agent receives: { _x402_sponsored: [{ resourceUrl, description, sponsor }], data: 'content' }
|
|
776
|
-
|
|
777
|
-
// Custom injection: control where recommendations appear
|
|
778
|
-
app.get('/api/data',
|
|
779
|
-
x402Middleware({
|
|
780
|
-
payTo: '...', amount: '0.01',
|
|
781
|
-
sponsoredAccess: {
|
|
782
|
-
inject: (body, recs) => ({ ...body, related_tools: recs }),
|
|
783
|
-
onMatch: (recs, settlement) => {
|
|
784
|
-
console.log(`Matched ${recs.length} recommendations for tx ${settlement.transaction}`);
|
|
785
|
-
},
|
|
786
|
-
},
|
|
787
|
-
}),
|
|
788
|
-
(req, res) => res.json({ data: 'content' })
|
|
789
|
-
);
|
|
790
|
-
```
|
|
791
|
-
|
|
792
|
-
### Client — Read Recommendations
|
|
793
|
-
|
|
794
|
-
```typescript
|
|
795
|
-
import {
|
|
796
|
-
wrapFetch,
|
|
797
|
-
getSponsoredRecommendations,
|
|
798
|
-
fireImpressionBeacon,
|
|
799
|
-
} from '@dexterai/x402/client';
|
|
800
|
-
|
|
801
|
-
const x402Fetch = wrapFetch(fetch, { walletPrivateKey: key });
|
|
802
|
-
const response = await x402Fetch('https://api.example.com/data');
|
|
803
|
-
|
|
804
|
-
// Extract typed recommendations from the payment receipt
|
|
805
|
-
const recs = getSponsoredRecommendations(response);
|
|
806
|
-
if (recs) {
|
|
807
|
-
for (const rec of recs) {
|
|
808
|
-
console.log(`${rec.sponsor}: ${rec.description} -- ${rec.resourceUrl}`);
|
|
809
|
-
}
|
|
810
|
-
// Confirm delivery to the ad network
|
|
811
|
-
await fireImpressionBeacon(response);
|
|
506
|
+
if (result.ok && result.paid) {
|
|
507
|
+
result.response; // the merchant's response
|
|
508
|
+
result.amountPaid; // amount actually paid, in the token's smallest denomination
|
|
509
|
+
result.network; // NetworkRef { caip2, bare, family }
|
|
510
|
+
result.txSignature; // optional; tx hash where the chain reports one
|
|
511
|
+
} else if (result.ok && !result.paid) {
|
|
512
|
+
result.response; // the merchant didn't demand payment; pass-through
|
|
513
|
+
} else {
|
|
514
|
+
result.reason; // 'merchant_rejected' | 'settlement_failed' | 'timeout' | ...
|
|
515
|
+
result.detail; // verbatim merchant error for settlement_failed
|
|
812
516
|
}
|
|
813
517
|
```
|
|
814
518
|
|
|
815
|
-
###
|
|
816
|
-
|
|
817
|
-
```tsx
|
|
818
|
-
import { useX402Payment } from '@dexterai/x402/react';
|
|
819
|
-
|
|
820
|
-
function PayButton() {
|
|
821
|
-
const {
|
|
822
|
-
fetch,
|
|
823
|
-
isLoading,
|
|
824
|
-
sponsoredRecommendations, // auto-populated after payment
|
|
825
|
-
} = useX402Payment({ wallets });
|
|
826
|
-
|
|
827
|
-
return (
|
|
828
|
-
<div>
|
|
829
|
-
<button onClick={() => fetch(url)} disabled={isLoading}>Pay</button>
|
|
830
|
-
{sponsoredRecommendations?.map((rec, i) => (
|
|
831
|
-
<a key={i} href={rec.resourceUrl}>{rec.sponsor}: {rec.description}</a>
|
|
832
|
-
))}
|
|
833
|
-
</div>
|
|
834
|
-
);
|
|
835
|
-
}
|
|
836
|
-
```
|
|
837
|
-
|
|
838
|
-
### Types
|
|
839
|
-
|
|
840
|
-
All types are re-exported from `@dexterai/x402-ads-types`:
|
|
841
|
-
|
|
842
|
-
```typescript
|
|
843
|
-
import type { SponsoredRecommendation, SponsoredAccessSettlementInfo } from '@dexterai/x402/client';
|
|
844
|
-
|
|
845
|
-
interface SponsoredRecommendation {
|
|
846
|
-
resourceUrl: string; // The URL to call
|
|
847
|
-
description: string; // Agent-readable description
|
|
848
|
-
sponsor: string; // Brand name
|
|
849
|
-
bazaarId?: string; // Bazaar catalog ID
|
|
850
|
-
price?: string; // Cost in atomic units
|
|
851
|
-
currency?: string; // e.g., "USDC"
|
|
852
|
-
}
|
|
853
|
-
```
|
|
854
|
-
|
|
855
|
-
### How It Works
|
|
856
|
-
|
|
857
|
-
1. Agent pays for an API via x402
|
|
858
|
-
2. Facilitator settles payment and calls the ad network's match API
|
|
859
|
-
3. If a campaign matches (by URL pattern, category, network), a recommendation is injected into `SettlementResponse.extensions["sponsored-access"]`
|
|
860
|
-
4. Publisher middleware (with `sponsoredAccess: true`) injects it into the JSON response body
|
|
861
|
-
5. Agent's LLM sees the recommendation and can call the suggested resource
|
|
862
|
-
6. If the agent calls it, the facilitator records a conversion with both tx hashes as proof
|
|
863
|
-
|
|
864
|
-
### Advertise Your API
|
|
865
|
-
|
|
866
|
-
Want your API recommended to agents across the x402 network? Create and fund campaigns through the Agent API — no signup, no accounts. Your wallet is your identity.
|
|
867
|
-
|
|
868
|
-
```typescript
|
|
869
|
-
// All campaign management is x402-gated at x402ads.io
|
|
870
|
-
const x402Fetch = wrapFetch(fetch, { walletPrivateKey: key });
|
|
871
|
-
|
|
872
|
-
// Create a campaign ($0.10 USDC)
|
|
873
|
-
const res = await x402Fetch('https://x402ads.io/v1/agent/campaigns', {
|
|
874
|
-
method: 'POST',
|
|
875
|
-
headers: { 'Content-Type': 'application/json' },
|
|
876
|
-
body: JSON.stringify({
|
|
877
|
-
name: 'Promote My API',
|
|
878
|
-
rec_resource_url: 'https://api.example.com/data',
|
|
879
|
-
rec_description: 'Real-time market data',
|
|
880
|
-
rec_sponsor_name: 'Example Corp',
|
|
881
|
-
target_categories: ['defi', 'data'],
|
|
882
|
-
bid_strategy: 'cpa',
|
|
883
|
-
max_bid_amount: '50000',
|
|
884
|
-
budget_daily: '5000000',
|
|
885
|
-
}),
|
|
886
|
-
});
|
|
887
|
-
```
|
|
888
|
-
|
|
889
|
-
Full advertiser guide: [docs.dexter.cash/docs/sponsored-access/for-advertisers](https://docs.dexter.cash/docs/sponsored-access/for-advertisers)
|
|
890
|
-
|
|
891
|
-
---
|
|
892
|
-
|
|
893
|
-
## API Reference
|
|
894
|
-
|
|
895
|
-
### `createX402Client(options)`
|
|
896
|
-
|
|
897
|
-
| Option | Type | Required | Description |
|
|
898
|
-
|--------|------|----------|-------------|
|
|
899
|
-
| `wallets` | `{ solana?, evm? }` | Yes | Multi-chain wallets |
|
|
900
|
-
| `wallet` | `SolanaWallet` | No | Single Solana wallet (legacy) |
|
|
901
|
-
| `preferredNetwork` | `string` | No | Prefer this network when multiple options available |
|
|
902
|
-
| `rpcUrls` | `Record<string, string>` | No | RPC endpoints per network (defaults to Dexter proxy) |
|
|
903
|
-
| `maxAmountAtomic` | `string` | No | Maximum payment cap |
|
|
904
|
-
| `verbose` | `boolean` | No | Enable debug logging |
|
|
905
|
-
|
|
906
|
-
### `x402AccessPass(options)`
|
|
907
|
-
|
|
908
|
-
| Option | Type | Required | Description |
|
|
909
|
-
|--------|------|----------|-------------|
|
|
910
|
-
| `payTo` | `string` | Yes | Address to receive payments |
|
|
911
|
-
| `tiers` | `Record<string, string>` | One of `tiers` or `ratePerHour` | Named tiers (e.g., `{ '1h': '0.50' }`) |
|
|
912
|
-
| `ratePerHour` | `string` | One of `tiers` or `ratePerHour` | USD rate for custom durations |
|
|
913
|
-
| `network` | `string` | No | CAIP-2 network (default: Solana mainnet) |
|
|
914
|
-
| `secret` | `Buffer` | No | HMAC secret for JWT (auto-generated) |
|
|
915
|
-
| `facilitatorUrl` | `string` | No | Facilitator URL (default: x402.dexter.cash) |
|
|
916
|
-
| `verbose` | `boolean` | No | Enable debug logging |
|
|
917
|
-
|
|
918
|
-
### `useX402Payment(options)`
|
|
919
|
-
|
|
920
|
-
Returns:
|
|
921
|
-
|
|
922
|
-
| Property | Type | Description |
|
|
923
|
-
|----------|------|-------------|
|
|
924
|
-
| `fetch` | `function` | Payment-aware fetch |
|
|
925
|
-
| `isLoading` | `boolean` | Payment in progress |
|
|
926
|
-
| `status` | `string` | `'idle'` \| `'pending'` \| `'success'` \| `'error'` |
|
|
927
|
-
| `error` | `X402Error?` | Error details if failed |
|
|
928
|
-
| `transactionId` | `string?` | Transaction signature |
|
|
929
|
-
| `transactionUrl` | `string?` | Block explorer link |
|
|
930
|
-
| `balances` | `Balance[]` | Token balances per chain |
|
|
931
|
-
| `refreshBalances` | `function` | Manual refresh |
|
|
932
|
-
| `reset` | `function` | Clear state |
|
|
933
|
-
| `accessPass` | `object?` | Active pass state (tier, expiresAt, remainingSeconds) |
|
|
934
|
-
|
|
935
|
-
### `useAccessPass(options)`
|
|
519
|
+
### `x402Middleware(config)`
|
|
936
520
|
|
|
937
521
|
| Option | Type | Required | Description |
|
|
938
|
-
|
|
939
|
-
| `
|
|
940
|
-
| `
|
|
941
|
-
| `
|
|
942
|
-
| `
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
|
947
|
-
|
|
948
|
-
| `
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
522
|
+
|---|---|---|---|
|
|
523
|
+
| `payTo` | `string \| { 'solana:*'?, 'eip155:*'?, [caip2]? }` | Yes | Receiver address; map for per-chain receivers |
|
|
524
|
+
| `amount` | `string` | Yes | USD amount, e.g., `'0.01'` |
|
|
525
|
+
| `network` | `string \| string[]` | No | CAIP-2 network(s). Default: Solana mainnet |
|
|
526
|
+
| `description` | `string` | No | Human-readable description |
|
|
527
|
+
| `scheme` | `'exact' \| 'batch-settlement'` | No | Use `'batch-settlement'` to mount as a batch-settlement seller |
|
|
528
|
+
| `extensions` | `ResourceServerExtension[]` | No | E.g., `[bazaarExtension()]` |
|
|
529
|
+
| `declarations` | `Record<string, unknown>` | No | Per-route extension config (see `declareDiscoveryExtension`) |
|
|
530
|
+
| `sponsoredAccess` | `boolean \| { inject?, onMatch? }` | No | Enable Instinct ad-network recommendation injection |
|
|
531
|
+
| `facilitatorUrl` | `string` | No | Override facilitator (default: `x402.dexter.cash`) |
|
|
532
|
+
| `verbose` | `boolean` | No | Debug logging |
|
|
533
|
+
|
|
534
|
+
### `useX402Payment({ wallets })`
|
|
535
|
+
|
|
536
|
+
Returns `{ fetch, isLoading, status, error, transactionId, transactionUrl, balances, refreshBalances, reset, sponsoredRecommendations }`. Accepts wallets directly from `@solana/wallet-adapter-react` and `wagmi`, with no manual adapter wrapping.
|
|
537
|
+
|
|
538
|
+
### `createBatchSettlementSeller(config)`
|
|
539
|
+
|
|
540
|
+
| Option | Type | Description |
|
|
541
|
+
|---|---|---|
|
|
542
|
+
| `payTo` | `string` | EVM receiver |
|
|
543
|
+
| `network` | `string` | CAIP-2 network |
|
|
544
|
+
| `price` | `string` | Per-call USD price |
|
|
545
|
+
| `storage` | `ChannelStorage` | Optional. Defaults to file storage under `~/.dexter-x402/channels` |
|
|
546
|
+
|
|
547
|
+
Returns an Express handler with `.stop()`, `.closeAll()`, `.closeChannel(channelId)`.
|
|
548
|
+
|
|
549
|
+
### `bazaarExtension()` / `declareDiscoveryExtension(config)`
|
|
550
|
+
|
|
551
|
+
The bazaar extension factory takes no arguments. Per-route discovery config is supplied through `declareDiscoveryExtension(config)`:
|
|
552
|
+
|
|
553
|
+
| Field | Type | Notes |
|
|
554
|
+
|---|---|---|
|
|
555
|
+
| `method` | `'GET' \| 'HEAD' \| 'DELETE' \| 'POST' \| 'PUT' \| 'PATCH'` | Optional. If omitted, the actual request method is used. |
|
|
556
|
+
| `queryParams` | `Record<string, ParamSpec>` | For GET/HEAD/DELETE routes |
|
|
557
|
+
| `bodyType` | `'json' \| 'form'` | For POST/PUT/PATCH routes |
|
|
558
|
+
| `body` | `Record<string, ParamSpec>` | For POST/PUT/PATCH routes |
|
|
559
|
+
| `inputSchema` | JSON Schema (Draft 2020-12) | Validates `info` |
|
|
560
|
+
| `output` | `{ example, schema? }` | Example response payload |
|
|
954
561
|
|
|
955
562
|
---
|
|
956
563
|
|
|
957
564
|
## Development
|
|
958
565
|
|
|
959
566
|
```bash
|
|
960
|
-
npm run build #
|
|
567
|
+
npm run build # ESM + CJS
|
|
961
568
|
npm run dev # Watch mode
|
|
962
|
-
npm run typecheck
|
|
569
|
+
npm run typecheck
|
|
570
|
+
npm test # 273 vitest tests
|
|
963
571
|
```
|
|
964
572
|
|
|
965
573
|
---
|
|
966
574
|
|
|
967
575
|
## License
|
|
968
576
|
|
|
969
|
-
MIT
|
|
577
|
+
MIT. See [LICENSE](./LICENSE).
|
|
970
578
|
|
|
971
579
|
---
|
|
972
580
|
|
|
973
581
|
<p align="center">
|
|
974
|
-
<a href="https://x402.dexter.cash">Dexter Facilitator</a> ·
|
|
975
|
-
<a href="https://dexter.cash/opendexter">OpenDexter Marketplace</a> ·
|
|
976
|
-
<a href="https://dexter.cash/sdk">Live Demo</a> ·
|
|
977
|
-
<a href="https://dexter.cash/access-pass">Access Pass Demo</a> ·
|
|
582
|
+
<a href="https://x402.dexter.cash">Dexter Facilitator</a> ·
|
|
583
|
+
<a href="https://dexter.cash/opendexter">OpenDexter Marketplace</a> ·
|
|
584
|
+
<a href="https://dexter.cash/sdk">Live Demo</a> ·
|
|
978
585
|
<a href="https://dexter.cash/onboard">Become a Seller</a>
|
|
979
586
|
</p>
|