(
baseFetch: FetchFn,
client: x402Client,
ttlMs = DEFAULT_TTL_MS,
options?: { skipPreAuth?: boolean; estimateAmount?: EstimateFn },
)
| 45 | type FetchFn = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>; |
| 46 | |
| 47 | export function createPayFetchWithPreAuth( |
| 48 | baseFetch: FetchFn, |
| 49 | client: x402Client, |
| 50 | ttlMs = DEFAULT_TTL_MS, |
| 51 | options?: { skipPreAuth?: boolean; estimateAmount?: EstimateFn }, |
| 52 | ): FetchFn { |
| 53 | const httpClient = new x402HTTPClient(client); |
| 54 | const cache = new Map<string, CachedEntry>(); |
| 55 | |
| 56 | return async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => { |
| 57 | const request = new Request(input, init); |
| 58 | const urlPath = new URL(request.url).pathname; |
| 59 | |
| 60 | // Extract model + size from the request body. Model gives a per-model cache |
| 61 | // key (a cached sonnet payment must not be applied to a free model); body |
| 62 | // length + max_tokens drive the up-front cost estimate used to decide |
| 63 | // whether a cached pre-auth still covers this (possibly larger) request. |
| 64 | let requestModel = ""; |
| 65 | let bodyLength = 0; |
| 66 | let maxTokens = 0; |
| 67 | if (init?.body) { |
| 68 | try { |
| 69 | const bodyStr = |
| 70 | init.body instanceof Uint8Array |
| 71 | ? new TextDecoder().decode(init.body) |
| 72 | : typeof init.body === "string" |
| 73 | ? init.body |
| 74 | : ""; |
| 75 | if (bodyStr) { |
| 76 | bodyLength = bodyStr.length; |
| 77 | const parsed = JSON.parse(bodyStr) as { model?: string; max_tokens?: number }; |
| 78 | requestModel = parsed.model ?? ""; |
| 79 | maxTokens = Number(parsed.max_tokens) || 0; |
| 80 | } |
| 81 | } catch { |
| 82 | /* not JSON, use empty model */ |
| 83 | } |
| 84 | } |
| 85 | const cacheKey = `${urlPath}:${requestModel}`; |
| 86 | |
| 87 | // Up-front estimate of what THIS request will cost (USDC micro-units), used |
| 88 | // both to gate pre-auth reuse and to record what a new cache entry covers. |
| 89 | const estimateMicros = (): number | undefined => { |
| 90 | if (!options?.estimateAmount || !requestModel) return undefined; |
| 91 | const est = options.estimateAmount(requestModel, bodyLength, maxTokens); |
| 92 | return est === undefined ? undefined : Number(est); |
| 93 | }; |
| 94 | const needMicros = estimateMicros(); |
| 95 | |
| 96 | // Try pre-auth only when we can PROVE the cached payment still covers this |
| 97 | // request (needMicros <= what the cached entry covered). Skip for Solana: |
| 98 | // payments use per-tx blockhashes that expire ~60-90s, making cached |
| 99 | // requirements useless and causing double charges. |
| 100 | const cached = !options?.skipPreAuth ? cache.get(cacheKey) : undefined; |
| 101 | const preAuthCovers = |
| 102 | cached !== undefined && |
| 103 | Date.now() - cached.cachedAt < ttlMs && |
| 104 | cached.coverMicros !== undefined && |
no test coverage detected