@dexterai/x402 3.16.0 → 3.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,13 +5,18 @@
5
5
  <h1 align="center">@dexterai/x402</h1>
6
6
 
7
7
  <p align="center">
8
- <strong>HTTP-native micropayments for agents. Solana and the major EVM chains.</strong>
8
+ <strong>Give your agent a spending limit it can't exceed.</strong>
9
+ </p>
10
+
11
+ <p align="center">
12
+ Open a tab, set a cap, and your agent pays as it works, with no signature on each charge. Your money stays in your own wallet, and the seller is still guaranteed payment. Buyer and seller SDKs, on Solana and the major EVM chains.
9
13
  </p>
10
14
 
11
15
  <p align="center">
12
16
  <a href="https://www.npmjs.com/package/@dexterai/x402"><img src="https://img.shields.io/npm/v/@dexterai/x402.svg" alt="npm"></a>
13
17
  <a href="https://nodejs.org"><img src="https://img.shields.io/badge/node-%3E=18-brightgreen.svg" alt="Node"></a>
14
- <a href="https://dexter.cash/sdk"><img src="https://img.shields.io/badge/Live_Demo-dexter.cash%2Fsdk-blueviolet" alt="Live Demo"></a>
18
+ <img src="https://img.shields.io/badge/non--custodial-passkey-brightgreen" alt="Non-custodial">
19
+ <a href="https://dexter.cash/sdk"><img src="https://img.shields.io/badge/Try_it-real_payments-blueviolet" alt="Live Demo"></a>
15
20
  </p>
16
21
 
17
22
  <p align="center">
@@ -20,378 +25,185 @@
20
25
 
21
26
  ---
22
27
 
23
- ## What is x402?
28
+ ## Why a tab
24
29
 
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.
30
+ A tab gives an agent a spending limit that the Solana program enforces at consensus. You set a cap, the agent pays against it call by call with no signature on each charge, and your USDC stays in your own wallet the entire time.
26
31
 
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.
32
+ The two older ways to let an agent spend each give up something a tab keeps. Prefunding an escrow moves your money to a custodian, so your balance is on the table and you have paid a stranger in advance. Handing a wallet a spending delegate keeps your custody but lets you withdraw the funds mid-charge, so the seller can be left unpaid and serious sellers decline it. A tab keeps both halves: the money never leaves your wallet, and while the tab is open the chain blocks you from pulling it out from under accrued charges. The seller gets paid when they settle, and settlement is automatic.
28
33
 
29
- You call `payAndFetch()` on the client. You add `x402Middleware()` on the server. Payments happen.
30
-
31
- Built against the official x402 v1 and v2 specs. Adds the multi-chain buyer and seller surface, the React hook, a discriminated `PayResult` type, and batch-settlement channels for high-frequency calls.
34
+ The closest familiar shape is an auth-and-capture card hold, with the hold enforced on-chain instead of by a processor.
32
35
 
33
36
  ---
34
37
 
35
- ## Quick start
38
+ ## Install
36
39
 
37
40
  ```bash
38
41
  npm install @dexterai/x402
39
42
  ```
40
43
 
