@blockrun/llm 3.2.3 → 3.5.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
@@ -165,15 +165,59 @@ const tweet = await client.chat('xai/grok-3-mini', 'What is trending on X?', { s
165
165
  **Supported endpoint:** `https://sol.blockrun.ai/api`
166
166
  **Payment:** Solana USDC (SPL, mainnet)
167
167
 
168
- ## How It Works
168
+ ## How Payment Works
169
169
 
170
- 1. You send a request to BlockRun's API
171
- 2. The API returns a 402 Payment Required with the price
172
- 3. The SDK automatically signs a USDC payment on Base
173
- 4. The request is retried with the payment proof
174
- 5. You receive the AI response
170
+ No API keys, no subscription. You hold USDC in your own wallet, and **every request pays for itself** with an on-chain micropayment. Two phases:
175
171
 
176
- **Your private key never leaves your machine** - it's only used for local signing.
172
+ ### Phase 1 — Fund your wallet once
173
+
174
+ You only do this when your balance runs low. Three ways to get USDC into your wallet:
175
+
176
+ - **(a) Buy with a card (Base USDC).** Call the new `onramp()` method to mint a one-time Coinbase Onramp link, then open the returned `pay.coinbase.com` URL — pay by card/bank in 60+ fiat currencies and the USDC lands in your wallet. The call itself is **free**. Onramp is **Base-only** (buying USDC with a card always lands Base USDC), and the funding address must equal your signing wallet:
177
+
178
+ ```typescript
179
+ const { url } = await client.onramp(client.getWalletAddress());
180
+ console.log(`Fund your wallet: ${url}`); // single-use, expires ~5 min — mint at click time
181
+ ```
182
+
183
+ - **(b) Transfer existing USDC.** Send USDC you already hold to your wallet address (`client.getWalletAddress()`). On Base, send Base USDC; on Solana (`SolanaLLMClient`), send Solana SPL USDC.
184
+
185
+ - **(c) Skip funding entirely.** Use the free NVIDIA models (e.g. `nvidia/deepseek-v4-flash`) — every call is **$0**, no balance required.
186
+
187
+ $5 of USDC covers thousands of paid requests. Check your balance any time:
188
+
189
+ ```typescript
190
+ const balance = await client.getBalance(); // USDC on Base
191
+ console.log(`Balance: $${balance.toFixed(2)} USDC`);
192
+ ```
193
+
194
+ ### Phase 2 — Every request pays itself (automatic x402)
195
+
196
+ You just call e.g. `client.chat(...)` — the payment is invisible:
197
+
198
+ 1. You send a request to BlockRun's API.
199
+ 2. The gateway returns **402 Payment Required** with the price.
200
+ 3. The SDK signs a USDC payment **locally** (EIP-712) — on **Base** for `LLMClient`, on **Solana** for `SolanaLLMClient` — using your wallet key.
201
+ 4. The request is retried automatically with the payment proof.
202
+ 5. The gateway settles on-chain and returns the AI response.
203
+
204
+ One call, no separate pay step. Free NVIDIA models settle at **$0** (no payment signed).
205
+
206
+ ### Track spend and verify settlements
207
+
208
+ ```typescript
209
+ import { getCostSummary } from '@blockrun/llm';
210
+
211
+ const spent = client.getSpending(); // this session
212
+ console.log(`Spent $${spent.totalUsd.toFixed(4)} across ${spent.calls} calls`);
213
+
214
+ const summary = getCostSummary(); // across sessions (~/.blockrun/data/costs.jsonl)
215
+ console.log(`Lifetime: $${summary.totalUsd.toFixed(2)} over ${summary.calls} calls`);
216
+ ```
217
+
218
+ Every paid request is a real on-chain USDC transfer — look up your wallet address on [Basescan](https://basescan.org) (or a Solana explorer) to verify each settlement independently.
219
+
220
+ **Non-custodial by design: your private key never leaves your machine** — it is only used for local signing, and no funds are ever held by BlockRun.
177
221
 
178
222
  ## Smart Routing (ClawRouter)
179
223
 
@@ -298,6 +342,7 @@ Released 2026-04-23 — first fully retrained base since GPT-4.5. 1M context, 12
298
342
  ### Anthropic Claude
299
343
  | Model | Input Price | Output Price | Context | Notes |
300
344
  |-------|-------------|--------------|---------|-------|
345
+ | `anthropic/claude-fable-5` | $10.00/M | $50.00/M | **1M** | Mythos-class flagship above Opus — always-on thinking, 128K output, fallback `claude-opus-4.8`. Alias: `claude-fable-5` |
301
346
  | `anthropic/claude-opus-4.8` | $5.00/M | $25.00/M | **1M** | Flagship — agentic coding + adaptive thinking, 128K output |
302
347
  | `anthropic/claude-opus-4.7` | $5.00/M | $25.00/M | **1M** | Agentic coding + adaptive thinking, 128K output |
303
348
  | `anthropic/claude-opus-4.6` | $5.00/M | $25.00/M | 200K | Hidden but still callable — kept as in-family hot-swap fallback |
@@ -1320,7 +1365,7 @@ const gptResponse = await client.messages.create({
1320
1365
  });
1321
1366
  ```
1322
1367
 
1323
- The `AnthropicClient` wraps the official `@anthropic-ai/sdk` with a custom fetch that handles x402 payment automatically. Your private key never leaves your machine.
1368
+ The `AnthropicClient` wraps the official `@anthropic-ai/sdk` with a custom fetch that handles x402 payment automatically. Your private key never leaves your machine. The Mythos-class `claude-fable-5` alias is available here too (1M context, always-on thinking).
1324
1369
 
1325
1370
  ## Links
1326
1371
 
package/dist/index.cjs CHANGED
@@ -544,7 +544,17 @@ function mapRawToImageModel(m) {
544
544
  }
545
545
  var DEFAULT_API_URL = "https://blockrun.ai/api";
546
546
  var DEFAULT_MAX_TOKENS = 1024;
547
- var DEFAULT_TIMEOUT = 6e4;
547
+ function resolveDefaultTimeout() {
548
+ const envVal = typeof process !== "undefined" && process.env ? process.env.BLOCKRUN_CHAT_TIMEOUT : void 0;
549
+ if (envVal !== void 0) {
550
+ const seconds = Number(envVal);
551
+ if (Number.isFinite(seconds) && seconds > 0) {
552
+ return seconds * 1e3;
553
+ }
554
+ }
555
+ return 6e5;
556
+ }
557
+ var DEFAULT_TIMEOUT = resolveDefaultTimeout();
548
558
  var SDK_VERSION = "1.5.0";
549
559
  var USER_AGENT = `blockrun-ts/${SDK_VERSION}`;
550
560
  var LLMClient = class _LLMClient {
@@ -1581,6 +1591,40 @@ var LLMClient = class _LLMClient {
1581
1591
  const data = await this.requestWithPaymentRaw("/v1/exa/find-similar", body);
1582
1592
  return data.data;
1583
1593
  }
1594
+ /**
1595
+ * Mint a one-time Coinbase Onramp link to fund this wallet with USDC on Base.
1596
+ *
1597
+ * FREE — the x402 signature only authenticates the wallet (no payment is
1598
+ * charged). Because the funding address must equal the signing wallet, this
1599
+ * mints a `pay.coinbase.com` link prefilled for `address` so a card/bank can
1600
+ * top it up (60+ fiat currencies → Base USDC). Base / USDC only.
1601
+ *
1602
+ * The returned URL is single-use and expires in ~5 minutes — mint it at click
1603
+ * time and never cache it. The funding `address` must match the signing wallet.
1604
+ *
1605
+ * @param address - Destination Base wallet address (0x + 40 hex chars). Must
1606
+ * equal the signing wallet that funds it.
1607
+ * @returns `{ url }` — a one-time https://pay.coinbase.com/... link.
1608
+ *
1609
+ * @example
1610
+ * const { url } = await client.onramp(client.getWalletAddress());
1611
+ * console.log(`Fund your wallet: ${url}`);
1612
+ */
1613
+ async onramp(address) {
1614
+ if (!/^0x[0-9a-fA-F]{40}$/.test(address)) {
1615
+ throw new Error("address must be a 0x-prefixed 40-hex-character Base address");
1616
+ }
1617
+ const data = await this.requestWithPaymentRaw("/v1/onramp/token", {
1618
+ address,
1619
+ network: "base",
1620
+ asset: "USDC"
1621
+ });
1622
+ const url = typeof data.url === "string" ? data.url : "";
1623
+ if (!url.startsWith("https://pay.coinbase.com/")) {
1624
+ throw new APIError("gateway returned no onramp url", 502, { message: "gateway returned no onramp url" });
1625
+ }
1626
+ return { url };
1627
+ }
1584
1628
  /**
1585
1629
  * Get USDC balance on Base mainnet.
1586
1630
  *
@@ -2522,8 +2566,9 @@ var DEFAULT_API_URL5 = "https://blockrun.ai/api";
2522
2566
  var DEFAULT_MODEL4 = "xai/grok-imagine-video";
2523
2567
  var DEFAULT_TIMEOUT5 = 12e4;
2524
2568
  var POLL_INTERVAL_MS2 = 5e3;
2525
- var DEFAULT_GENERATE_BUDGET_MS = 3e5;
2569
+ var DEFAULT_GENERATE_BUDGET_MS = 9e5;
2526
2570
  var MAX_TIMEOUT_SECONDS = 600;
2571
+ var MAX_POLL_RESIGNS = 2;
2527
2572
  var VideoClient = class {
2528
2573
  account;
2529
2574
  privateKey;
@@ -2551,8 +2596,9 @@ var VideoClient = class {
2551
2596
  * Generate a short video clip from a text prompt (or text + image).
2552
2597
  *
2553
2598
  * Submits an async job, then polls until the video is ready. Typical total
2554
- * wall-time is 60-180s. If upstream runs past the budget (default 5min),
2555
- * throws without charging.
2599
+ * wall-time is 60-180s, but upstream status can lag several minutes behind
2600
+ * actual completion. If upstream runs past the budget (default 15min),
2601
+ * throws without charging — the job stays claimable ~48h via poll_url.
2556
2602
  *
2557
2603
  * @param prompt - Text description of the video
2558
2604
  * @param options - Optional generation parameters
@@ -2670,21 +2716,9 @@ var VideoClient = class {
2670
2716
  if (resp402.status !== 402) {
2671
2717
  await this.throwApiError(resp402, "Expected 402 on first POST");
2672
2718
  }
2673
- const paymentRequired = await this.extractPaymentRequired(resp402);
2674
- const details = extractPaymentDetails(paymentRequired);
2675
- const paymentPayload = await createPaymentPayload(
2676
- this.privateKey,
2677
- this.account.address,
2678
- details.recipient,
2679
- details.amount,
2680
- details.network || "eip155:8453",
2681
- {
2682
- resourceUrl: details.resource?.url || submitUrl,
2683
- resourceDescription: details.resource?.description || "BlockRun Video Generation",
2684
- // Ensure signed auth covers the entire polling window.
2685
- maxTimeoutSeconds: Math.max(details.maxTimeoutSeconds || 0, MAX_TIMEOUT_SECONDS),
2686
- extra: details.extra
2687
- }
2719
+ let { payload: paymentPayload, details } = await this.signFromChallenge(
2720
+ resp402,
2721
+ submitUrl
2688
2722
  );
2689
2723
  const submitResp = await this.fetchWithTimeout(submitUrl, {
2690
2724
  method: "POST",
@@ -2711,6 +2745,7 @@ var VideoClient = class {
2711
2745
  const pollUrl = this.absolute(submitData.poll_url);
2712
2746
  const deadline = Date.now() + budgetMs;
2713
2747
  let lastStatus = submitData.status || "queued";
2748
+ let resignsLeft = MAX_POLL_RESIGNS;
2714
2749
  while (Date.now() < deadline) {
2715
2750
  await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS2));
2716
2751
  const pollResp = await this.fetchWithTimeout(pollUrl, {
@@ -2741,15 +2776,55 @@ var VideoClient = class {
2741
2776
  if (txHash) data.txHash = txHash;
2742
2777
  return data;
2743
2778
  }
2779
+ if (pollResp.status === 402) {
2780
+ if (resignsLeft > 0) {
2781
+ resignsLeft--;
2782
+ const challenge = await this.fetchWithTimeout(pollUrl, { method: "GET" });
2783
+ if (challenge.status === 402) {
2784
+ ({ payload: paymentPayload, details } = await this.signFromChallenge(
2785
+ challenge,
2786
+ pollUrl
2787
+ ));
2788
+ continue;
2789
+ }
2790
+ }
2791
+ throw new PaymentError(
2792
+ "Payment verification failed mid-poll (not a signature-expiry). Check the wallet balance and that you poll from the wallet that submitted the job."
2793
+ );
2794
+ }
2744
2795
  if (pollResp.status !== 200 && pollResp.status !== 202 && pollResp.status !== 504) {
2745
2796
  await this.throwApiError(pollResp, "Poll failed");
2746
2797
  }
2747
2798
  }
2748
2799
  throw new APIError(
2749
- `Video generation did not complete within ${Math.round(budgetMs / 1e3)}s (last status: ${lastStatus}). No payment was taken.`,
2800
+ `Video generation did not complete within ${Math.round(budgetMs / 1e3)}s (last status: ${lastStatus}). No payment was taken. The job is NOT lost: it stays claimable for ~48h \u2014 re-GET poll_url with a fresh signature from the same wallet to fetch (and settle) the finished video.`,
2750
2801
  504,
2751
- { id: submitData.id, last_status: lastStatus }
2802
+ { id: submitData.id, last_status: lastStatus, poll_url: pollUrl }
2803
+ );
2804
+ }
2805
+ /**
2806
+ * Parse an x402 challenge response and sign a payment payload for it.
2807
+ * Used for the initial submit AND for mid-poll re-signing after the 600s
2808
+ * authorization window lapses on long polls.
2809
+ */
2810
+ async signFromChallenge(resp402, fallbackUrl) {
2811
+ const paymentRequired = await this.extractPaymentRequired(resp402);
2812
+ const details = extractPaymentDetails(paymentRequired);
2813
+ const payload = await createPaymentPayload(
2814
+ this.privateKey,
2815
+ this.account.address,
2816
+ details.recipient,
2817
+ details.amount,
2818
+ details.network || "eip155:8453",
2819
+ {
2820
+ resourceUrl: details.resource?.url || fallbackUrl,
2821
+ resourceDescription: details.resource?.description || "BlockRun Video Generation",
2822
+ // Ensure signed auth covers as much of the polling window as allowed.
2823
+ maxTimeoutSeconds: Math.max(details.maxTimeoutSeconds || 0, MAX_TIMEOUT_SECONDS),
2824
+ extra: details.extra
2825
+ }
2752
2826
  );
2827
+ return { payload, details };
2753
2828
  }
2754
2829
  absolute(url) {
2755
2830
  if (url.startsWith("http://") || url.startsWith("https://")) return url;
@@ -5739,7 +5814,7 @@ var OpenAI = class {
5739
5814
  constructor(options = {}) {
5740
5815
  const privateKey = options.walletKey || options.privateKey;
5741
5816
  const apiUrl = options.baseURL || "https://blockrun.ai/api";
5742
- const timeout = options.timeout || 6e4;
5817
+ const timeout = options.timeout ?? DEFAULT_TIMEOUT;
5743
5818
  this.client = new LLMClient({
5744
5819
  privateKey,
5745
5820
  apiUrl,
@@ -5773,7 +5848,7 @@ var AnthropicClient = class {
5773
5848
  const apiUrl = options.apiUrl ?? "https://blockrun.ai/api";
5774
5849
  validateApiUrl(apiUrl);
5775
5850
  this._apiUrl = apiUrl.replace(/\/$/, "");
5776
- this._timeout = options.timeout ?? 6e4;
5851
+ this._timeout = options.timeout ?? DEFAULT_TIMEOUT;
5777
5852
  }
5778
5853
  async _getClient() {
5779
5854
  if (this._client) return this._client;
package/dist/index.d.cts CHANGED
@@ -245,7 +245,7 @@ interface LLMClientOptions {
245
245
  privateKey?: `0x${string}` | string;
246
246
  /** API endpoint URL (default: https://blockrun.ai/api) */
247
247
  apiUrl?: string;
248
- /** Request timeout in milliseconds (default: 60000) */
248
+ /** Request timeout in milliseconds (default: 600000 / 600s; override via BLOCKRUN_CHAT_TIMEOUT env, in seconds) */
249
249
  timeout?: number;
250
250
  }
251
251
  /**
@@ -727,6 +727,10 @@ interface ExaFindSimilarOptions {
727
727
  /** Exclude pages from the same domain as the reference URL */
728
728
  excludeSourceDomain?: boolean;
729
729
  }
730
+ interface OnrampResult {
731
+ /** One-time https://pay.coinbase.com/... URL prefilled for this wallet. */
732
+ url: string;
733
+ }
730
734
  type PriceCategory = "crypto" | "fx" | "commodity" | "usstock" | "stocks";
731
735
  type StockMarket = "us" | "hk" | "jp" | "kr" | "gb" | "de" | "fr" | "nl" | "ie" | "lu" | "cn" | "ca";
732
736
  type BarResolution = "1" | "5" | "15" | "60" | "240" | "D" | "W" | "M";
@@ -806,7 +810,7 @@ interface PhoneClientOptions {
806
810
  privateKey?: `0x${string}` | string;
807
811
  /** API endpoint URL (default: https://blockrun.ai/api) */
808
812
  apiUrl?: string;
809
- /** Request timeout in milliseconds (default: 60000) */
813
+ /** Request timeout in milliseconds (default: 600000 / 600s; override via BLOCKRUN_CHAT_TIMEOUT env, in seconds) */
810
814
  timeout?: number;
811
815
  }
812
816
  /**
@@ -875,7 +879,7 @@ interface PortraitClientOptions {
875
879
  privateKey?: `0x${string}` | string;
876
880
  /** API endpoint URL (default: https://blockrun.ai/api) */
877
881
  apiUrl?: string;
878
- /** Request timeout in milliseconds (default: 60000) */
882
+ /** Request timeout in milliseconds (default: 600000 / 600s; override via BLOCKRUN_CHAT_TIMEOUT env, in seconds) */
879
883
  timeout?: number;
880
884
  }
881
885
  interface PortraitEnrollOptions {
@@ -1220,6 +1224,26 @@ declare class LLMClient {
1220
1224
  * @param options - Optional filters (numResults, excludeSourceDomain)
1221
1225
  */
1222
1226
  exaFindSimilar(url: string, options?: ExaFindSimilarOptions): Promise<ExaSearchResponse>;
1227
+ /**
1228
+ * Mint a one-time Coinbase Onramp link to fund this wallet with USDC on Base.
1229
+ *
1230
+ * FREE — the x402 signature only authenticates the wallet (no payment is
1231
+ * charged). Because the funding address must equal the signing wallet, this
1232
+ * mints a `pay.coinbase.com` link prefilled for `address` so a card/bank can
1233
+ * top it up (60+ fiat currencies → Base USDC). Base / USDC only.
1234
+ *
1235
+ * The returned URL is single-use and expires in ~5 minutes — mint it at click
1236
+ * time and never cache it. The funding `address` must match the signing wallet.
1237
+ *
1238
+ * @param address - Destination Base wallet address (0x + 40 hex chars). Must
1239
+ * equal the signing wallet that funds it.
1240
+ * @returns `{ url }` — a one-time https://pay.coinbase.com/... link.
1241
+ *
1242
+ * @example
1243
+ * const { url } = await client.onramp(client.getWalletAddress());
1244
+ * console.log(`Fund your wallet: ${url}`);
1245
+ */
1246
+ onramp(address: string): Promise<OnrampResult>;
1223
1247
  /**
1224
1248
  * Get USDC balance on Base mainnet.
1225
1249
  *
@@ -1658,7 +1682,8 @@ declare class SpeechClient {
1658
1682
  * POST /v1/videos/generations -> 402 -> sign -> 202 { id, poll_url }
1659
1683
  * GET /v1/videos/generations/{id} -> loop until status=completed
1660
1684
  *
1661
- * The client signs ONCE and replays the same PAYMENT-SIGNATURE on every poll.
1685
+ * The client signs once and replays the same PAYMENT-SIGNATURE on every poll,
1686
+ * re-signing automatically if the 600s authorization window lapses mid-poll.
1662
1687
  * Settlement happens only on the first completed poll, so upstream failure or
1663
1688
  * the caller giving up = zero charge.
1664
1689
  *
@@ -1689,8 +1714,9 @@ declare class VideoClient {
1689
1714
  * Generate a short video clip from a text prompt (or text + image).
1690
1715
  *
1691
1716
  * Submits an async job, then polls until the video is ready. Typical total
1692
- * wall-time is 60-180s. If upstream runs past the budget (default 5min),
1693
- * throws without charging.
1717
+ * wall-time is 60-180s, but upstream status can lag several minutes behind
1718
+ * actual completion. If upstream runs past the budget (default 15min),
1719
+ * throws without charging — the job stays claimable ~48h via poll_url.
1694
1720
  *
1695
1721
  * @param prompt - Text description of the video
1696
1722
  * @param options - Optional generation parameters
@@ -1734,6 +1760,12 @@ declare class VideoClient {
1734
1760
  returnLastFrame?: boolean;
1735
1761
  } & Record<string, unknown>): Promise<VideoResponse>;
1736
1762
  private submitAndPoll;
1763
+ /**
1764
+ * Parse an x402 challenge response and sign a payment payload for it.
1765
+ * Used for the initial submit AND for mid-poll re-signing after the 600s
1766
+ * authorization window lapses on long polls.
1767
+ */
1768
+ private signFromChallenge;
1737
1769
  private absolute;
1738
1770
  private extractPaymentRequired;
1739
1771
  private throwApiError;
package/dist/index.d.ts CHANGED
@@ -245,7 +245,7 @@ interface LLMClientOptions {
245
245
  privateKey?: `0x${string}` | string;
246
246
  /** API endpoint URL (default: https://blockrun.ai/api) */
247
247
  apiUrl?: string;
248
- /** Request timeout in milliseconds (default: 60000) */
248
+ /** Request timeout in milliseconds (default: 600000 / 600s; override via BLOCKRUN_CHAT_TIMEOUT env, in seconds) */
249
249
  timeout?: number;
250
250
  }
251
251
  /**
@@ -727,6 +727,10 @@ interface ExaFindSimilarOptions {
727
727
  /** Exclude pages from the same domain as the reference URL */
728
728
  excludeSourceDomain?: boolean;
729
729
  }
730
+ interface OnrampResult {
731
+ /** One-time https://pay.coinbase.com/... URL prefilled for this wallet. */
732
+ url: string;
733
+ }
730
734
  type PriceCategory = "crypto" | "fx" | "commodity" | "usstock" | "stocks";
731
735
  type StockMarket = "us" | "hk" | "jp" | "kr" | "gb" | "de" | "fr" | "nl" | "ie" | "lu" | "cn" | "ca";
732
736
  type BarResolution = "1" | "5" | "15" | "60" | "240" | "D" | "W" | "M";
@@ -806,7 +810,7 @@ interface PhoneClientOptions {
806
810
  privateKey?: `0x${string}` | string;
807
811
  /** API endpoint URL (default: https://blockrun.ai/api) */
808
812
  apiUrl?: string;
809
- /** Request timeout in milliseconds (default: 60000) */
813
+ /** Request timeout in milliseconds (default: 600000 / 600s; override via BLOCKRUN_CHAT_TIMEOUT env, in seconds) */
810
814
  timeout?: number;
811
815
  }
812
816
  /**
@@ -875,7 +879,7 @@ interface PortraitClientOptions {
875
879
  privateKey?: `0x${string}` | string;
876
880
  /** API endpoint URL (default: https://blockrun.ai/api) */
877
881
  apiUrl?: string;
878
- /** Request timeout in milliseconds (default: 60000) */
882
+ /** Request timeout in milliseconds (default: 600000 / 600s; override via BLOCKRUN_CHAT_TIMEOUT env, in seconds) */
879
883
  timeout?: number;
880
884
  }
881
885
  interface PortraitEnrollOptions {
@@ -1220,6 +1224,26 @@ declare class LLMClient {
1220
1224
  * @param options - Optional filters (numResults, excludeSourceDomain)
1221
1225
  */
1222
1226
  exaFindSimilar(url: string, options?: ExaFindSimilarOptions): Promise<ExaSearchResponse>;
1227
+ /**
1228
+ * Mint a one-time Coinbase Onramp link to fund this wallet with USDC on Base.
1229
+ *
1230
+ * FREE — the x402 signature only authenticates the wallet (no payment is
1231
+ * charged). Because the funding address must equal the signing wallet, this
1232
+ * mints a `pay.coinbase.com` link prefilled for `address` so a card/bank can
1233
+ * top it up (60+ fiat currencies → Base USDC). Base / USDC only.
1234
+ *
1235
+ * The returned URL is single-use and expires in ~5 minutes — mint it at click
1236
+ * time and never cache it. The funding `address` must match the signing wallet.
1237
+ *
1238
+ * @param address - Destination Base wallet address (0x + 40 hex chars). Must
1239
+ * equal the signing wallet that funds it.
1240
+ * @returns `{ url }` — a one-time https://pay.coinbase.com/... link.
1241
+ *
1242
+ * @example
1243
+ * const { url } = await client.onramp(client.getWalletAddress());
1244
+ * console.log(`Fund your wallet: ${url}`);
1245
+ */
1246
+ onramp(address: string): Promise<OnrampResult>;
1223
1247
  /**
1224
1248
  * Get USDC balance on Base mainnet.
1225
1249
  *
@@ -1658,7 +1682,8 @@ declare class SpeechClient {
1658
1682
  * POST /v1/videos/generations -> 402 -> sign -> 202 { id, poll_url }
1659
1683
  * GET /v1/videos/generations/{id} -> loop until status=completed
1660
1684
  *
1661
- * The client signs ONCE and replays the same PAYMENT-SIGNATURE on every poll.
1685
+ * The client signs once and replays the same PAYMENT-SIGNATURE on every poll,
1686
+ * re-signing automatically if the 600s authorization window lapses mid-poll.
1662
1687
  * Settlement happens only on the first completed poll, so upstream failure or
1663
1688
  * the caller giving up = zero charge.
1664
1689
  *
@@ -1689,8 +1714,9 @@ declare class VideoClient {
1689
1714
  * Generate a short video clip from a text prompt (or text + image).
1690
1715
  *
1691
1716
  * Submits an async job, then polls until the video is ready. Typical total
1692
- * wall-time is 60-180s. If upstream runs past the budget (default 5min),
1693
- * throws without charging.
1717
+ * wall-time is 60-180s, but upstream status can lag several minutes behind
1718
+ * actual completion. If upstream runs past the budget (default 15min),
1719
+ * throws without charging — the job stays claimable ~48h via poll_url.
1694
1720
  *
1695
1721
  * @param prompt - Text description of the video
1696
1722
  * @param options - Optional generation parameters
@@ -1734,6 +1760,12 @@ declare class VideoClient {
1734
1760
  returnLastFrame?: boolean;
1735
1761
  } & Record<string, unknown>): Promise<VideoResponse>;
1736
1762
  private submitAndPoll;
1763
+ /**
1764
+ * Parse an x402 challenge response and sign a payment payload for it.
1765
+ * Used for the initial submit AND for mid-poll re-signing after the 600s
1766
+ * authorization window lapses on long polls.
1767
+ */
1768
+ private signFromChallenge;
1737
1769
  private absolute;
1738
1770
  private extractPaymentRequired;
1739
1771
  private throwApiError;
package/dist/index.js CHANGED
@@ -437,7 +437,17 @@ function mapRawToImageModel(m) {
437
437
  }
438
438
  var DEFAULT_API_URL = "https://blockrun.ai/api";
439
439
  var DEFAULT_MAX_TOKENS = 1024;
440
- var DEFAULT_TIMEOUT = 6e4;
440
+ function resolveDefaultTimeout() {
441
+ const envVal = typeof process !== "undefined" && process.env ? process.env.BLOCKRUN_CHAT_TIMEOUT : void 0;
442
+ if (envVal !== void 0) {
443
+ const seconds = Number(envVal);
444
+ if (Number.isFinite(seconds) && seconds > 0) {
445
+ return seconds * 1e3;
446
+ }
447
+ }
448
+ return 6e5;
449
+ }
450
+ var DEFAULT_TIMEOUT = resolveDefaultTimeout();
441
451
  var SDK_VERSION = "1.5.0";
442
452
  var USER_AGENT = `blockrun-ts/${SDK_VERSION}`;
443
453
  var LLMClient = class _LLMClient {
@@ -1474,6 +1484,40 @@ var LLMClient = class _LLMClient {
1474
1484
  const data = await this.requestWithPaymentRaw("/v1/exa/find-similar", body);
1475
1485
  return data.data;
1476
1486
  }
1487
+ /**
1488
+ * Mint a one-time Coinbase Onramp link to fund this wallet with USDC on Base.
1489
+ *
1490
+ * FREE — the x402 signature only authenticates the wallet (no payment is
1491
+ * charged). Because the funding address must equal the signing wallet, this
1492
+ * mints a `pay.coinbase.com` link prefilled for `address` so a card/bank can
1493
+ * top it up (60+ fiat currencies → Base USDC). Base / USDC only.
1494
+ *
1495
+ * The returned URL is single-use and expires in ~5 minutes — mint it at click
1496
+ * time and never cache it. The funding `address` must match the signing wallet.
1497
+ *
1498
+ * @param address - Destination Base wallet address (0x + 40 hex chars). Must
1499
+ * equal the signing wallet that funds it.
1500
+ * @returns `{ url }` — a one-time https://pay.coinbase.com/... link.
1501
+ *
1502
+ * @example
1503
+ * const { url } = await client.onramp(client.getWalletAddress());
1504
+ * console.log(`Fund your wallet: ${url}`);
1505
+ */
1506
+ async onramp(address) {
1507
+ if (!/^0x[0-9a-fA-F]{40}$/.test(address)) {
1508
+ throw new Error("address must be a 0x-prefixed 40-hex-character Base address");
1509
+ }
1510
+ const data = await this.requestWithPaymentRaw("/v1/onramp/token", {
1511
+ address,
1512
+ network: "base",
1513
+ asset: "USDC"
1514
+ });
1515
+ const url = typeof data.url === "string" ? data.url : "";
1516
+ if (!url.startsWith("https://pay.coinbase.com/")) {
1517
+ throw new APIError("gateway returned no onramp url", 502, { message: "gateway returned no onramp url" });
1518
+ }
1519
+ return { url };
1520
+ }
1477
1521
  /**
1478
1522
  * Get USDC balance on Base mainnet.
1479
1523
  *
@@ -2415,8 +2459,9 @@ var DEFAULT_API_URL5 = "https://blockrun.ai/api";
2415
2459
  var DEFAULT_MODEL4 = "xai/grok-imagine-video";
2416
2460
  var DEFAULT_TIMEOUT5 = 12e4;
2417
2461
  var POLL_INTERVAL_MS2 = 5e3;
2418
- var DEFAULT_GENERATE_BUDGET_MS = 3e5;
2462
+ var DEFAULT_GENERATE_BUDGET_MS = 9e5;
2419
2463
  var MAX_TIMEOUT_SECONDS = 600;
2464
+ var MAX_POLL_RESIGNS = 2;
2420
2465
  var VideoClient = class {
2421
2466
  account;
2422
2467
  privateKey;
@@ -2444,8 +2489,9 @@ var VideoClient = class {
2444
2489
  * Generate a short video clip from a text prompt (or text + image).
2445
2490
  *
2446
2491
  * Submits an async job, then polls until the video is ready. Typical total
2447
- * wall-time is 60-180s. If upstream runs past the budget (default 5min),
2448
- * throws without charging.
2492
+ * wall-time is 60-180s, but upstream status can lag several minutes behind
2493
+ * actual completion. If upstream runs past the budget (default 15min),
2494
+ * throws without charging — the job stays claimable ~48h via poll_url.
2449
2495
  *
2450
2496
  * @param prompt - Text description of the video
2451
2497
  * @param options - Optional generation parameters
@@ -2563,21 +2609,9 @@ var VideoClient = class {
2563
2609
  if (resp402.status !== 402) {
2564
2610
  await this.throwApiError(resp402, "Expected 402 on first POST");
2565
2611
  }
2566
- const paymentRequired = await this.extractPaymentRequired(resp402);
2567
- const details = extractPaymentDetails(paymentRequired);
2568
- const paymentPayload = await createPaymentPayload(
2569
- this.privateKey,
2570
- this.account.address,
2571
- details.recipient,
2572
- details.amount,
2573
- details.network || "eip155:8453",
2574
- {
2575
- resourceUrl: details.resource?.url || submitUrl,
2576
- resourceDescription: details.resource?.description || "BlockRun Video Generation",
2577
- // Ensure signed auth covers the entire polling window.
2578
- maxTimeoutSeconds: Math.max(details.maxTimeoutSeconds || 0, MAX_TIMEOUT_SECONDS),
2579
- extra: details.extra
2580
- }
2612
+ let { payload: paymentPayload, details } = await this.signFromChallenge(
2613
+ resp402,
2614
+ submitUrl
2581
2615
  );
2582
2616
  const submitResp = await this.fetchWithTimeout(submitUrl, {
2583
2617
  method: "POST",
@@ -2604,6 +2638,7 @@ var VideoClient = class {
2604
2638
  const pollUrl = this.absolute(submitData.poll_url);
2605
2639
  const deadline = Date.now() + budgetMs;
2606
2640
  let lastStatus = submitData.status || "queued";
2641
+ let resignsLeft = MAX_POLL_RESIGNS;
2607
2642
  while (Date.now() < deadline) {
2608
2643
  await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS2));
2609
2644
  const pollResp = await this.fetchWithTimeout(pollUrl, {
@@ -2634,15 +2669,55 @@ var VideoClient = class {
2634
2669
  if (txHash) data.txHash = txHash;
2635
2670
  return data;
2636
2671
  }
2672
+ if (pollResp.status === 402) {
2673
+ if (resignsLeft > 0) {
2674
+ resignsLeft--;
2675
+ const challenge = await this.fetchWithTimeout(pollUrl, { method: "GET" });
2676
+ if (challenge.status === 402) {
2677
+ ({ payload: paymentPayload, details } = await this.signFromChallenge(
2678
+ challenge,
2679
+ pollUrl
2680
+ ));
2681
+ continue;
2682
+ }
2683
+ }
2684
+ throw new PaymentError(
2685
+ "Payment verification failed mid-poll (not a signature-expiry). Check the wallet balance and that you poll from the wallet that submitted the job."
2686
+ );
2687
+ }
2637
2688
  if (pollResp.status !== 200 && pollResp.status !== 202 && pollResp.status !== 504) {
2638
2689
  await this.throwApiError(pollResp, "Poll failed");
2639
2690
  }
2640
2691
  }
2641
2692
  throw new APIError(
2642
- `Video generation did not complete within ${Math.round(budgetMs / 1e3)}s (last status: ${lastStatus}). No payment was taken.`,
2693
+ `Video generation did not complete within ${Math.round(budgetMs / 1e3)}s (last status: ${lastStatus}). No payment was taken. The job is NOT lost: it stays claimable for ~48h \u2014 re-GET poll_url with a fresh signature from the same wallet to fetch (and settle) the finished video.`,
2643
2694
  504,
2644
- { id: submitData.id, last_status: lastStatus }
2695
+ { id: submitData.id, last_status: lastStatus, poll_url: pollUrl }
2696
+ );
2697
+ }
2698
+ /**
2699
+ * Parse an x402 challenge response and sign a payment payload for it.
2700
+ * Used for the initial submit AND for mid-poll re-signing after the 600s
2701
+ * authorization window lapses on long polls.
2702
+ */
2703
+ async signFromChallenge(resp402, fallbackUrl) {
2704
+ const paymentRequired = await this.extractPaymentRequired(resp402);
2705
+ const details = extractPaymentDetails(paymentRequired);
2706
+ const payload = await createPaymentPayload(
2707
+ this.privateKey,
2708
+ this.account.address,
2709
+ details.recipient,
2710
+ details.amount,
2711
+ details.network || "eip155:8453",
2712
+ {
2713
+ resourceUrl: details.resource?.url || fallbackUrl,
2714
+ resourceDescription: details.resource?.description || "BlockRun Video Generation",
2715
+ // Ensure signed auth covers as much of the polling window as allowed.
2716
+ maxTimeoutSeconds: Math.max(details.maxTimeoutSeconds || 0, MAX_TIMEOUT_SECONDS),
2717
+ extra: details.extra
2718
+ }
2645
2719
  );
2720
+ return { payload, details };
2646
2721
  }
2647
2722
  absolute(url) {
2648
2723
  if (url.startsWith("http://") || url.startsWith("https://")) return url;
@@ -5632,7 +5707,7 @@ var OpenAI = class {
5632
5707
  constructor(options = {}) {
5633
5708
  const privateKey = options.walletKey || options.privateKey;
5634
5709
  const apiUrl = options.baseURL || "https://blockrun.ai/api";
5635
- const timeout = options.timeout || 6e4;
5710
+ const timeout = options.timeout ?? DEFAULT_TIMEOUT;
5636
5711
  this.client = new LLMClient({
5637
5712
  privateKey,
5638
5713
  apiUrl,
@@ -5666,7 +5741,7 @@ var AnthropicClient = class {
5666
5741
  const apiUrl = options.apiUrl ?? "https://blockrun.ai/api";
5667
5742
  validateApiUrl(apiUrl);
5668
5743
  this._apiUrl = apiUrl.replace(/\/$/, "");
5669
- this._timeout = options.timeout ?? 6e4;
5744
+ this._timeout = options.timeout ?? DEFAULT_TIMEOUT;
5670
5745
  }
5671
5746
  async _getClient() {
5672
5747
  if (this._client) return this._client;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockrun/llm",
3
- "version": "3.2.3",
3
+ "version": "3.5.0",
4
4
  "type": "module",
5
5
  "description": "BlockRun SDK - Pay-per-request AI (LLM, Image, Video, Music, Voice) via x402 on Base and Solana",
6
6
  "main": "dist/index.cjs",