Documentation Index
Fetch the complete documentation index at: https://docs.faremeter.xyz/llms.txt
Use this file to discover all available pages before exploring further.
@faremeter/fetch wraps the global fetch with automatic 402 payment handling. Unlike @faremeter/rides, you configure handlers and options explicitly.
pnpm add @faremeter/fetch
Basic usage
import { Keypair, PublicKey } from "@solana/web3.js"
import { wrap } from "@faremeter/fetch"
import { createPaymentHandler } from "@faremeter/payment-solana/exact"
import { createLocalWallet } from "@faremeter/wallet-solana"
import { lookupKnownSPLToken } from "@faremeter/info/solana"
const keypair = Keypair.fromSecretKey(new Uint8Array(keypairBytes))
const wallet = await createLocalWallet("devnet", keypair)
const splTokenInfo = lookupKnownSPLToken("devnet", "USDC")
if (!splTokenInfo) throw new Error("Unknown SPL token")
const mint = new PublicKey(splTokenInfo.address)
const handler = createPaymentHandler(wallet, mint)
const fetchWithPayment = wrap(fetch, {
handlers: [handler],
})
const response = await fetchWithPayment("https://api.example.com/resource")
wrap
import { wrap } from "@faremeter/fetch"
function wrap(phase2Fetch: typeof fetch, options: WrapOpts): typeof fetch
Returns a new fetch function that intercepts 402 responses and handles payment automatically.
WrapOpts
| Option | Type | Default | Description |
|---|
handlers | PaymentHandler[] | Required | Payment handlers to use for 402 responses. |
payerChooser | (execers: PaymentExecer[]) => Promise<PaymentExecer> | chooseFirstAvailable | Selects which payer to use when multiple can fulfill a requirement. |
phase1Fetch | typeof fetch | Same as phase2Fetch | Fetch function for the initial request (before payment). |
retryCount | number | 2 | Number of retries after payment (3 total attempts). |
initialRetryDelay | number | 100 | Starting backoff delay in milliseconds. |
returnPaymentFailure | boolean | false | Return the 402 response instead of throwing on payment failure. |
Phase 1 and Phase 2 fetch
The wrapper uses two fetch calls per payment:
- Phase 1: The initial request that discovers the 402 response.
- Phase 2: The retry with the
X-PAYMENT header attached.
By default, both use the same fetch function. You can provide a separate phase1Fetch if the initial request needs different configuration (e.g., different headers or timeouts).
Payer selection
When multiple handlers can fulfill a payment requirement, the payerChooser function selects one.
import { wrap, chooseFirstAvailable } from "@faremeter/fetch"
const wrappedFetch = wrap(fetch, {
handlers: [solanaHandler, evmHandler],
payerChooser: chooseFirstAvailable,
})
chooseFirstAvailable is the default. It picks the first handler that returns a PaymentExecer.
Custom payer chooser
import type { PaymentExecer } from "@faremeter/types/client"
const preferSolana = async (execers: PaymentExecer[]) => {
const solana = execers.find((e) => e.requirements.network.startsWith("solana"))
return solana ?? execers[0]
}
const wrappedFetch = wrap(fetch, {
handlers: [solanaHandler, evmHandler],
payerChooser: preferSolana,
})
Error handling
If all payment attempts fail, wrap throws a WrappedFetchError:
import { WrappedFetchError } from "@faremeter/fetch"
import { getLogger } from "@faremeter/logs"
const logger = await getLogger(["my-app"])
try {
const response = await wrappedFetch("https://api.example.com/resource")
} catch (error) {
if (error instanceof WrappedFetchError) {
logger.error(`Payment failed: ${error.message}, status: ${error.response.status}`)
}
}
Alternatively, set returnPaymentFailure: true to receive the 402 response instead of throwing:
const wrappedFetch = wrap(fetch, {
handlers: [handler],
returnPaymentFailure: true,
})
const response = await wrappedFetch("https://api.example.com/resource")
if (response.status === 402) {
// Payment failed, handle accordingly
}
Retry behavior
Failed payment attempts are retried with exponential backoff:
- Default: 2 retries (3 total attempts)
- Backoff starts at 100ms and doubles each retry
- Only retries when the 402 payment flow fails, not on other HTTP errors
Testing with mocks
@faremeter/fetch exports a mock namespace for testing payment flows without real network calls.
responseFeeder
import { mock } from "@faremeter/fetch"
responseFeeder(responses) takes an array of Response objects or fetch-like functions and returns a mock fetch. Each call to the returned function consumes the next item in the array.
Basic test pattern
Queue a 402 response followed by a success response to simulate a full payment flow:
import { mock, wrap } from "@faremeter/fetch"
const mockFetch = mock.responseFeeder([
// Phase 1: server returns 402
new Response(JSON.stringify({ requirements: [/* ... */] }), {
status: 402,
headers: { "Content-Type": "application/json" },
}),
// Phase 2: server accepts payment header
new Response(JSON.stringify({ data: "paid content" }), {
status: 200,
headers: { "Content-Type": "application/json" },
}),
])
const wrappedFetch = wrap(mockFetch, {
handlers: [testHandler],
})
const response = await wrappedFetch("https://api.example.com/resource")
// response.status === 200
Separate phase 1 and phase 2 mocks
Use phase1Fetch to mock each phase independently:
const phase1Mock = mock.responseFeeder([
new Response(JSON.stringify({ requirements: [/* ... */] }), { status: 402 }),
])
const phase2Mock = mock.responseFeeder([
new Response("OK", { status: 200 }),
])
const wrappedFetch = wrap(phase2Mock, {
handlers: [testHandler],
phase1Fetch: phase1Mock,
})
Using fetch-like functions
Instead of static Response objects, pass functions for dynamic responses:
const mockFetch = mock.responseFeeder([
async (input: RequestInfo | URL, init?: RequestInit) => {
const url = input.toString()
if (url.includes("/paid")) {
return new Response(null, { status: 402 })
}
return new Response("free", { status: 200 })
},
])
Further reading