41
- ### Pay for a resource (Node.js, any chain)
42
-
43
- ```typescript
44
- import { payAndFetch, createKeypairWallet, createEvmKeypairWallet } from '@dexterai/x402/client';
45
-
46
- const solana = await createKeypairWallet(process.env.SOLANA_PRIVATE_KEY);
47
- const evm = await createEvmKeypairWallet(process.env.EVM_PRIVATE_KEY); // requires: npm install viem
48
-
49
- const result = await payAndFetch(
50
- 'https://api.example.com/protected',
51
- { method: 'GET' },
52
- { solana, evm },
53
- {},
54
- );
55
-
56
- if (result.ok && result.paid) {
57
- const data = await result.response.json();
58
- console.log(`Paid ${result.amountPaid} on ${result.network.bare}, tx ${result.txSignature}`);
59
- } else if (result.ok && !result.paid) {
60
- // Endpoint didn't demand payment; response came through unchanged.
61
- const data = await result.response.json();
62
- } else {
63
- console.error(result.reason, result.detail);
64
- }
65
- ```
66
-
67
- `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.
68
-
69
- ### Pay for a resource (Browser, React)
70
-
71
- `useX402Payment` accepts wallets from your existing providers (`@solana/wallet-adapter-react`, `wagmi`) and exposes a `fetch` that pays automatically.
72
-
73
- ```tsx
74
- import { useX402Payment } from '@dexterai/x402/react';
75
- import { useWallet } from '@solana/wallet-adapter-react';
76
- import { useAccount } from 'wagmi';
77
-
78
- function PayButton({ url }: { url: string }) {
79
- const solanaWallet = useWallet();
80
- const evmWallet = useAccount();
81
-
82
- const { fetch, isLoading, balances, transactionUrl } = useX402Payment({
83
- wallets: { solana: solanaWallet, evm: evmWallet },
84
- });
44
+ One install is both sides: the buyer surface at `@dexterai/x402/tab`, the seller surface at `@dexterai/x402/tab/seller`.
85
45
 
86
- return (
87
- <div>
88
- <p>Balance: ${balances[0]?.balance.toFixed(2)}</p>
89
- <button onClick={() => fetch(url)} disabled={isLoading}>
90
- {isLoading ? 'Paying…' : 'Pay'}
91
- </button>
92
- {transactionUrl && <a href={transactionUrl}>View transaction</a>}
93
- </div>
94
- );
95
- }
96
- ```
97
-
98
- ### Protect an endpoint (server)
99
-
100
- ```typescript
101
- import express from 'express';
102
- import { x402Middleware } from '@dexterai/x402/server';
103
-
104
- const app = express();
105
-
106
- app.get(
107
- '/api/protected',
108
- x402Middleware({
109
- payTo: 'YourReceivingAddress',
110
- amount: '0.01', // $0.01 USDC
111
- network: 'eip155:8453', // Base. Pass an array for multi-chain.
112
- }),
113
- (req, res) => res.json({ data: 'protected content' }),
114
- );
115
- ```
46
+ ## Open a tab and pay (buyer)
116
47
 
117
- 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.
48
+ A buyer drives tabs through a `vault` adapter over their passkey-rooted Solana vault. Build it once from the vault's addresses, which you receive when you enroll at [dexter.cash](https://dexter.cash), plus your passkey signer:
118
49
 
119
- ### Reading the receipt
120
-
121
- `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).
122
-
123
- ```typescript
124
- import { payAndFetch, getPaymentReceipt } from '@dexterai/x402/client';
125
-
126
- const result = await payAndFetch(url, { method: 'GET' }, wallets, {});
127
- if (result.ok && result.paid) {
128
- const receipt = getPaymentReceipt(result.response);
129
- console.log('tx:', receipt?.transaction, 'on', receipt?.network);
130
- }
50
+ ```ts
51
+ import { createSolanaVaultAdapter } from '@dexterai/x402/tab/adapters/solana';
52
+
53
+ const vault = createSolanaVaultAdapter({
54
+ connection, // your Solana Connection (any RPC)
55
+ swigAddress, // the vault's Swig state account, from enrollment
56
+ vaultPda, // the vault's gate PDA, from enrollment
57
+ passkeySigner, // browser: WebAuthnAssertion; server agent: passkeySignerFromP256Keypair(kp)
58
+ feePayer, // lamport fee payer (a Signer)
59
+ });
131
60
  ```
132
61
 
133
- ---
134
-
135
- ## Batch settlement (EVM)
136
-
137
- 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.
138
-
139
- 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.
140
-
141
- ---
142
-
143
- ## Tabs (Solana) — streaming micropayments, settled on close
144
-
145
- `@dexterai/x402/tab` is the streaming peer of batch settlement: *continuous metered consumption* (tokens, bytes, frames, seconds) where the unit of billing is smaller than a request. One passkey-authorized, drain-protected session signs many vouchers off-chain; the seller verifies locally; one on-chain settle on close moves USDC from the buyer's vault to the seller. Non-custodial — funds never leave the buyer's vault until settle, and the vault freezes withdrawals while a tab is open so the seller can't be rugged. Design: [`docs/DESIGN-tab-streaming.md`](./docs/DESIGN-tab-streaming.md).
146
-
147
- ### Pay a URL — zero seller knowledge
148
-
149
- Given ONLY a URL, the buyer discovers the seller from the URL's own standard
150
- x402 `402` challenge, opens a freeze-protected tab to it, and pays. The seller
151
- pubkey comes off the wire — never from your code.
62
+ Given only a URL, the buyer then reads the seller's terms from the URL's own `402` challenge, opens a freeze-protected tab, and pays. The seller's address comes off the wire, never from your code:
152
63
 
153
64
  ```ts
154
65
  import { payUrlWithTab } from '@dexterai/x402/tab';
155
66
 
156
- const tabs = new Map(); // reuse one open tab per seller across calls
67
+ const tabs = new Map(); // one open tab per seller, reused across calls
157
68
  const { result, tab } = await payUrlWithTab(
158
69
  'https://api.example.com/paid/infer',
159
70
  { method: 'GET' },
160
71
  { vault, perUnitCap: '0.01', totalCap: '1.00', tabs },
161
72
  );
162
73
  // ...more payUrlWithTab calls reuse the same tab via `tabs`...
163
- await tab?.close(); // ONE on-chain settle for everything streamed
74
+ await tab?.close(); // one on-chain settle for everything the agent spent
164
75
  ```
165
76
 
166
- `resolveTabOffer(url)` is the underlying resolution primitive probe a URL and read its tab terms (counterparty, quote, network) without paying.
167
-
168
- ### Tab seller (Express)
169
-
170
- Compose `tabChallengeMiddleware` (answers voucher-less requests with the standard x402 challenge, so strangers can discover you) BEFORE `tabMiddleware` (verifies vouchers):
77
+ To decide before you pay, `resolveTabTerms(url)` reads a URL's price and settlement terms without paying, for consent screens, directories, or an agent that plans ahead:
171
78
 
172
79
  ```ts
173
- import { tabChallengeMiddleware, tabMiddleware, requireTab, openSse } from '@dexterai/x402/tab/seller';
80
+ import { resolveTabTerms } from '@dexterai/x402/tab';
174
81
 
175
- app.get('/paid/infer',
176
- tabChallengeMiddleware({ sellerPubkey, network: 'solana:mainnet', perUnit: '0.001', facilitatorUrl }),
177
- tabMiddleware({ connection, sellerPubkey, network: 'solana:mainnet', perUnit: '0.001', settle: 'on-close', facilitatorUrl }),
178
- async (req, res) => {
179
- const meter = openSse(res, { tab: requireTab(req), perUnit: '0.001' });
180
- await meter.charge(1);
181
- meter.send('...');
182
- meter.end();
183
- });
184
- ```
185
-
186
- ### Buyer
187
-
188
- ```ts
189
- import { openBatchChannel } from '@dexterai/x402/batch-settlement';
190
-
191
- const channel = await openBatchChannel({
192
- wallet: evmWallet, // any { address, signTypedData }
193
- network: 'eip155:8453', // Base
194
- deposit: '0.30', // USDC escrowed for this channel
195
- });
196
-
197
- const a = await channel.fetch('https://api.example.com/v1/data');
198
- const b = await channel.fetch('https://api.example.com/v1/data');
199
-
200
- console.log(channel.state); // { deposited: '0.3', spent: '0.16', remaining: '0.14' }
201
-
202
- const { closed } = await channel.close();
82
+ const resolved = await resolveTabTerms('https://api.example.com/paid/tick');
83
+ if (resolved.kind === 'terms') {
84
+ console.log(resolved.terms.counterparty, resolved.terms.perRequest.human);
85
+ // settlement: { custody: 'non-custodial', protection: 'freeze', settleOn: 'close' }
86
+ }
203
87
  ```
204
88
 
205
- 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.
89
+ ## Accept tabs on your API (seller)
206
90
 
207
- Resume after a process restart with the wallet, network, and the channel's salt:
91
+ `tabOrExactMiddleware` is the recommended default: one middleware that advertises a tab and a one-shot price in a single 402 challenge, so agents pay by tab and one-shot callers pay exact, at the same price.
208
92
 
209
93
  ```ts
210
- import { resumeBatchChannel } from '@dexterai/x402/batch-settlement';
94
+ import { tabOrExactMiddleware, requireTab, openSse } from '@dexterai/x402/tab/seller';
95
+ import type { X402Request } from '@dexterai/x402/server';
211
96
 
212
- const channel = await resumeBatchChannel({
213
- wallet: evmWallet,
214
- network: 'eip155:8453',
215
- salt: savedSalt,
216
- });
217
- ```
218
-
219
- 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.
220
-
221
- #### Escape hatch: `forceWithdraw()` / `finalizeWithdraw()`
222
-
223
- If the seller never settles, the buyer can reclaim unspent escrow directly via the channel contract's timed withdrawal:
224
-
225
- ```ts
226
- await channel.forceWithdraw();
227
- // after the channel's withdraw delay elapses
228
- await channel.finalizeWithdraw();
97
+ app.get('/paid/tick',
98
+ tabOrExactMiddleware({ connection, sellerPubkey, network: 'solana:mainnet', perUnit: '0.01' }),
99
+ async (req, res) => {
100
+ if ((req as X402Request).x402) { res.json({ data: '...', paidVia: 'exact' }); return; } // exact rail
101
+ const tab = requireTab(req); // tab rail
102
+ const meter = openSse(res, { tab, perUnit: '0.01' });
103
+ await meter.charge(1);
104
+ meter.send(JSON.stringify({ data: '...' }));
105
+ await meter.end();
106
+ });
229
107
  ```
230
108
 
231
- 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.
232
-
233
- ### Seller
109
+ For a tab-only endpoint, compose the two middlewares directly: `tabChallengeMiddleware` (answers voucher-less requests with the standard x402 challenge, so any agent can discover you) before `tabMiddleware` (verifies the per-charge vouchers). Both are exported from `@dexterai/x402/tab/seller`.
234
110
 
235
- `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.
236
-
237
- ```ts
238
- import { createBatchSettlementSeller } from '@dexterai/x402/batch-settlement/seller';
111
+ ---
239
112
 
240
- const seller = createBatchSettlementSeller({
241
- payTo: '0xYourReceivingAddress',
242
- network: 'eip155:8453',
243
- price: '0.08',
244
- });
113
+ ## How it works
245
114
 
246
- app.use('/api/data', seller);
115
+ Three nouns and one actor.
247
116
 
248
- process.on('SIGTERM', async () => {
249
- await seller.stop(); // flushes a final settle so no vouchers are lost
250
- });
251
- ```
117
+ - **Vault:** your money, held in your own wallet and locked by your passkey. The program never takes custody.
118
+ - **Tab:** a capped spending limit you open against your vault, for one agent and one counterparty. The agent draws against it; the vault enforces the cap.
119
+ - **Passkey:** your key. You tap it to set up the vault and to open or approve a tab. Nothing else can authorize a withdrawal.
120
+ - **Your agent:** who you open the tab for.
252
121
 
253
- Mounting via `x402Middleware` also works. With `scheme: 'batch-settlement'` it returns the same callable seller object, so you keep the `.stop()` / `.closeAll()` / `.closeChannel()` handle.
122
+ A tab opens with one passkey tap, the agent spends against it with no further prompts, and one on-chain settle pays the seller and closes it. Everything between open and settle is off-chain, so a charge costs no gas and no signature.
254
123
 
255
124
  ---
256
125
 
257
- ## Discovery (bazaar extension)
258
-
259
- 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.
260
-
261
- 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.
126
+ ## Why you can trust it
262
127
 
263
- ```typescript
264
- import {
265
- x402Middleware,
266
- bazaarExtension,
267
- declareDiscoveryExtension,
268
- } from '@dexterai/x402/server';
269
-
270
- app.post(
271
- '/v1/translate',
272
- x402Middleware({
273
- payTo: '...',
274
- amount: '0.02',
275
- network: 'eip155:8453',
276
- extensions: [bazaarExtension()],
277
- declarations: {
278
- ...declareDiscoveryExtension({
279
- method: 'POST',
280
- bodyType: 'json',
281
- inputSchema: {
282
- properties: {
283
- text: { type: 'string', description: 'Source text' },
284
- targetLang: { type: 'string', description: 'ISO 639-1 code' },
285
- },
286
- required: ['text', 'targetLang'],
287
- },
288
- output: {
289
- example: { translation: 'Bonjour' },
290
- },
291
- }),
292
- },
293
- }),
294
- (req, res) => res.json({ translation: translate(req.body) }),
295
- );
296
- ```
128
+ The word "unruggable" has to be earned, so here is what actually backs it. The properties below are enforced by the on-chain program, not by this SDK.
297
129
 
298
- `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.
130
+ - **Non-custodial.** Your USDC stays in your own wallet. The program holds no funds; it records bindings and gates a withdrawal. There is no escrow account and no custodian to fail.
131
+ - **The cap is enforced on-chain.** The limit is checked by the Solana program at consensus, not by this library and not by Dexter. You can read the program and verify the cap yourself: [`Hg3wRaydFtJhYrdvYrKECacpJYDsC9Px7yKmpncj2fhc`](https://solscan.io/account/Hg3wRaydFtJhYrdvYrKECacpJYDsC9Px7yKmpncj2fhc) on Solana mainnet.
132
+ - **Only your passkey moves funds.** Withdrawals require a WebAuthn assertion verified by Solana's secp256r1 precompile. The SDK and the facilitator never hold a key that can drain your wallet.
133
+ - **The seller is protected.** While a tab is open the program blocks the buyer's withdrawal, so funds can't be pulled out from under accrued charges. If a seller ever goes silent, the buyer recovers an abandoned tab themselves after a fixed grace period; nobody's funds can be frozen indefinitely.
134
+ - **Live on Solana mainnet.** Tabs settle on mainnet today. We can demonstrate the program rejecting a forged passkey from a clone: see the [`dexter-vault`](https://github.com/Dexter-DAO/dexter-vault) program repo.
135
+ - **Pre-audit, and we say so.** Not yet externally audited; funding is in flight. The report and any findings publish in the program repo. Responsible disclosure: branch@dexter.cash.
299
136
 
300
- 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.
137
+ The full threat model and trust assumptions live in the program's [`SECURITY.md`](https://github.com/Dexter-DAO/dexter-vault).
301
138
 
302
139
  ---
303
140
 
304
- ## Sponsored Access (Instinct ad network)
141
+ ## Approving a tab is one hosted screen
305
142
 
306
- 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.
143
+ When a partner's app opens a tab for a user, the approval runs on one Dexter-hosted consent screen, deep-linked from the partner's app. The user sees the counterparty, the cap, and the expiry, taps their passkey once, and control returns to the app. The partner builds no approval UI and never handles a passkey.
307
144
 
308
- 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.
145
+ The screen is hosted by Dexter for a structural reason, not a stylistic one: the vault's passkey can only sign on Dexter's own origin, so a user cannot be phished into approving on a look-alike page. The safety is a property of where the key will sign. Flow and routing: [docs.dexter.cash/tabs](https://docs.dexter.cash). **[TODO: confirm final docs path once #5 lands.]**
309
146
 
310
- ### Seller: enable recommendation injection
311
-
312
- ```typescript
313
- import { x402Middleware } from '@dexterai/x402/server';
147
+ ---
314
148
 
315
- app.get(
316
- '/api/data',
317
- x402Middleware({
318
- payTo: '...',
319
- amount: '0.01',
320
- sponsoredAccess: true, // injects _x402_sponsored into JSON responses
321
- }),
322
- (req, res) => res.json({ data: 'content' }),
323
- );
324
- // Response: { _x402_sponsored: [{ resourceUrl, description, sponsor }], data: 'content' }
325
- ```
149
+ ## One-shot payments
326
150
 
327
- For custom placement (where in the body the recommendation appears, conversion logging, etc.), pass an object instead of `true`:
151
+ When a charge is a single discrete purchase rather than metered consumption, pay it one-shot. x402 is HTTP's payment protocol: a server returns `402 Payment Required` describing what it wants paid, the client signs and retries, and the resource comes back. USDC on Solana and the major EVM chains, behind one API.
328
152
 
329
153
  ```typescript
330
- sponsoredAccess: {
331
- inject: (body, recs) => ({ ...body, related_tools: recs }),
332
- onMatch: (recs, settlement) => log(`matched ${recs.length} for tx ${settlement.transaction}`),
333
- },
334
- ```
154
+ import { payAndFetch, createKeypairWallet, createEvmKeypairWallet } from '@dexterai/x402/client';
335
155
 
336
- ### Buyer: read recommendations off a paid response
156
+ const solana = await createKeypairWallet(process.env.SOLANA_PRIVATE_KEY);
157
+ const evm = await createEvmKeypairWallet(process.env.EVM_PRIVATE_KEY); // requires: npm install viem
337
158
 
338
- ```typescript
339
- import {
340
- payAndFetch,
341
- getSponsoredRecommendations,
342
- fireImpressionBeacon,
343
- } from '@dexterai/x402/client';
159
+ const result = await payAndFetch(
160
+ 'https://api.example.com/protected',
161
+ { method: 'GET' },
162
+ { solana, evm },
163
+ {},
164
+ );
344
165
 
345
- const result = await payAndFetch(url, { method: 'GET' }, wallets, {});
346
166
  if (result.ok && result.paid) {
347
- const recs = getSponsoredRecommendations(result.response);
348
- if (recs) {
349
- for (const rec of recs) {
350
- console.log(`${rec.sponsor}: ${rec.description} (${rec.resourceUrl})`);
351
- }
352
- await fireImpressionBeacon(result.response);
353
- }
167
+ const data = await result.response.json();
168
+ console.log(`Paid ${result.amountPaid} on ${result.network.bare}, tx ${result.txSignature}`);
169
+ } else if (result.ok && !result.paid) {
170
+ const data = await result.response.json(); // endpoint didn't demand payment; passed through
171
+ } else {
172
+ console.error(result.reason, result.detail);
354
173
  }
355
174
  ```
356
175
 
357
- ### React: recommendations in the hook
176
+ `payAndFetch` handles x402 v1 and v2 transparently and returns a discriminated `PayResult`: `ok` splits into `paid: true | false`, so a free 200 is distinguishable from a paid one, and expected failures don't throw.
358
177
 
359
- ```tsx
360
- import { useX402Payment } from '@dexterai/x402/react';
361
-
362
- function PayButton() {
363
- const { fetch, isLoading, sponsoredRecommendations } = useX402Payment({ wallets });
364
-
365
- return (
366
- <div>
367
- <button onClick={() => fetch(url)} disabled={isLoading}>Pay</button>
368
- {sponsoredRecommendations?.map((rec, i) => (
369
- <a key={i} href={rec.resourceUrl}>{rec.sponsor}: {rec.description}</a>
370
- ))}
371
- </div>
372
- );
373
- }
374
- ```
178
+ Protect an endpoint with `x402Middleware`; the handler runs only after payment settles. In React, `useX402Payment` takes wallets from `@solana/wallet-adapter-react` or `wagmi` and returns a `fetch` that pays automatically. Read a settled receipt off any paid response with `getPaymentReceipt(response)`.
375
179
 
376
- ### Advertise
180
+ ```typescript
181
+ import { x402Middleware } from '@dexterai/x402/server';
377
182
 
378
- 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).
183
+ app.get('/api/protected',
184
+ x402Middleware({ payTo: 'YourReceivingAddress', amount: '0.01', network: 'eip155:8453' }),
185
+ (req, res) => res.json({ data: 'protected content' }),
186
+ );
187
+ ```
379
188
 
380
189
  ---
381
190
 
382
- ## Auto-listing in OpenDexter
191
+ ## Also in this package
383
192
 
384
- 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.
193
+ Four supporting surfaces, each with its own reference below.
385
194
 
386
- Browse the live catalog at [dexter.cash/opendexter](https://dexter.cash/opendexter).
195
+ - **Batch settlement (EVM).** Prepay an escrow once, make many discrete paid calls against it with off-chain vouchers, and settle in a handful of transactions to amortize gas. EVM only. `openBatchChannel` / `createBatchSettlementSeller`.
196
+ - **Discovery (bazaar).** Make any `x402Middleware`-protected route discoverable through the official x402 bazaar spec, so agents find it by capability. `bazaarExtension()`.
197
+ - **Sponsored access.** When an agent pays through Dexter's facilitator, a matched recommendation can ride along in the receipt; the agent's model may act on it. `sponsoredAccess: true`.
198
+ - **Auto-listing.** Endpoints paid through the facilitator are auto-discovered, named, and quality-tested, then surfaced in `x402_search` across MCP clients. No registration step.
199
+
200
+ Full examples for each are in the [reference](#reference) section.
387
201
 
388
202
  ---
389
203
 
390
204
  ## Supported networks
391
205
 
392
- All networks supported by the [Dexter facilitator](https://x402.dexter.cash/supported). USDC on every chain.
393
-
394
- **Mainnets:**
206
+ All networks supported by the [Dexter facilitator](https://x402.dexter.cash/supported). USDC on every chain. Tabs are Solana; one-shot and batch settlement span Solana and the EVM chains below.
395
207
 
396
208
  | Network | CAIP-2 | Status |
397
209
  |---------|--------|--------|
@@ -404,139 +216,46 @@ All networks supported by the [Dexter facilitator](https://x402.dexter.cash/supp
404
216
  | BSC | `eip155:56` | Production |
405
217
  | SKALE Base | `eip155:1187947933` | Production (zero gas) |
406
218
 
407
- **Testnets:**
219
+ Testnets: Solana Devnet/Testnet, Base Sepolia, SKALE Base Sepolia. Multi-chain endpoints accept any chain in the list; the buyer picks. Pass `network` as an array to `x402Middleware`, with a `payTo` map for per-chain receivers.
408
220
 
409
- | Network | CAIP-2 |
410
- |---------|--------|
411
- | Solana Devnet | `solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1` |
412
- | Solana Testnet | `solana:4uhcVJyU9pJkvQyS88uRDiswHXSCkY3z` |
413
- | Base Sepolia | `eip155:84532` |
414
- | SKALE Base Sepolia | `eip155:324705682` |
221
+ ---
415
222
 
416
- Multi-chain endpoints accept payments on any chain in the list. The buyer picks:
223
+ ## Reference
417
224
 
418
- ```typescript
419
- app.get('/api/data', x402Middleware({
420
- payTo: {
421
- 'solana:*': 'YourSolanaAddress...',
422
- 'eip155:*': '0xYourEvmAddress...',
423
- },
424
- amount: '0.01',
425
- network: [
426
- 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp',
427
- 'eip155:8453',
428
- 'eip155:137',
429
- 'eip155:42161',
430
- 'eip155:10',
431
- 'eip155:43114',
432
- 'eip155:56',
433
- 'eip155:1187947933',
434
- ],
435
- }));
436
- ```
225
+ ### Package exports
437
226
 
438
- ---
227
+ ```typescript
228
+ // Tabs (Solana): buyer
229
+ import { payUrlWithTab, resolveTabTerms, resolveTabOffer } from '@dexterai/x402/tab';
439
230
 
440
- ## Package exports
231
+ // Tabs (Solana): seller
232
+ import { tabChallengeMiddleware, tabMiddleware, tabOrExactMiddleware, requireTab, openSse } from '@dexterai/x402/tab/seller';
441
233
 
442
- ```typescript
443
- // Client: canonical entrypoint
234
+ // One-shot client
444
235
  import { payAndFetch, createKeypairWallet, createEvmKeypairWallet, getPaymentReceipt } from '@dexterai/x402/client';
445
236
 
446
- // Client: sponsored access reader
447
- import { getSponsoredRecommendations, fireImpressionBeacon } from '@dexterai/x402/client';
448
-
449
237
  // React
450
238
  import { useX402Payment } from '@dexterai/x402/react';
451
239
 
452
- // Server: middleware
453
- import { x402Middleware } from '@dexterai/x402/server';
454
-
455
- // Server: discovery (bazaar extension)
456
- import { bazaarExtension, declareDiscoveryExtension } from '@dexterai/x402/server';
457
-
458
- // Server: manual control
459
- import { createX402Server } from '@dexterai/x402/server';
240
+ // Server middleware + discovery
241
+ import { x402Middleware, bazaarExtension, declareDiscoveryExtension, createX402Server } from '@dexterai/x402/server';
460
242
 
461
243
  // Batch settlement
462
244
  import { openBatchChannel, resumeBatchChannel } from '@dexterai/x402/batch-settlement';
463
245
  import { createBatchSettlementSeller } from '@dexterai/x402/batch-settlement/seller';
464
246
 
465
- // Adapters (advanced)
247
+ // Adapters (advanced) + utilities
466
248
  import { createSolanaAdapter, createEvmAdapter } from '@dexterai/x402/adapters';
467
-
468
- // Utilities
469
249
  import { toAtomicUnits, fromAtomicUnits } from '@dexterai/x402/utils';
470
250
  ```
471
251
 
472
- ---
252
+ ### `payUrlWithTab(url, init, opts) → Promise<{ result, tab }>`
473
253
 
474
- ## Utilities
254
+ Opens (or reuses) a freeze-protected tab to the seller discovered from the URL's `402` challenge, and pays. `opts`: `{ vault, perUnitCap, totalCap, tabs }`. Reuse one `tabs` map across calls to keep a single open tab per seller; `tab.close()` settles everything spent in one transaction.
475
255
 
476
- ```typescript
477
- import { toAtomicUnits, fromAtomicUnits } from '@dexterai/x402/utils';
478
-
479
- toAtomicUnits(0.05, 6); // '50000'
480
- toAtomicUnits(1.50, 6); // '1500000'
481
- fromAtomicUnits('50000', 6); // 0.05
482
- fromAtomicUnits(1500000n, 6); // 1.5
483
- ```
484
-
485
- ---
256
+ ### `resolveTabTerms(url) → Promise<TabResolution>`
486
257
 
487
- ## Manual server (advanced)
488
-
489
- For full control over the payment flow without `x402Middleware`:
490
-
491
- ```typescript
492
- import { createX402Server } from '@dexterai/x402/server';
493
-
494
- const server = createX402Server({
495
- payTo: 'YourAddress...',
496
- network: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp',
497
- });
498
-
499
- app.post('/protected', async (req, res) => {
500
- const paymentSig = req.headers['payment-signature'];
501
-
502
- if (!paymentSig) {
503
- const requirements = await server.buildRequirements({
504
- amountAtomic: '50000', // $0.05 USDC
505
- resourceUrl: req.originalUrl,
506
- });
507
- res.setHeader('PAYMENT-REQUIRED', server.encodeRequirements(requirements));
508
- return res.status(402).json({});
509
- }
510
-
511
- const result = await server.settlePayment(paymentSig);
512
- if (!result.success) {
513
- return res.status(402).json({ error: result.errorReason });
514
- }
515
-
516
- res.json({ data: 'protected content' });
517
- });
518
- ```
519
-
520
- ---
521
-
522
- ## Legacy capabilities
523
-
524
- 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.
525
-
526
- | Symbol | Migration target |
527
- |---|---|
528
- | `wrapFetch` (`@dexterai/x402/client`) | `payAndFetch` (version-agnostic, discriminated return type) |
529
- | `createX402Client` (`@dexterai/x402/client`) | `payAndFetch` |
530
- | `x402AccessPass`, `useAccessPass` | No replacement. Per-request `x402Middleware` + `payAndFetch` covers the same usage pattern. |
531
- | `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. |
532
- | `stripePayTo` | No replacement in the SDK. Integrate Stripe at your application layer if needed. |
533
- | `x402BrowserSupport` | No replacement. Build a custom paywall page if you need one. |
534
-
535
- None of these will be removed in 3.x.
536
-
537
- ---
538
-
539
- ## API reference
258
+ Reads a URL's tab terms without paying. Returns `{ kind: 'terms', terms: { counterparty, perRequest, network, settlement } }`, or a non-terms kind when the URL offers no tab.
540
259
 
541
260
  ### `payAndFetch(url, init, wallets, opts) → Promise<PayResult>`
542
261
 
@@ -547,19 +266,16 @@ None of these will be removed in 3.x.
547
266
  | `wallets` | `WalletSet` | `{ solana?, evm? }`. The SDK picks the chain by what the merchant accepts and what you can pay |
548
267
  | `opts` | `PayAndFetchOptions` | `maxAmountAtomic`, `timeoutMs`, `solanaRpcUrl` |
549
268
 
550
- `PayResult` is a discriminated union. Narrow on `ok` first, then on `paid`:
269
+ `PayResult` is a discriminated union. Narrow on `ok`, then on `paid`:
551
270
 
552
271
  ```typescript
553
272
  if (result.ok && result.paid) {
554
- result.response; // the merchant's response
555
- result.amountPaid; // amount actually paid, in the token's smallest denomination
556
- result.network; // NetworkRef { caip2, bare, family }
557
- result.txSignature; // optional; tx hash where the chain reports one
273
+ result.response; result.amountPaid; result.network; result.txSignature;
558
274
  } else if (result.ok && !result.paid) {
559
- result.response; // the merchant didn't demand payment; pass-through
275
+ result.response; // merchant didn't demand payment; pass-through
560
276
  } else {
561
277
  result.reason; // 'merchant_rejected' | 'settlement_failed' | 'timeout' | ...
562
- result.detail; // verbatim merchant error for settlement_failed
278
+ result.detail;
563
279
  }
564
280
  ```
565
281
 
@@ -570,41 +286,31 @@ if (result.ok && result.paid) {
570
286
  | `payTo` | `string \| { 'solana:*'?, 'eip155:*'?, [caip2]? }` | Yes | Receiver address; map for per-chain receivers |
571
287
  | `amount` | `string` | Yes | USD amount, e.g., `'0.01'` |
572
288
  | `network` | `string \| string[]` | No | CAIP-2 network(s). Default: Solana mainnet |
573
- | `description` | `string` | No | Human-readable description |
574
- | `scheme` | `'exact' \| 'batch-settlement'` | No | Use `'batch-settlement'` to mount as a batch-settlement seller |
289
+ | `scheme` | `'exact' \| 'batch-settlement'` | No | `'batch-settlement'` mounts as a batch-settlement seller |
575
290
  | `extensions` | `ResourceServerExtension[]` | No | E.g., `[bazaarExtension()]` |
576
- | `declarations` | `Record<string, unknown>` | No | Per-route extension config (see `declareDiscoveryExtension`) |
577
- | `sponsoredAccess` | `boolean \| { inject?, onMatch? }` | No | Enable Instinct ad-network recommendation injection |
291
+ | `sponsoredAccess` | `boolean \| { inject?, onMatch? }` | No | Instinct ad-network recommendation injection |
578
292
  | `facilitatorUrl` | `string` | No | Override facilitator (default: `x402.dexter.cash`) |
579
- | `verbose` | `boolean` | No | Debug logging |
580
293
 
581
- ### `useX402Payment({ wallets })`
294
+ ### Batch settlement
582
295
 
583
- 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.
296
+ ```ts
297
+ import { openBatchChannel } from '@dexterai/x402/batch-settlement';
584
298
 
585
- ### `createBatchSettlementSeller(config)`
299
+ const escrow = await openBatchChannel({ wallet: evmWallet, network: 'eip155:8453', deposit: '0.30' });
300
+ await escrow.fetch('https://api.example.com/v1/data');
301
+ console.log(escrow.state); // { deposited: '0.3', spent: '0.16', remaining: '0.14' }
302
+ await escrow.close();
303
+ ```
586
304
 
587
- | Option | Type | Description |
588
- |---|---|---|
589
- | `payTo` | `string` | EVM receiver |
590
- | `network` | `string` | CAIP-2 network |
591
- | `price` | `string` | Per-call USD price |
592
- | `storage` | `ChannelStorage` | Optional. Defaults to file storage under `~/.dexter-x402/channels` |
305
+ State auto-persists and resumes with `resumeBatchChannel({ wallet, network, salt })`. If the seller never settles, reclaim unspent escrow with `forceWithdraw()` then `finalizeWithdraw()`. The seller mounts `createBatchSettlementSeller(config)` as an Express handler; Dexter operates the authorizer, so the seller manages no signing key. Returns a handler with `.stop()`, `.closeAll()`, `.closeChannel(id)`.
593
306
 
594
- Returns an Express handler with `.stop()`, `.closeAll()`, `.closeChannel(channelId)`.
307
+ ### Discovery, sponsored access
595
308
 
596
- ### `bazaarExtension()` / `declareDiscoveryExtension(config)`
309
+ `bazaarExtension()` plus `declareDiscoveryExtension(config)` attach a spec-compliant `extensions.bazaar` block to a route's 402; extensions are opt-in and failure-isolated, so the payment path is never affected. `sponsoredAccess` injects `_x402_sponsored` into responses; read it with `getSponsoredRecommendations(response)`. Campaign creation is x402-gated at `x402ads.io`.
597
310
 
598
- The bazaar extension factory takes no arguments. Per-route discovery config is supplied through `declareDiscoveryExtension(config)`:
311
+ ### Legacy
599
312
 
600
- | Field | Type | Notes |
601
- |---|---|---|
602
- | `method` | `'GET' \| 'HEAD' \| 'DELETE' \| 'POST' \| 'PUT' \| 'PATCH'` | Optional. If omitted, the actual request method is used. |
603
- | `queryParams` | `Record<string, ParamSpec>` | For GET/HEAD/DELETE routes |
604
- | `bodyType` | `'json' \| 'form'` | For POST/PUT/PATCH routes |
605
- | `body` | `Record<string, ParamSpec>` | For POST/PUT/PATCH routes |
606
- | `inputSchema` | JSON Schema (Draft 2020-12) | Validates `info` |
607
- | `output` | `{ example, schema? }` | Example response payload |
313
+ v1-era helpers (`wrapFetch`, `createX402Client`, `x402AccessPass`, `createDynamicPricing`, `stripePayTo`, `x402BrowserSupport`) ship `@deprecated` with JSDoc migration targets and keep working. None will be removed in 3.x. New code should use `payAndFetch` and `x402Middleware`.
608
314
 
609
315
  ---
610
316
 
@@ -617,8 +323,6 @@ npm run typecheck
617
323
  npm test # 273 vitest tests
618
324
  ```
619
325
 
620
- ---
621
-
622
326
  ## License
623
327
 
624
328
  MIT. See [LICENSE](./LICENSE).