@blockrun/llm 1.4.1 → 1.4.2

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/dist/index.js ADDED
@@ -0,0 +1,2716 @@
1
+ import {
2
+ __require,
3
+ __toCommonJS,
4
+ index_esm_exports,
5
+ init_index_esm
6
+ } from "./chunk-KRDGCX7W.js";
7
+
8
+ // src/client.ts
9
+ import { privateKeyToAccount } from "viem/accounts";
10
+
11
+ // src/types.ts
12
+ var BlockrunError = class extends Error {
13
+ constructor(message) {
14
+ super(message);
15
+ this.name = "BlockrunError";
16
+ }
17
+ };
18
+ var PaymentError = class extends BlockrunError {
19
+ constructor(message) {
20
+ super(message);
21
+ this.name = "PaymentError";
22
+ }
23
+ };
24
+ var APIError = class extends BlockrunError {
25
+ statusCode;
26
+ response;
27
+ constructor(message, statusCode, response) {
28
+ super(message);
29
+ this.name = "APIError";
30
+ this.statusCode = statusCode;
31
+ this.response = response;
32
+ }
33
+ };
34
+
35
+ // src/client.ts
36
+ import { route, DEFAULT_ROUTING_CONFIG } from "@blockrun/clawrouter";
37
+
38
+ // src/x402.ts
39
+ import { signTypedData } from "viem/accounts";
40
+ var BASE_CHAIN_ID = 8453;
41
+ var USDC_BASE = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
42
+ var SOLANA_NETWORK = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
43
+ var USDC_SOLANA = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
44
+ var DEFAULT_COMPUTE_UNIT_PRICE_MICROLAMPORTS = 1;
45
+ var DEFAULT_COMPUTE_UNIT_LIMIT = 8e3;
46
+ var USDC_DOMAIN = {
47
+ name: "USD Coin",
48
+ version: "2",
49
+ chainId: BASE_CHAIN_ID,
50
+ verifyingContract: USDC_BASE
51
+ };
52
+ var TRANSFER_TYPES = {
53
+ TransferWithAuthorization: [
54
+ { name: "from", type: "address" },
55
+ { name: "to", type: "address" },
56
+ { name: "value", type: "uint256" },
57
+ { name: "validAfter", type: "uint256" },
58
+ { name: "validBefore", type: "uint256" },
59
+ { name: "nonce", type: "bytes32" }
60
+ ]
61
+ };
62
+ function createNonce() {
63
+ const bytes = new Uint8Array(32);
64
+ crypto.getRandomValues(bytes);
65
+ return `0x${Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("")}`;
66
+ }
67
+ async function createPaymentPayload(privateKey, fromAddress, recipient, amount, network = "eip155:8453", options = {}) {
68
+ const now = Math.floor(Date.now() / 1e3);
69
+ const validAfter = now - 600;
70
+ const validBefore = now + (options.maxTimeoutSeconds || 300);
71
+ const nonce = createNonce();
72
+ const domain = USDC_DOMAIN;
73
+ const signature = await signTypedData({
74
+ privateKey,
75
+ domain,
76
+ types: TRANSFER_TYPES,
77
+ primaryType: "TransferWithAuthorization",
78
+ message: {
79
+ from: fromAddress,
80
+ to: recipient,
81
+ value: BigInt(amount),
82
+ validAfter: BigInt(validAfter),
83
+ validBefore: BigInt(validBefore),
84
+ nonce
85
+ }
86
+ });
87
+ const paymentData = {
88
+ x402Version: 2,
89
+ resource: {
90
+ url: options.resourceUrl || "https://blockrun.ai/api/v1/chat/completions",
91
+ description: options.resourceDescription || "BlockRun AI API call",
92
+ mimeType: "application/json"
93
+ },
94
+ accepted: {
95
+ scheme: "exact",
96
+ network,
97
+ amount,
98
+ asset: USDC_BASE,
99
+ payTo: recipient,
100
+ maxTimeoutSeconds: options.maxTimeoutSeconds || 300,
101
+ extra: { name: "USD Coin", version: "2" }
102
+ },
103
+ payload: {
104
+ signature,
105
+ authorization: {
106
+ from: fromAddress,
107
+ to: recipient,
108
+ value: amount,
109
+ validAfter: validAfter.toString(),
110
+ validBefore: validBefore.toString(),
111
+ nonce
112
+ }
113
+ },
114
+ extensions: options.extensions || {}
115
+ };
116
+ return btoa(JSON.stringify(paymentData));
117
+ }
118
+ async function createSolanaPaymentPayload(secretKey, fromAddress, recipient, amount, feePayer, options = {}) {
119
+ const { Connection, PublicKey, TransactionMessage, VersionedTransaction, ComputeBudgetProgram } = await import("./index.esm-SXKIFLA7.js");
120
+ const { getAssociatedTokenAddress, createTransferCheckedInstruction, getMint } = await import("./esm-PTFDM6PE.js");
121
+ const { Keypair } = await import("./index.esm-SXKIFLA7.js");
122
+ const rpcUrl = options.rpcUrl || "https://api.mainnet-beta.solana.com";
123
+ const connection = new Connection(rpcUrl);
124
+ const keypair = Keypair.fromSecretKey(secretKey);
125
+ const feePayerPubkey = new PublicKey(feePayer);
126
+ const ownerPubkey = keypair.publicKey;
127
+ const tokenMint = new PublicKey(USDC_SOLANA);
128
+ const payToPubkey = new PublicKey(recipient);
129
+ const mintInfo = await getMint(connection, tokenMint);
130
+ const sourceATA = await getAssociatedTokenAddress(tokenMint, ownerPubkey, false);
131
+ const destinationATA = await getAssociatedTokenAddress(tokenMint, payToPubkey, false);
132
+ const { blockhash } = await connection.getLatestBlockhash();
133
+ const setComputeUnitPriceIx = ComputeBudgetProgram.setComputeUnitPrice({
134
+ microLamports: DEFAULT_COMPUTE_UNIT_PRICE_MICROLAMPORTS
135
+ });
136
+ const setComputeUnitLimitIx = ComputeBudgetProgram.setComputeUnitLimit({
137
+ units: DEFAULT_COMPUTE_UNIT_LIMIT
138
+ });
139
+ const transferIx = createTransferCheckedInstruction(
140
+ sourceATA,
141
+ tokenMint,
142
+ destinationATA,
143
+ ownerPubkey,
144
+ BigInt(amount),
145
+ mintInfo.decimals
146
+ );
147
+ const messageV0 = new TransactionMessage({
148
+ payerKey: feePayerPubkey,
149
+ recentBlockhash: blockhash,
150
+ instructions: [setComputeUnitLimitIx, setComputeUnitPriceIx, transferIx]
151
+ }).compileToV0Message();
152
+ const transaction = new VersionedTransaction(messageV0);
153
+ transaction.sign([keypair]);
154
+ const serializedTx = Buffer.from(transaction.serialize()).toString("base64");
155
+ const paymentData = {
156
+ x402Version: 2,
157
+ resource: {
158
+ url: options.resourceUrl || "https://blockrun.ai/api/v1/chat/completions",
159
+ description: options.resourceDescription || "BlockRun AI API call",
160
+ mimeType: "application/json"
161
+ },
162
+ accepted: {
163
+ scheme: "exact",
164
+ network: SOLANA_NETWORK,
165
+ amount,
166
+ asset: USDC_SOLANA,
167
+ payTo: recipient,
168
+ maxTimeoutSeconds: options.maxTimeoutSeconds || 300,
169
+ extra: options.extra || { feePayer }
170
+ },
171
+ payload: {
172
+ transaction: serializedTx
173
+ },
174
+ extensions: options.extensions || {}
175
+ };
176
+ return btoa(JSON.stringify(paymentData));
177
+ }
178
+ function parsePaymentRequired(headerValue) {
179
+ try {
180
+ const decoded = atob(headerValue);
181
+ const parsed = JSON.parse(decoded);
182
+ if (!parsed.accepts || !Array.isArray(parsed.accepts)) {
183
+ throw new Error("Invalid payment required structure: missing or invalid 'accepts' field");
184
+ }
185
+ return parsed;
186
+ } catch (error) {
187
+ if (error instanceof Error) {
188
+ if (error.message.includes("Invalid payment required structure")) {
189
+ throw error;
190
+ }
191
+ throw new Error("Failed to parse payment required header: invalid format");
192
+ }
193
+ throw new Error("Failed to parse payment required header");
194
+ }
195
+ }
196
+ function extractPaymentDetails(paymentRequired, preferredNetwork) {
197
+ const accepts = paymentRequired.accepts || [];
198
+ if (accepts.length === 0) {
199
+ throw new Error("No payment options in payment required response");
200
+ }
201
+ let option = null;
202
+ if (preferredNetwork) {
203
+ option = accepts.find((opt) => opt.network === preferredNetwork) || null;
204
+ }
205
+ if (!option) {
206
+ option = accepts[0];
207
+ }
208
+ const amount = option.amount || option.maxAmountRequired;
209
+ if (!amount) {
210
+ throw new Error("No amount found in payment requirements");
211
+ }
212
+ return {
213
+ amount,
214
+ recipient: option.payTo,
215
+ network: option.network,
216
+ asset: option.asset,
217
+ scheme: option.scheme,
218
+ maxTimeoutSeconds: option.maxTimeoutSeconds || 300,
219
+ extra: option.extra,
220
+ resource: paymentRequired.resource
221
+ };
222
+ }
223
+
224
+ // src/validation.ts
225
+ var LOCALHOST_DOMAINS = ["localhost", "127.0.0.1"];
226
+ function validatePrivateKey(key) {
227
+ if (typeof key !== "string") {
228
+ throw new Error("Private key must be a string");
229
+ }
230
+ if (!key.startsWith("0x")) {
231
+ throw new Error("Private key must start with 0x");
232
+ }
233
+ if (key.length !== 66) {
234
+ throw new Error(
235
+ "Private key must be 66 characters (0x + 64 hexadecimal characters)"
236
+ );
237
+ }
238
+ if (!/^0x[0-9a-fA-F]{64}$/.test(key)) {
239
+ throw new Error(
240
+ "Private key must contain only hexadecimal characters (0-9, a-f, A-F)"
241
+ );
242
+ }
243
+ }
244
+ function validateApiUrl(url) {
245
+ let parsed;
246
+ try {
247
+ parsed = new URL(url);
248
+ } catch {
249
+ throw new Error("Invalid API URL format");
250
+ }
251
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
252
+ throw new Error(
253
+ `Invalid protocol: ${parsed.protocol}. Use http:// or https://`
254
+ );
255
+ }
256
+ const isLocalhost = LOCALHOST_DOMAINS.includes(parsed.hostname);
257
+ if (parsed.protocol !== "https:" && !isLocalhost) {
258
+ throw new Error(
259
+ `API URL must use HTTPS for non-localhost endpoints. Use https:// instead of ${parsed.protocol}//`
260
+ );
261
+ }
262
+ }
263
+ function sanitizeErrorResponse(errorBody) {
264
+ if (typeof errorBody !== "object" || errorBody === null) {
265
+ return { message: "API request failed" };
266
+ }
267
+ const body = errorBody;
268
+ return {
269
+ message: typeof body.error === "string" ? body.error : "API request failed",
270
+ code: typeof body.code === "string" ? body.code : void 0
271
+ };
272
+ }
273
+ function validateResourceUrl(url, baseUrl) {
274
+ try {
275
+ const parsed = new URL(url);
276
+ const baseParsed = new URL(baseUrl);
277
+ if (parsed.hostname !== baseParsed.hostname) {
278
+ console.warn(
279
+ `Resource URL hostname mismatch: ${parsed.hostname} vs ${baseParsed.hostname}. Using safe default instead.`
280
+ );
281
+ return `${baseUrl}/v1/chat/completions`;
282
+ }
283
+ if (parsed.protocol !== baseParsed.protocol) {
284
+ console.warn(
285
+ `Resource URL protocol mismatch: ${parsed.protocol} vs ${baseParsed.protocol}. Using safe default instead.`
286
+ );
287
+ return `${baseUrl}/v1/chat/completions`;
288
+ }
289
+ return url;
290
+ } catch {
291
+ console.warn(`Invalid resource URL format: ${url}. Using safe default.`);
292
+ return `${baseUrl}/v1/chat/completions`;
293
+ }
294
+ }
295
+
296
+ // src/client.ts
297
+ var DEFAULT_API_URL = "https://blockrun.ai/api";
298
+ var TESTNET_API_URL = "https://testnet.blockrun.ai/api";
299
+ var DEFAULT_MAX_TOKENS = 1024;
300
+ var DEFAULT_TIMEOUT = 6e4;
301
+ var SDK_VERSION = "0.3.0";
302
+ var USER_AGENT = `blockrun-ts/${SDK_VERSION}`;
303
+ var LLMClient = class {
304
+ static DEFAULT_API_URL = DEFAULT_API_URL;
305
+ static TESTNET_API_URL = TESTNET_API_URL;
306
+ account;
307
+ privateKey;
308
+ apiUrl;
309
+ timeout;
310
+ sessionTotalUsd = 0;
311
+ sessionCalls = 0;
312
+ modelPricingCache = null;
313
+ modelPricingPromise = null;
314
+ /**
315
+ * Initialize the BlockRun LLM client.
316
+ *
317
+ * @param options - Client configuration options (optional if BASE_CHAIN_WALLET_KEY env var is set)
318
+ */
319
+ constructor(options = {}) {
320
+ const envKey = typeof process !== "undefined" && process.env ? process.env.BASE_CHAIN_WALLET_KEY : void 0;
321
+ const privateKey = options.privateKey || envKey;
322
+ if (!privateKey) {
323
+ throw new Error(
324
+ "Private key required. Pass privateKey in options or set BASE_CHAIN_WALLET_KEY environment variable."
325
+ );
326
+ }
327
+ validatePrivateKey(privateKey);
328
+ this.privateKey = privateKey;
329
+ this.account = privateKeyToAccount(privateKey);
330
+ const apiUrl = options.apiUrl || DEFAULT_API_URL;
331
+ validateApiUrl(apiUrl);
332
+ this.apiUrl = apiUrl.replace(/\/$/, "");
333
+ this.timeout = options.timeout || DEFAULT_TIMEOUT;
334
+ }
335
+ /**
336
+ * Simple 1-line chat interface.
337
+ *
338
+ * @param model - Model ID (e.g., 'openai/gpt-4o', 'anthropic/claude-sonnet-4')
339
+ * @param prompt - User message
340
+ * @param options - Optional chat parameters
341
+ * @returns Assistant's response text
342
+ *
343
+ * @example
344
+ * const response = await client.chat('gpt-4o', 'What is the capital of France?');
345
+ * console.log(response); // 'The capital of France is Paris.'
346
+ */
347
+ async chat(model, prompt, options) {
348
+ const messages = [];
349
+ if (options?.system) {
350
+ messages.push({ role: "system", content: options.system });
351
+ }
352
+ messages.push({ role: "user", content: prompt });
353
+ const result = await this.chatCompletion(model, messages, {
354
+ maxTokens: options?.maxTokens,
355
+ temperature: options?.temperature,
356
+ topP: options?.topP,
357
+ search: options?.search,
358
+ searchParameters: options?.searchParameters
359
+ });
360
+ return result.choices[0].message.content || "";
361
+ }
362
+ /**
363
+ * Smart chat with automatic model routing.
364
+ *
365
+ * Uses ClawRouter's 14-dimension rule-based scoring algorithm (<1ms, 100% local)
366
+ * to select the cheapest model that can handle your request.
367
+ *
368
+ * @param prompt - User message
369
+ * @param options - Optional chat and routing parameters
370
+ * @returns SmartChatResponse with response text, selected model, and routing metadata
371
+ *
372
+ * @example Simple usage (auto profile)
373
+ * ```ts
374
+ * const result = await client.smartChat('What is 2+2?');
375
+ * console.log(result.response); // '4'
376
+ * console.log(result.model); // 'google/gemini-2.5-flash-lite'
377
+ * console.log(result.routing.savings); // 0.78 (78% savings)
378
+ * ```
379
+ *
380
+ * @example With routing profile
381
+ * ```ts
382
+ * // Free tier only (zero cost)
383
+ * const result = await client.smartChat('Hello!', { routingProfile: 'free' });
384
+ *
385
+ * // Eco mode (budget optimized)
386
+ * const result = await client.smartChat('Explain quantum computing', { routingProfile: 'eco' });
387
+ *
388
+ * // Premium mode (best quality)
389
+ * const result = await client.smartChat('Write a business plan', { routingProfile: 'premium' });
390
+ * ```
391
+ */
392
+ async smartChat(prompt, options) {
393
+ const modelPricing = await this.getModelPricing();
394
+ const maxOutputTokens = options?.maxOutputTokens || options?.maxTokens || 1024;
395
+ const decision = route(prompt, options?.system, maxOutputTokens, {
396
+ config: DEFAULT_ROUTING_CONFIG,
397
+ modelPricing,
398
+ routingProfile: options?.routingProfile
399
+ });
400
+ const response = await this.chat(decision.model, prompt, {
401
+ system: options?.system,
402
+ maxTokens: options?.maxTokens,
403
+ temperature: options?.temperature,
404
+ topP: options?.topP,
405
+ search: options?.search,
406
+ searchParameters: options?.searchParameters
407
+ });
408
+ return {
409
+ response,
410
+ model: decision.model,
411
+ routing: decision
412
+ };
413
+ }
414
+ /**
415
+ * Get model pricing map (cached).
416
+ * Fetches from API on first call, then returns cached result.
417
+ */
418
+ async getModelPricing() {
419
+ if (this.modelPricingCache) {
420
+ return this.modelPricingCache;
421
+ }
422
+ if (this.modelPricingPromise) {
423
+ return this.modelPricingPromise;
424
+ }
425
+ this.modelPricingPromise = this.fetchModelPricing();
426
+ try {
427
+ this.modelPricingCache = await this.modelPricingPromise;
428
+ return this.modelPricingCache;
429
+ } finally {
430
+ this.modelPricingPromise = null;
431
+ }
432
+ }
433
+ /**
434
+ * Fetch model pricing from API.
435
+ */
436
+ async fetchModelPricing() {
437
+ const models = await this.listModels();
438
+ const pricing = /* @__PURE__ */ new Map();
439
+ for (const model of models) {
440
+ pricing.set(model.id, {
441
+ inputPrice: model.inputPrice,
442
+ outputPrice: model.outputPrice
443
+ });
444
+ }
445
+ return pricing;
446
+ }
447
+ /**
448
+ * Full chat completion interface (OpenAI-compatible).
449
+ *
450
+ * @param model - Model ID
451
+ * @param messages - Array of messages with role and content
452
+ * @param options - Optional completion parameters
453
+ * @returns ChatResponse object with choices and usage
454
+ */
455
+ async chatCompletion(model, messages, options) {
456
+ const body = {
457
+ model,
458
+ messages,
459
+ max_tokens: options?.maxTokens || DEFAULT_MAX_TOKENS
460
+ };
461
+ if (options?.temperature !== void 0) {
462
+ body.temperature = options.temperature;
463
+ }
464
+ if (options?.topP !== void 0) {
465
+ body.top_p = options.topP;
466
+ }
467
+ if (options?.searchParameters !== void 0) {
468
+ body.search_parameters = options.searchParameters;
469
+ } else if (options?.search === true) {
470
+ body.search_parameters = { mode: "on" };
471
+ }
472
+ if (options?.tools !== void 0) {
473
+ body.tools = options.tools;
474
+ }
475
+ if (options?.toolChoice !== void 0) {
476
+ body.tool_choice = options.toolChoice;
477
+ }
478
+ return this.requestWithPayment("/v1/chat/completions", body);
479
+ }
480
+ /**
481
+ * Make a request with automatic x402 payment handling.
482
+ */
483
+ async requestWithPayment(endpoint, body) {
484
+ const url = `${this.apiUrl}${endpoint}`;
485
+ const response = await this.fetchWithTimeout(url, {
486
+ method: "POST",
487
+ headers: { "Content-Type": "application/json", "User-Agent": USER_AGENT },
488
+ body: JSON.stringify(body)
489
+ });
490
+ if (response.status === 402) {
491
+ return this.handlePaymentAndRetry(url, body, response);
492
+ }
493
+ if (!response.ok) {
494
+ let errorBody;
495
+ try {
496
+ errorBody = await response.json();
497
+ } catch {
498
+ errorBody = { error: "Request failed" };
499
+ }
500
+ throw new APIError(
501
+ `API error: ${response.status}`,
502
+ response.status,
503
+ sanitizeErrorResponse(errorBody)
504
+ );
505
+ }
506
+ return response.json();
507
+ }
508
+ /**
509
+ * Handle 402 response: parse requirements, sign payment, retry.
510
+ */
511
+ async handlePaymentAndRetry(url, body, response) {
512
+ let paymentHeader = response.headers.get("payment-required");
513
+ if (!paymentHeader) {
514
+ try {
515
+ const respBody = await response.json();
516
+ if (respBody.x402 || respBody.accepts) {
517
+ paymentHeader = btoa(JSON.stringify(respBody));
518
+ }
519
+ } catch (parseError) {
520
+ console.debug("Failed to parse payment header from response body", parseError);
521
+ }
522
+ }
523
+ if (!paymentHeader) {
524
+ throw new PaymentError("402 response but no payment requirements found");
525
+ }
526
+ const paymentRequired = parsePaymentRequired(paymentHeader);
527
+ const details = extractPaymentDetails(paymentRequired);
528
+ const extensions = paymentRequired.extensions;
529
+ const paymentPayload = await createPaymentPayload(
530
+ this.privateKey,
531
+ this.account.address,
532
+ details.recipient,
533
+ details.amount,
534
+ details.network || "eip155:8453",
535
+ {
536
+ resourceUrl: validateResourceUrl(
537
+ details.resource?.url || `${this.apiUrl}/v1/chat/completions`,
538
+ this.apiUrl
539
+ ),
540
+ resourceDescription: details.resource?.description || "BlockRun AI API call",
541
+ maxTimeoutSeconds: details.maxTimeoutSeconds || 300,
542
+ extra: details.extra,
543
+ extensions
544
+ }
545
+ );
546
+ const retryResponse = await this.fetchWithTimeout(url, {
547
+ method: "POST",
548
+ headers: {
549
+ "Content-Type": "application/json",
550
+ "User-Agent": USER_AGENT,
551
+ "PAYMENT-SIGNATURE": paymentPayload
552
+ },
553
+ body: JSON.stringify(body)
554
+ });
555
+ if (retryResponse.status === 402) {
556
+ throw new PaymentError("Payment was rejected. Check your wallet balance.");
557
+ }
558
+ if (!retryResponse.ok) {
559
+ let errorBody;
560
+ try {
561
+ errorBody = await retryResponse.json();
562
+ } catch {
563
+ errorBody = { error: "Request failed" };
564
+ }
565
+ throw new APIError(
566
+ `API error after payment: ${retryResponse.status}`,
567
+ retryResponse.status,
568
+ sanitizeErrorResponse(errorBody)
569
+ );
570
+ }
571
+ const costUsd = parseFloat(details.amount) / 1e6;
572
+ this.sessionCalls += 1;
573
+ this.sessionTotalUsd += costUsd;
574
+ return retryResponse.json();
575
+ }
576
+ /**
577
+ * Make a request with automatic x402 payment handling, returning raw JSON.
578
+ * Used for non-ChatResponse endpoints (X/Twitter, search, image edit, etc.).
579
+ */
580
+ async requestWithPaymentRaw(endpoint, body) {
581
+ const url = `${this.apiUrl}${endpoint}`;
582
+ const response = await this.fetchWithTimeout(url, {
583
+ method: "POST",
584
+ headers: { "Content-Type": "application/json", "User-Agent": USER_AGENT },
585
+ body: JSON.stringify(body)
586
+ });
587
+ if (response.status === 402) {
588
+ return this.handlePaymentAndRetryRaw(url, body, response);
589
+ }
590
+ if (!response.ok) {
591
+ let errorBody;
592
+ try {
593
+ errorBody = await response.json();
594
+ } catch {
595
+ errorBody = { error: "Request failed" };
596
+ }
597
+ throw new APIError(
598
+ `API error: ${response.status}`,
599
+ response.status,
600
+ sanitizeErrorResponse(errorBody)
601
+ );
602
+ }
603
+ return response.json();
604
+ }
605
+ /**
606
+ * Handle 402 response for raw endpoints: parse requirements, sign payment, retry.
607
+ */
608
+ async handlePaymentAndRetryRaw(url, body, response) {
609
+ let paymentHeader = response.headers.get("payment-required");
610
+ if (!paymentHeader) {
611
+ try {
612
+ const respBody = await response.json();
613
+ if (respBody.x402 || respBody.accepts) {
614
+ paymentHeader = btoa(JSON.stringify(respBody));
615
+ }
616
+ } catch {
617
+ console.debug("Failed to parse payment header from response body");
618
+ }
619
+ }
620
+ if (!paymentHeader) {
621
+ throw new PaymentError("402 response but no payment requirements found");
622
+ }
623
+ const paymentRequired = parsePaymentRequired(paymentHeader);
624
+ const details = extractPaymentDetails(paymentRequired);
625
+ const extensions = paymentRequired.extensions;
626
+ const paymentPayload = await createPaymentPayload(
627
+ this.privateKey,
628
+ this.account.address,
629
+ details.recipient,
630
+ details.amount,
631
+ details.network || "eip155:8453",
632
+ {
633
+ resourceUrl: validateResourceUrl(
634
+ details.resource?.url || url,
635
+ this.apiUrl
636
+ ),
637
+ resourceDescription: details.resource?.description || "BlockRun AI API call",
638
+ maxTimeoutSeconds: details.maxTimeoutSeconds || 300,
639
+ extra: details.extra,
640
+ extensions
641
+ }
642
+ );
643
+ const retryResponse = await this.fetchWithTimeout(url, {
644
+ method: "POST",
645
+ headers: {
646
+ "Content-Type": "application/json",
647
+ "User-Agent": USER_AGENT,
648
+ "PAYMENT-SIGNATURE": paymentPayload
649
+ },
650
+ body: JSON.stringify(body)
651
+ });
652
+ if (retryResponse.status === 402) {
653
+ throw new PaymentError("Payment was rejected. Check your wallet balance.");
654
+ }
655
+ if (!retryResponse.ok) {
656
+ let errorBody;
657
+ try {
658
+ errorBody = await retryResponse.json();
659
+ } catch {
660
+ errorBody = { error: "Request failed" };
661
+ }
662
+ throw new APIError(
663
+ `API error after payment: ${retryResponse.status}`,
664
+ retryResponse.status,
665
+ sanitizeErrorResponse(errorBody)
666
+ );
667
+ }
668
+ const costUsd = parseFloat(details.amount) / 1e6;
669
+ this.sessionCalls += 1;
670
+ this.sessionTotalUsd += costUsd;
671
+ return retryResponse.json();
672
+ }
673
+ /**
674
+ * GET with automatic x402 payment handling, returning raw JSON.
675
+ * Used for Predexon prediction market endpoints that use GET + query params.
676
+ */
677
+ async getWithPaymentRaw(endpoint, params) {
678
+ const query = params ? "?" + new URLSearchParams(params).toString() : "";
679
+ const url = `${this.apiUrl}${endpoint}${query}`;
680
+ const response = await this.fetchWithTimeout(url, {
681
+ method: "GET",
682
+ headers: { "User-Agent": USER_AGENT }
683
+ });
684
+ if (response.status === 402) {
685
+ return this.handleGetPaymentAndRetryRaw(url, endpoint, params, response);
686
+ }
687
+ if (!response.ok) {
688
+ let errorBody;
689
+ try {
690
+ errorBody = await response.json();
691
+ } catch {
692
+ errorBody = { error: "Request failed" };
693
+ }
694
+ throw new APIError(
695
+ `API error: ${response.status}`,
696
+ response.status,
697
+ sanitizeErrorResponse(errorBody)
698
+ );
699
+ }
700
+ return response.json();
701
+ }
702
+ /**
703
+ * Handle 402 response for GET endpoints: parse requirements, sign payment, retry with GET.
704
+ */
705
+ async handleGetPaymentAndRetryRaw(url, endpoint, params, response) {
706
+ let paymentHeader = response.headers.get("payment-required");
707
+ if (!paymentHeader) {
708
+ try {
709
+ const respBody = await response.json();
710
+ if (respBody.x402 || respBody.accepts) {
711
+ paymentHeader = btoa(JSON.stringify(respBody));
712
+ }
713
+ } catch {
714
+ console.debug("Failed to parse payment header from response body");
715
+ }
716
+ }
717
+ if (!paymentHeader) {
718
+ throw new PaymentError("402 response but no payment requirements found");
719
+ }
720
+ const paymentRequired = parsePaymentRequired(paymentHeader);
721
+ const details = extractPaymentDetails(paymentRequired);
722
+ const extensions = paymentRequired.extensions;
723
+ const paymentPayload = await createPaymentPayload(
724
+ this.privateKey,
725
+ this.account.address,
726
+ details.recipient,
727
+ details.amount,
728
+ details.network || "eip155:8453",
729
+ {
730
+ resourceUrl: validateResourceUrl(
731
+ details.resource?.url || url,
732
+ this.apiUrl
733
+ ),
734
+ resourceDescription: details.resource?.description || "BlockRun AI API call",
735
+ maxTimeoutSeconds: details.maxTimeoutSeconds || 300,
736
+ extra: details.extra,
737
+ extensions
738
+ }
739
+ );
740
+ const query = params ? "?" + new URLSearchParams(params).toString() : "";
741
+ const retryUrl = `${this.apiUrl}${endpoint}${query}`;
742
+ const retryResponse = await this.fetchWithTimeout(retryUrl, {
743
+ method: "GET",
744
+ headers: {
745
+ "User-Agent": USER_AGENT,
746
+ "PAYMENT-SIGNATURE": paymentPayload
747
+ }
748
+ });
749
+ if (retryResponse.status === 402) {
750
+ throw new PaymentError("Payment was rejected. Check your wallet balance.");
751
+ }
752
+ if (!retryResponse.ok) {
753
+ let errorBody;
754
+ try {
755
+ errorBody = await retryResponse.json();
756
+ } catch {
757
+ errorBody = { error: "Request failed" };
758
+ }
759
+ throw new APIError(
760
+ `API error after payment: ${retryResponse.status}`,
761
+ retryResponse.status,
762
+ sanitizeErrorResponse(errorBody)
763
+ );
764
+ }
765
+ const costUsd = parseFloat(details.amount) / 1e6;
766
+ this.sessionCalls += 1;
767
+ this.sessionTotalUsd += costUsd;
768
+ return retryResponse.json();
769
+ }
770
+ /**
771
+ * Fetch with timeout.
772
+ */
773
+ async fetchWithTimeout(url, options) {
774
+ const controller = new AbortController();
775
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
776
+ try {
777
+ const response = await fetch(url, {
778
+ ...options,
779
+ signal: controller.signal
780
+ });
781
+ return response;
782
+ } finally {
783
+ clearTimeout(timeoutId);
784
+ }
785
+ }
786
+ /**
787
+ * List available LLM models with pricing.
788
+ */
789
+ async listModels() {
790
+ const response = await this.fetchWithTimeout(`${this.apiUrl}/v1/models`, {
791
+ method: "GET"
792
+ });
793
+ if (!response.ok) {
794
+ let errorBody;
795
+ try {
796
+ errorBody = await response.json();
797
+ } catch {
798
+ errorBody = { error: "Request failed" };
799
+ }
800
+ throw new APIError(
801
+ `Failed to list models: ${response.status}`,
802
+ response.status,
803
+ sanitizeErrorResponse(errorBody)
804
+ );
805
+ }
806
+ const data = await response.json();
807
+ return data.data || [];
808
+ }
809
+ /**
810
+ * List available image generation models with pricing.
811
+ */
812
+ async listImageModels() {
813
+ const response = await this.fetchWithTimeout(
814
+ `${this.apiUrl}/v1/images/models`,
815
+ { method: "GET" }
816
+ );
817
+ if (!response.ok) {
818
+ throw new APIError(
819
+ `Failed to list image models: ${response.status}`,
820
+ response.status
821
+ );
822
+ }
823
+ const data = await response.json();
824
+ return data.data || [];
825
+ }
826
+ /**
827
+ * List all available models (both LLM and image) with pricing.
828
+ *
829
+ * @returns Array of all models with 'type' field ('llm' or 'image')
830
+ *
831
+ * @example
832
+ * const models = await client.listAllModels();
833
+ * for (const model of models) {
834
+ * if (model.type === 'llm') {
835
+ * console.log(`LLM: ${model.id} - $${model.inputPrice}/M input`);
836
+ * } else {
837
+ * console.log(`Image: ${model.id} - $${model.pricePerImage}/image`);
838
+ * }
839
+ * }
840
+ */
841
+ async listAllModels() {
842
+ const llmModels = await this.listModels();
843
+ for (const model of llmModels) {
844
+ model.type = "llm";
845
+ }
846
+ const imageModels = await this.listImageModels();
847
+ for (const model of imageModels) {
848
+ model.type = "image";
849
+ }
850
+ return [...llmModels, ...imageModels];
851
+ }
852
+ /**
853
+ * Edit an image using img2img.
854
+ *
855
+ * @param prompt - Text description of the desired edit
856
+ * @param image - Base64-encoded image or URL of the source image
857
+ * @param options - Optional edit parameters
858
+ * @returns ImageResponse with edited image URLs
859
+ */
860
+ async imageEdit(prompt, image, options) {
861
+ const body = {
862
+ model: options?.model || "openai/gpt-image-1",
863
+ prompt,
864
+ image,
865
+ size: options?.size || "1024x1024",
866
+ n: options?.n || 1
867
+ };
868
+ if (options?.mask !== void 0) {
869
+ body.mask = options.mask;
870
+ }
871
+ const data = await this.requestWithPaymentRaw("/v1/images/image2image", body);
872
+ return data;
873
+ }
874
+ /**
875
+ * Standalone search (web, X/Twitter, news).
876
+ *
877
+ * @param query - Search query
878
+ * @param options - Optional search parameters
879
+ * @returns SearchResult with summary and citations
880
+ */
881
+ async search(query, options) {
882
+ const body = {
883
+ query,
884
+ max_results: options?.maxResults || 10
885
+ };
886
+ if (options?.sources !== void 0) body.sources = options.sources;
887
+ if (options?.fromDate !== void 0) body.from_date = options.fromDate;
888
+ if (options?.toDate !== void 0) body.to_date = options.toDate;
889
+ const data = await this.requestWithPaymentRaw("/v1/search", body);
890
+ return data;
891
+ }
892
+ /**
893
+ * Get USDC balance on Base network.
894
+ *
895
+ * Automatically detects mainnet vs testnet based on API URL.
896
+ *
897
+ * @returns USDC balance as a float (6 decimal places normalized)
898
+ *
899
+ * @example
900
+ * const balance = await client.getBalance();
901
+ * console.log(`Balance: $${balance.toFixed(2)} USDC`);
902
+ */
903
+ async getBalance() {
904
+ const isTestnet = this.isTestnet();
905
+ const usdcContract = isTestnet ? "0x036CbD53842c5426634e7929541eC2318f3dCF7e" : "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
906
+ const rpcs = isTestnet ? ["https://sepolia.base.org", "https://base-sepolia-rpc.publicnode.com"] : ["https://base.publicnode.com", "https://mainnet.base.org", "https://base.meowrpc.com"];
907
+ const selector = "0x70a08231";
908
+ const paddedAddress = this.account.address.slice(2).toLowerCase().padStart(64, "0");
909
+ const data = selector + paddedAddress;
910
+ const payload = {
911
+ jsonrpc: "2.0",
912
+ method: "eth_call",
913
+ params: [{ to: usdcContract, data }, "latest"],
914
+ id: 1
915
+ };
916
+ let lastError;
917
+ for (const rpc of rpcs) {
918
+ try {
919
+ const response = await fetch(rpc, {
920
+ method: "POST",
921
+ headers: { "Content-Type": "application/json" },
922
+ body: JSON.stringify(payload)
923
+ });
924
+ const result = await response.json();
925
+ const balanceRaw = parseInt(result.result || "0x0", 16);
926
+ return balanceRaw / 1e6;
927
+ } catch (e) {
928
+ lastError = e;
929
+ }
930
+ }
931
+ throw lastError || new Error("All RPCs failed");
932
+ }
933
+ // ============================================================
934
+ // X/Twitter endpoints (powered by AttentionVC)
935
+ // ============================================================
936
+ /**
937
+ * Look up X/Twitter user profiles by username.
938
+ *
939
+ * Powered by AttentionVC. $0.002 per user (min $0.02, max $0.20).
940
+ *
941
+ * @param usernames - Single username or array of usernames (without @)
942
+ */
943
+ async xUserLookup(usernames) {
944
+ const names = Array.isArray(usernames) ? usernames : [usernames];
945
+ const data = await this.requestWithPaymentRaw("/v1/x/users/lookup", { usernames: names });
946
+ return data;
947
+ }
948
+ /**
949
+ * Get followers of an X/Twitter user.
950
+ *
951
+ * Powered by AttentionVC. $0.05 per page (~200 accounts).
952
+ *
953
+ * @param username - X/Twitter username (without @)
954
+ * @param cursor - Pagination cursor from previous response
955
+ */
956
+ async xFollowers(username, cursor) {
957
+ const body = { username };
958
+ if (cursor !== void 0) body.cursor = cursor;
959
+ const data = await this.requestWithPaymentRaw("/v1/x/users/followers", body);
960
+ return data;
961
+ }
962
+ /**
963
+ * Get accounts an X/Twitter user is following.
964
+ *
965
+ * Powered by AttentionVC. $0.05 per page (~200 accounts).
966
+ *
967
+ * @param username - X/Twitter username (without @)
968
+ * @param cursor - Pagination cursor from previous response
969
+ */
970
+ async xFollowings(username, cursor) {
971
+ const body = { username };
972
+ if (cursor !== void 0) body.cursor = cursor;
973
+ const data = await this.requestWithPaymentRaw("/v1/x/users/followings", body);
974
+ return data;
975
+ }
976
+ /**
977
+ * Get detailed profile info for a single X/Twitter user.
978
+ *
979
+ * Powered by AttentionVC. $0.002 per request.
980
+ *
981
+ * @param username - X/Twitter username (without @)
982
+ */
983
+ async xUserInfo(username) {
984
+ const data = await this.requestWithPaymentRaw("/v1/x/users/info", { username });
985
+ return data;
986
+ }
987
+ /**
988
+ * Get verified (blue-check) followers of an X/Twitter user.
989
+ *
990
+ * Powered by AttentionVC. $0.048 per page.
991
+ *
992
+ * @param userId - X/Twitter user ID (not username)
993
+ * @param cursor - Pagination cursor from previous response
994
+ */
995
+ async xVerifiedFollowers(userId, cursor) {
996
+ const body = { userId };
997
+ if (cursor !== void 0) body.cursor = cursor;
998
+ const data = await this.requestWithPaymentRaw("/v1/x/users/verified-followers", body);
999
+ return data;
1000
+ }
1001
+ /**
1002
+ * Get tweets posted by an X/Twitter user.
1003
+ *
1004
+ * Powered by AttentionVC. $0.032 per page.
1005
+ *
1006
+ * @param username - X/Twitter username (without @)
1007
+ * @param includeReplies - Include reply tweets (default: false)
1008
+ * @param cursor - Pagination cursor from previous response
1009
+ */
1010
+ async xUserTweets(username, includeReplies = false, cursor) {
1011
+ const body = { username, includeReplies };
1012
+ if (cursor !== void 0) body.cursor = cursor;
1013
+ const data = await this.requestWithPaymentRaw("/v1/x/users/tweets", body);
1014
+ return data;
1015
+ }
1016
+ /**
1017
+ * Get tweets that mention an X/Twitter user.
1018
+ *
1019
+ * Powered by AttentionVC. $0.032 per page.
1020
+ *
1021
+ * @param username - X/Twitter username (without @)
1022
+ * @param sinceTime - Start time filter (ISO8601 or Unix timestamp)
1023
+ * @param untilTime - End time filter (ISO8601 or Unix timestamp)
1024
+ * @param cursor - Pagination cursor from previous response
1025
+ */
1026
+ async xUserMentions(username, sinceTime, untilTime, cursor) {
1027
+ const body = { username };
1028
+ if (sinceTime !== void 0) body.sinceTime = sinceTime;
1029
+ if (untilTime !== void 0) body.untilTime = untilTime;
1030
+ if (cursor !== void 0) body.cursor = cursor;
1031
+ const data = await this.requestWithPaymentRaw("/v1/x/users/mentions", body);
1032
+ return data;
1033
+ }
1034
+ /**
1035
+ * Fetch full tweet data for up to 200 tweet IDs.
1036
+ *
1037
+ * Powered by AttentionVC. $0.16 per batch.
1038
+ *
1039
+ * @param tweetIds - Single tweet ID or array of tweet IDs (max 200)
1040
+ */
1041
+ async xTweetLookup(tweetIds) {
1042
+ const ids = Array.isArray(tweetIds) ? tweetIds : [tweetIds];
1043
+ const data = await this.requestWithPaymentRaw("/v1/x/tweets/lookup", { tweet_ids: ids });
1044
+ return data;
1045
+ }
1046
+ /**
1047
+ * Get replies to a specific tweet.
1048
+ *
1049
+ * Powered by AttentionVC. $0.032 per page.
1050
+ *
1051
+ * @param tweetId - The tweet ID to get replies for
1052
+ * @param queryType - Sort order: 'Latest' or 'Default'
1053
+ * @param cursor - Pagination cursor from previous response
1054
+ */
1055
+ async xTweetReplies(tweetId, queryType = "Latest", cursor) {
1056
+ const body = { tweetId, queryType };
1057
+ if (cursor !== void 0) body.cursor = cursor;
1058
+ const data = await this.requestWithPaymentRaw("/v1/x/tweets/replies", body);
1059
+ return data;
1060
+ }
1061
+ /**
1062
+ * Get the full thread context for a tweet.
1063
+ *
1064
+ * Powered by AttentionVC. $0.032 per page.
1065
+ *
1066
+ * @param tweetId - The tweet ID to get thread for
1067
+ * @param cursor - Pagination cursor from previous response
1068
+ */
1069
+ async xTweetThread(tweetId, cursor) {
1070
+ const body = { tweetId };
1071
+ if (cursor !== void 0) body.cursor = cursor;
1072
+ const data = await this.requestWithPaymentRaw("/v1/x/tweets/thread", body);
1073
+ return data;
1074
+ }
1075
+ /**
1076
+ * Search X/Twitter with advanced query operators.
1077
+ *
1078
+ * Powered by AttentionVC. $0.032 per page.
1079
+ *
1080
+ * @param query - Search query (supports Twitter search operators)
1081
+ * @param queryType - Sort order: 'Latest', 'Top', or 'Default'
1082
+ * @param cursor - Pagination cursor from previous response
1083
+ */
1084
+ async xSearch(query, queryType = "Latest", cursor) {
1085
+ const body = { query, queryType };
1086
+ if (cursor !== void 0) body.cursor = cursor;
1087
+ const data = await this.requestWithPaymentRaw("/v1/x/search", body);
1088
+ return data;
1089
+ }
1090
+ /**
1091
+ * Get current trending topics on X/Twitter.
1092
+ *
1093
+ * Powered by AttentionVC. $0.002 per request.
1094
+ */
1095
+ async xTrending() {
1096
+ const data = await this.requestWithPaymentRaw("/v1/x/trending", {});
1097
+ return data;
1098
+ }
1099
+ /**
1100
+ * Get rising/viral articles from X/Twitter.
1101
+ *
1102
+ * Powered by AttentionVC intelligence layer. $0.05 per request.
1103
+ */
1104
+ async xArticlesRising() {
1105
+ const data = await this.requestWithPaymentRaw("/v1/x/articles/rising", {});
1106
+ return data;
1107
+ }
1108
+ /**
1109
+ * Get author analytics and intelligence metrics for an X/Twitter user.
1110
+ *
1111
+ * Powered by AttentionVC intelligence layer. $0.02 per request.
1112
+ *
1113
+ * @param handle - X/Twitter handle (without @)
1114
+ */
1115
+ async xAuthorAnalytics(handle) {
1116
+ const data = await this.requestWithPaymentRaw("/v1/x/authors", { handle });
1117
+ return data;
1118
+ }
1119
+ /**
1120
+ * Compare two X/Twitter authors side-by-side with intelligence metrics.
1121
+ *
1122
+ * Powered by AttentionVC intelligence layer. $0.05 per request.
1123
+ *
1124
+ * @param handle1 - First X/Twitter handle (without @)
1125
+ * @param handle2 - Second X/Twitter handle (without @)
1126
+ */
1127
+ async xCompareAuthors(handle1, handle2) {
1128
+ const data = await this.requestWithPaymentRaw("/v1/x/compare", { handle1, handle2 });
1129
+ return data;
1130
+ }
1131
+ // ── Prediction Markets (Powered by Predexon) ────────────────────────────
1132
+ /**
1133
+ * Query Predexon prediction market data (GET endpoints).
1134
+ *
1135
+ * Access real-time data from Polymarket, Kalshi, dFlow, and Binance Futures.
1136
+ * Powered by Predexon. $0.001 per request.
1137
+ *
1138
+ * @param path - Endpoint path, e.g. "polymarket/events", "kalshi/markets/12345"
1139
+ * @param params - Query parameters passed to the endpoint
1140
+ *
1141
+ * @example
1142
+ * const events = await client.pm("polymarket/events");
1143
+ * const market = await client.pm("kalshi/markets/KXBTC-25MAR14");
1144
+ * const results = await client.pm("polymarket/search", { q: "bitcoin" });
1145
+ */
1146
+ async pm(path5, params) {
1147
+ return this.getWithPaymentRaw(`/v1/pm/${path5}`, params);
1148
+ }
1149
+ /**
1150
+ * Structured query for Predexon prediction market data (POST endpoints).
1151
+ *
1152
+ * For complex queries that require a JSON body. $0.005 per request.
1153
+ *
1154
+ * @param path - Endpoint path, e.g. "polymarket/query", "kalshi/query"
1155
+ * @param query - JSON body for the structured query
1156
+ *
1157
+ * @example
1158
+ * const data = await client.pmQuery("polymarket/query", { filter: "active", limit: 10 });
1159
+ */
1160
+ async pmQuery(path5, query) {
1161
+ return this.requestWithPaymentRaw(`/v1/pm/${path5}`, query);
1162
+ }
1163
+ /**
1164
+ * Get current session spending.
1165
+ *
1166
+ * @returns Object with totalUsd and calls count
1167
+ *
1168
+ * @example
1169
+ * const spending = client.getSpending();
1170
+ * console.log(`Spent $${spending.totalUsd.toFixed(4)} across ${spending.calls} calls`);
1171
+ */
1172
+ getSpending() {
1173
+ return {
1174
+ totalUsd: this.sessionTotalUsd,
1175
+ calls: this.sessionCalls
1176
+ };
1177
+ }
1178
+ /**
1179
+ * Get the wallet address being used for payments.
1180
+ */
1181
+ getWalletAddress() {
1182
+ return this.account.address;
1183
+ }
1184
+ /**
1185
+ * Check if client is configured for testnet.
1186
+ */
1187
+ isTestnet() {
1188
+ return this.apiUrl.includes("testnet.blockrun.ai");
1189
+ }
1190
+ };
1191
+ function testnetClient(options = {}) {
1192
+ return new LLMClient({
1193
+ ...options,
1194
+ apiUrl: TESTNET_API_URL
1195
+ });
1196
+ }
1197
+ var client_default = LLMClient;
1198
+
1199
+ // src/image.ts
1200
+ import { privateKeyToAccount as privateKeyToAccount2 } from "viem/accounts";
1201
+ var DEFAULT_API_URL2 = "https://blockrun.ai/api";
1202
+ var DEFAULT_MODEL = "google/nano-banana";
1203
+ var DEFAULT_SIZE = "1024x1024";
1204
+ var DEFAULT_TIMEOUT2 = 12e4;
1205
+ var ImageClient = class {
1206
+ account;
1207
+ privateKey;
1208
+ apiUrl;
1209
+ timeout;
1210
+ sessionTotalUsd = 0;
1211
+ sessionCalls = 0;
1212
+ /**
1213
+ * Initialize the BlockRun Image client.
1214
+ *
1215
+ * @param options - Client configuration options
1216
+ */
1217
+ constructor(options = {}) {
1218
+ const envKey = typeof process !== "undefined" && process.env ? process.env.BLOCKRUN_WALLET_KEY || process.env.BASE_CHAIN_WALLET_KEY : void 0;
1219
+ const privateKey = options.privateKey || envKey;
1220
+ if (!privateKey) {
1221
+ throw new Error(
1222
+ "Private key required. Pass privateKey in options or set BLOCKRUN_WALLET_KEY environment variable."
1223
+ );
1224
+ }
1225
+ validatePrivateKey(privateKey);
1226
+ this.privateKey = privateKey;
1227
+ this.account = privateKeyToAccount2(privateKey);
1228
+ const apiUrl = options.apiUrl || DEFAULT_API_URL2;
1229
+ validateApiUrl(apiUrl);
1230
+ this.apiUrl = apiUrl.replace(/\/$/, "");
1231
+ this.timeout = options.timeout || DEFAULT_TIMEOUT2;
1232
+ }
1233
+ /**
1234
+ * Generate an image from a text prompt.
1235
+ *
1236
+ * @param prompt - Text description of the image to generate
1237
+ * @param options - Optional generation parameters
1238
+ * @returns ImageResponse with generated image URLs
1239
+ *
1240
+ * @example
1241
+ * const result = await client.generate('A sunset over mountains');
1242
+ * console.log(result.data[0].url);
1243
+ */
1244
+ async generate(prompt, options) {
1245
+ const body = {
1246
+ model: options?.model || DEFAULT_MODEL,
1247
+ prompt,
1248
+ size: options?.size || DEFAULT_SIZE,
1249
+ n: options?.n || 1
1250
+ };
1251
+ if (options?.quality) {
1252
+ body.quality = options.quality;
1253
+ }
1254
+ return this.requestWithPayment("/v1/images/generations", body);
1255
+ }
1256
+ /**
1257
+ * Edit an image using img2img.
1258
+ *
1259
+ * @param prompt - Text description of the desired edit
1260
+ * @param image - Base64-encoded image or URL of the source image
1261
+ * @param options - Optional edit parameters
1262
+ * @returns ImageResponse with edited image URLs
1263
+ *
1264
+ * @example
1265
+ * const result = await client.edit('Make it a painting', imageBase64);
1266
+ * console.log(result.data[0].url);
1267
+ */
1268
+ async edit(prompt, image, options) {
1269
+ const body = {
1270
+ model: options?.model || "openai/gpt-image-1",
1271
+ prompt,
1272
+ image,
1273
+ size: options?.size || "1024x1024",
1274
+ n: options?.n || 1
1275
+ };
1276
+ if (options?.mask !== void 0) {
1277
+ body.mask = options.mask;
1278
+ }
1279
+ return this.requestWithPayment("/v1/images/image2image", body);
1280
+ }
1281
+ /**
1282
+ * List available image generation models with pricing.
1283
+ */
1284
+ async listImageModels() {
1285
+ const response = await this.fetchWithTimeout(
1286
+ `${this.apiUrl}/v1/images/models`,
1287
+ { method: "GET" }
1288
+ );
1289
+ if (!response.ok) {
1290
+ throw new APIError(
1291
+ `Failed to list image models: ${response.status}`,
1292
+ response.status
1293
+ );
1294
+ }
1295
+ const data = await response.json();
1296
+ return data.data || [];
1297
+ }
1298
+ /**
1299
+ * Make a request with automatic x402 payment handling.
1300
+ */
1301
+ async requestWithPayment(endpoint, body) {
1302
+ const url = `${this.apiUrl}${endpoint}`;
1303
+ const response = await this.fetchWithTimeout(url, {
1304
+ method: "POST",
1305
+ headers: { "Content-Type": "application/json" },
1306
+ body: JSON.stringify(body)
1307
+ });
1308
+ if (response.status === 402) {
1309
+ return this.handlePaymentAndRetry(url, body, response);
1310
+ }
1311
+ if (!response.ok) {
1312
+ let errorBody;
1313
+ try {
1314
+ errorBody = await response.json();
1315
+ } catch {
1316
+ errorBody = { error: "Request failed" };
1317
+ }
1318
+ throw new APIError(
1319
+ `API error: ${response.status}`,
1320
+ response.status,
1321
+ sanitizeErrorResponse(errorBody)
1322
+ );
1323
+ }
1324
+ return response.json();
1325
+ }
1326
+ /**
1327
+ * Handle 402 response: parse requirements, sign payment, retry.
1328
+ */
1329
+ async handlePaymentAndRetry(url, body, response) {
1330
+ let paymentHeader = response.headers.get("payment-required");
1331
+ if (!paymentHeader) {
1332
+ try {
1333
+ const respBody = await response.json();
1334
+ if (respBody.x402 || respBody.accepts) {
1335
+ paymentHeader = btoa(JSON.stringify(respBody));
1336
+ }
1337
+ } catch {
1338
+ }
1339
+ }
1340
+ if (!paymentHeader) {
1341
+ throw new PaymentError("402 response but no payment requirements found");
1342
+ }
1343
+ const paymentRequired = parsePaymentRequired(paymentHeader);
1344
+ const details = extractPaymentDetails(paymentRequired);
1345
+ const extensions = paymentRequired.extensions;
1346
+ const paymentPayload = await createPaymentPayload(
1347
+ this.privateKey,
1348
+ this.account.address,
1349
+ details.recipient,
1350
+ details.amount,
1351
+ details.network || "eip155:8453",
1352
+ {
1353
+ resourceUrl: validateResourceUrl(
1354
+ details.resource?.url || `${this.apiUrl}/v1/images/generations`,
1355
+ this.apiUrl
1356
+ ),
1357
+ resourceDescription: details.resource?.description || "BlockRun Image Generation",
1358
+ maxTimeoutSeconds: details.maxTimeoutSeconds || 300,
1359
+ extra: details.extra,
1360
+ extensions
1361
+ }
1362
+ );
1363
+ const retryResponse = await this.fetchWithTimeout(url, {
1364
+ method: "POST",
1365
+ headers: {
1366
+ "Content-Type": "application/json",
1367
+ "PAYMENT-SIGNATURE": paymentPayload
1368
+ },
1369
+ body: JSON.stringify(body)
1370
+ });
1371
+ if (retryResponse.status === 402) {
1372
+ throw new PaymentError(
1373
+ "Payment was rejected. Check your wallet balance."
1374
+ );
1375
+ }
1376
+ if (!retryResponse.ok) {
1377
+ let errorBody;
1378
+ try {
1379
+ errorBody = await retryResponse.json();
1380
+ } catch {
1381
+ errorBody = { error: "Request failed" };
1382
+ }
1383
+ throw new APIError(
1384
+ `API error after payment: ${retryResponse.status}`,
1385
+ retryResponse.status,
1386
+ sanitizeErrorResponse(errorBody)
1387
+ );
1388
+ }
1389
+ return retryResponse.json();
1390
+ }
1391
+ /**
1392
+ * Fetch with timeout.
1393
+ */
1394
+ async fetchWithTimeout(url, options) {
1395
+ const controller = new AbortController();
1396
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
1397
+ try {
1398
+ const response = await fetch(url, {
1399
+ ...options,
1400
+ signal: controller.signal
1401
+ });
1402
+ return response;
1403
+ } finally {
1404
+ clearTimeout(timeoutId);
1405
+ }
1406
+ }
1407
+ /**
1408
+ * Get the wallet address being used for payments.
1409
+ */
1410
+ getWalletAddress() {
1411
+ return this.account.address;
1412
+ }
1413
+ /**
1414
+ * Get session spending information.
1415
+ */
1416
+ getSpending() {
1417
+ return {
1418
+ totalUsd: this.sessionTotalUsd,
1419
+ calls: this.sessionCalls
1420
+ };
1421
+ }
1422
+ };
1423
+
1424
+ // src/wallet.ts
1425
+ import { privateKeyToAccount as privateKeyToAccount3, generatePrivateKey } from "viem/accounts";
1426
+ import * as fs from "fs";
1427
+ import * as path from "path";
1428
+ import * as os from "os";
1429
+ var USDC_BASE_CONTRACT = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
1430
+ var BASE_CHAIN_ID2 = "8453";
1431
+ var WALLET_DIR = path.join(os.homedir(), ".blockrun");
1432
+ var WALLET_FILE = path.join(WALLET_DIR, ".session");
1433
+ function createWallet() {
1434
+ const privateKey = generatePrivateKey();
1435
+ const account = privateKeyToAccount3(privateKey);
1436
+ return {
1437
+ address: account.address,
1438
+ privateKey
1439
+ };
1440
+ }
1441
+ function saveWallet(privateKey) {
1442
+ if (!fs.existsSync(WALLET_DIR)) {
1443
+ fs.mkdirSync(WALLET_DIR, { recursive: true });
1444
+ }
1445
+ fs.writeFileSync(WALLET_FILE, privateKey, { mode: 384 });
1446
+ return WALLET_FILE;
1447
+ }
1448
+ function scanWallets() {
1449
+ const home = os.homedir();
1450
+ const results = [];
1451
+ try {
1452
+ const entries = fs.readdirSync(home, { withFileTypes: true });
1453
+ for (const entry of entries) {
1454
+ if (!entry.name.startsWith(".") || !entry.isDirectory()) continue;
1455
+ const walletFile = path.join(home, entry.name, "wallet.json");
1456
+ if (!fs.existsSync(walletFile)) continue;
1457
+ try {
1458
+ const data = JSON.parse(fs.readFileSync(walletFile, "utf-8"));
1459
+ const pk = data.privateKey || "";
1460
+ const addr = data.address || "";
1461
+ if (pk && addr) {
1462
+ const mtime = fs.statSync(walletFile).mtimeMs;
1463
+ results.push({ mtime, privateKey: pk, address: addr });
1464
+ }
1465
+ } catch {
1466
+ continue;
1467
+ }
1468
+ }
1469
+ } catch {
1470
+ }
1471
+ results.sort((a, b) => b.mtime - a.mtime);
1472
+ return results.map(({ privateKey, address }) => ({ privateKey, address }));
1473
+ }
1474
+ function loadWallet() {
1475
+ const wallets = scanWallets();
1476
+ if (wallets.length > 0) return wallets[0].privateKey;
1477
+ if (fs.existsSync(WALLET_FILE)) {
1478
+ const key = fs.readFileSync(WALLET_FILE, "utf-8").trim();
1479
+ if (key) return key;
1480
+ }
1481
+ const legacyFile = path.join(WALLET_DIR, "wallet.key");
1482
+ if (fs.existsSync(legacyFile)) {
1483
+ const key = fs.readFileSync(legacyFile, "utf-8").trim();
1484
+ if (key) return key;
1485
+ }
1486
+ return null;
1487
+ }
1488
+ function getOrCreateWallet() {
1489
+ const envKey = typeof process !== "undefined" && process.env ? process.env.BLOCKRUN_WALLET_KEY || process.env.BASE_CHAIN_WALLET_KEY : void 0;
1490
+ if (envKey) {
1491
+ const account = privateKeyToAccount3(envKey);
1492
+ return { address: account.address, privateKey: envKey, isNew: false };
1493
+ }
1494
+ const fileKey = loadWallet();
1495
+ if (fileKey) {
1496
+ const account = privateKeyToAccount3(fileKey);
1497
+ return { address: account.address, privateKey: fileKey, isNew: false };
1498
+ }
1499
+ const { address, privateKey } = createWallet();
1500
+ saveWallet(privateKey);
1501
+ return { address, privateKey, isNew: true };
1502
+ }
1503
+ function getWalletAddress() {
1504
+ const envKey = typeof process !== "undefined" && process.env ? process.env.BLOCKRUN_WALLET_KEY || process.env.BASE_CHAIN_WALLET_KEY : void 0;
1505
+ if (envKey) {
1506
+ return privateKeyToAccount3(envKey).address;
1507
+ }
1508
+ const fileKey = loadWallet();
1509
+ if (fileKey) {
1510
+ return privateKeyToAccount3(fileKey).address;
1511
+ }
1512
+ return null;
1513
+ }
1514
+ function getEip681Uri(address, amountUsdc = 1) {
1515
+ const amountWei = Math.floor(amountUsdc * 1e6);
1516
+ return `ethereum:${USDC_BASE_CONTRACT}@${BASE_CHAIN_ID2}/transfer?address=${address}&uint256=${amountWei}`;
1517
+ }
1518
+ function getPaymentLinks(address) {
1519
+ return {
1520
+ basescan: `https://basescan.org/address/${address}`,
1521
+ walletLink: `ethereum:${USDC_BASE_CONTRACT}@${BASE_CHAIN_ID2}/transfer?address=${address}`,
1522
+ ethereum: `ethereum:${address}@${BASE_CHAIN_ID2}`,
1523
+ blockrun: `https://blockrun.ai/fund?address=${address}`
1524
+ };
1525
+ }
1526
+ function formatWalletCreatedMessage(address) {
1527
+ const links = getPaymentLinks(address);
1528
+ return `
1529
+ I'm your BlockRun Agent! I can access GPT-4, Grok, image generation, and more.
1530
+
1531
+ Please send $1-5 USDC on Base to start:
1532
+
1533
+ ${address}
1534
+
1535
+ What is Base? Base is Coinbase's blockchain network.
1536
+ You can buy USDC on Coinbase and send it directly to me.
1537
+
1538
+ What $1 USDC gets you:
1539
+ - ~1,000 GPT-4o calls
1540
+ - ~100 image generations
1541
+ - ~10,000 DeepSeek calls
1542
+
1543
+ Quick links:
1544
+ - Check my balance: ${links.basescan}
1545
+ - Get USDC: https://www.coinbase.com or https://bridge.base.org
1546
+
1547
+ Questions? care@blockrun.ai | Issues? github.com/BlockRunAI/blockrun-llm-ts/issues
1548
+
1549
+ Key stored securely in ~/.blockrun/
1550
+ Your private key never leaves your machine - only signatures are sent.
1551
+ `;
1552
+ }
1553
+ function formatNeedsFundingMessage(address) {
1554
+ const links = getPaymentLinks(address);
1555
+ return `
1556
+ I've run out of funds! Please send more USDC on Base to continue helping you.
1557
+
1558
+ Send to my address:
1559
+ ${address}
1560
+
1561
+ Check my balance: ${links.basescan}
1562
+
1563
+ What $1 USDC gets you: ~1,000 GPT-4o calls or ~100 images.
1564
+ Questions? care@blockrun.ai | Issues? github.com/BlockRunAI/blockrun-llm-ts/issues
1565
+
1566
+ Your private key never leaves your machine - only signatures are sent.
1567
+ `;
1568
+ }
1569
+ function formatFundingMessageCompact(address) {
1570
+ const links = getPaymentLinks(address);
1571
+ return `I need a little top-up to keep helping you! Send USDC on Base to: ${address}
1572
+ Check my balance: ${links.basescan}`;
1573
+ }
1574
+ var WALLET_FILE_PATH = WALLET_FILE;
1575
+ var WALLET_DIR_PATH = WALLET_DIR;
1576
+
1577
+ // src/solana-wallet.ts
1578
+ import * as fs2 from "fs";
1579
+ import * as path2 from "path";
1580
+ import * as os2 from "os";
1581
+ var WALLET_DIR2 = path2.join(os2.homedir(), ".blockrun");
1582
+ var SOLANA_WALLET_FILE = path2.join(WALLET_DIR2, ".solana-session");
1583
+ function createSolanaWallet() {
1584
+ const { Keypair } = (init_index_esm(), __toCommonJS(index_esm_exports));
1585
+ const bs58 = __require("bs58");
1586
+ const keypair = Keypair.generate();
1587
+ return {
1588
+ address: keypair.publicKey.toBase58(),
1589
+ privateKey: bs58.default?.encode(keypair.secretKey) ?? bs58.encode(keypair.secretKey)
1590
+ };
1591
+ }
1592
+ async function solanaKeyToBytes(privateKey) {
1593
+ try {
1594
+ const bs58 = await import("bs58");
1595
+ const bytes = (bs58.default ?? bs58).decode(privateKey);
1596
+ if (bytes.length !== 64) {
1597
+ throw new Error(`Invalid Solana key length: expected 64 bytes, got ${bytes.length}`);
1598
+ }
1599
+ return bytes;
1600
+ } catch (err) {
1601
+ const msg = err instanceof Error ? err.message : String(err);
1602
+ throw new Error(`Invalid Solana private key: ${msg}`);
1603
+ }
1604
+ }
1605
+ async function solanaPublicKey(privateKey) {
1606
+ const { Keypair } = await import("./index.esm-SXKIFLA7.js");
1607
+ const bytes = await solanaKeyToBytes(privateKey);
1608
+ return Keypair.fromSecretKey(bytes).publicKey.toBase58();
1609
+ }
1610
+ function saveSolanaWallet(privateKey) {
1611
+ if (!fs2.existsSync(WALLET_DIR2)) fs2.mkdirSync(WALLET_DIR2, { recursive: true });
1612
+ fs2.writeFileSync(SOLANA_WALLET_FILE, privateKey, { mode: 384 });
1613
+ return SOLANA_WALLET_FILE;
1614
+ }
1615
+ function scanSolanaWallets() {
1616
+ const home = os2.homedir();
1617
+ const results = [];
1618
+ try {
1619
+ const entries = fs2.readdirSync(home, { withFileTypes: true });
1620
+ for (const entry of entries) {
1621
+ if (!entry.name.startsWith(".") || !entry.isDirectory()) continue;
1622
+ const solanaWalletFile = path2.join(home, entry.name, "solana-wallet.json");
1623
+ if (fs2.existsSync(solanaWalletFile)) {
1624
+ try {
1625
+ const data = JSON.parse(fs2.readFileSync(solanaWalletFile, "utf-8"));
1626
+ const pk = data.privateKey || "";
1627
+ const addr = data.address || "";
1628
+ if (pk && addr) {
1629
+ const mtime = fs2.statSync(solanaWalletFile).mtimeMs;
1630
+ results.push({ mtime, secretKey: pk, publicKey: addr });
1631
+ }
1632
+ } catch {
1633
+ }
1634
+ }
1635
+ if (entry.name === ".brcc") {
1636
+ const brccWalletFile = path2.join(home, entry.name, "wallet.json");
1637
+ if (fs2.existsSync(brccWalletFile)) {
1638
+ try {
1639
+ const data = JSON.parse(fs2.readFileSync(brccWalletFile, "utf-8"));
1640
+ const pk = data.privateKey || "";
1641
+ const addr = data.address || "";
1642
+ if (pk && addr) {
1643
+ const mtime = fs2.statSync(brccWalletFile).mtimeMs;
1644
+ results.push({ mtime, secretKey: pk, publicKey: addr });
1645
+ }
1646
+ } catch {
1647
+ }
1648
+ }
1649
+ }
1650
+ }
1651
+ } catch {
1652
+ }
1653
+ results.sort((a, b) => b.mtime - a.mtime);
1654
+ return results.map(({ secretKey, publicKey }) => ({ secretKey, publicKey }));
1655
+ }
1656
+ function loadSolanaWallet() {
1657
+ const wallets = scanSolanaWallets();
1658
+ if (wallets.length > 0) return wallets[0].secretKey;
1659
+ if (fs2.existsSync(SOLANA_WALLET_FILE)) {
1660
+ const key = fs2.readFileSync(SOLANA_WALLET_FILE, "utf-8").trim();
1661
+ if (key) return key;
1662
+ }
1663
+ return null;
1664
+ }
1665
+ async function getOrCreateSolanaWallet() {
1666
+ const envKey = typeof process !== "undefined" && process.env ? process.env.SOLANA_WALLET_KEY : void 0;
1667
+ if (envKey) {
1668
+ const address2 = await solanaPublicKey(envKey);
1669
+ return { privateKey: envKey, address: address2, isNew: false };
1670
+ }
1671
+ const wallets = scanSolanaWallets();
1672
+ if (wallets.length > 0) {
1673
+ return { privateKey: wallets[0].secretKey, address: wallets[0].publicKey, isNew: false };
1674
+ }
1675
+ if (fs2.existsSync(SOLANA_WALLET_FILE)) {
1676
+ const fileKey = fs2.readFileSync(SOLANA_WALLET_FILE, "utf-8").trim();
1677
+ if (fileKey) {
1678
+ const address2 = await solanaPublicKey(fileKey);
1679
+ return { privateKey: fileKey, address: address2, isNew: false };
1680
+ }
1681
+ }
1682
+ const { address, privateKey } = createSolanaWallet();
1683
+ saveSolanaWallet(privateKey);
1684
+ return { address, privateKey, isNew: true };
1685
+ }
1686
+
1687
+ // src/solana-client.ts
1688
+ var SOLANA_API_URL = "https://sol.blockrun.ai/api";
1689
+ var DEFAULT_MAX_TOKENS2 = 1024;
1690
+ var DEFAULT_TIMEOUT3 = 6e4;
1691
+ var SDK_VERSION2 = "0.3.0";
1692
+ var USER_AGENT2 = `blockrun-ts/${SDK_VERSION2}`;
1693
+ var SolanaLLMClient = class {
1694
+ static SOLANA_API_URL = SOLANA_API_URL;
1695
+ privateKey;
1696
+ apiUrl;
1697
+ rpcUrl;
1698
+ timeout;
1699
+ sessionTotalUsd = 0;
1700
+ sessionCalls = 0;
1701
+ addressCache = null;
1702
+ constructor(options = {}) {
1703
+ const envKey = typeof process !== "undefined" && process.env ? process.env.SOLANA_WALLET_KEY : void 0;
1704
+ const privateKey = options.privateKey || envKey;
1705
+ if (!privateKey) {
1706
+ throw new Error(
1707
+ "Private key required. Pass privateKey in options or set SOLANA_WALLET_KEY environment variable."
1708
+ );
1709
+ }
1710
+ this.privateKey = privateKey;
1711
+ const apiUrl = options.apiUrl || SOLANA_API_URL;
1712
+ validateApiUrl(apiUrl);
1713
+ this.apiUrl = apiUrl.replace(/\/$/, "");
1714
+ this.rpcUrl = options.rpcUrl || "https://api.mainnet-beta.solana.com";
1715
+ this.timeout = options.timeout || DEFAULT_TIMEOUT3;
1716
+ }
1717
+ /** Get Solana wallet address (public key in base58). */
1718
+ async getWalletAddress() {
1719
+ if (!this.addressCache) {
1720
+ this.addressCache = await solanaPublicKey(this.privateKey);
1721
+ }
1722
+ return this.addressCache;
1723
+ }
1724
+ /** Simple 1-line chat. */
1725
+ async chat(model, prompt, options) {
1726
+ const messages = [];
1727
+ if (options?.system) messages.push({ role: "system", content: options.system });
1728
+ messages.push({ role: "user", content: prompt });
1729
+ const result = await this.chatCompletion(model, messages, {
1730
+ maxTokens: options?.maxTokens,
1731
+ temperature: options?.temperature,
1732
+ topP: options?.topP,
1733
+ search: options?.search,
1734
+ searchParameters: options?.searchParameters
1735
+ });
1736
+ return result.choices[0].message.content || "";
1737
+ }
1738
+ /** Full chat completion (OpenAI-compatible). */
1739
+ async chatCompletion(model, messages, options) {
1740
+ const body = {
1741
+ model,
1742
+ messages,
1743
+ max_tokens: options?.maxTokens || DEFAULT_MAX_TOKENS2
1744
+ };
1745
+ if (options?.temperature !== void 0) body.temperature = options.temperature;
1746
+ if (options?.topP !== void 0) body.top_p = options.topP;
1747
+ if (options?.searchParameters !== void 0) body.search_parameters = options.searchParameters;
1748
+ else if (options?.search === true) body.search_parameters = { mode: "on" };
1749
+ if (options?.tools !== void 0) body.tools = options.tools;
1750
+ if (options?.toolChoice !== void 0) body.tool_choice = options.toolChoice;
1751
+ return this.requestWithPayment("/v1/chat/completions", body);
1752
+ }
1753
+ /** List available models. */
1754
+ async listModels() {
1755
+ const response = await this.fetchWithTimeout(`${this.apiUrl}/v1/models`, { method: "GET" });
1756
+ if (!response.ok) {
1757
+ throw new APIError(`Failed to list models: ${response.status}`, response.status);
1758
+ }
1759
+ const data = await response.json();
1760
+ return data.data || [];
1761
+ }
1762
+ /**
1763
+ * Get Solana USDC balance.
1764
+ *
1765
+ * @returns USDC balance as a float
1766
+ */
1767
+ async getBalance() {
1768
+ const usdc_mint = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
1769
+ const address = await this.getWalletAddress();
1770
+ try {
1771
+ const response = await fetch(this.rpcUrl, {
1772
+ method: "POST",
1773
+ headers: { "Content-Type": "application/json" },
1774
+ body: JSON.stringify({
1775
+ jsonrpc: "2.0",
1776
+ id: 1,
1777
+ method: "getTokenAccountsByOwner",
1778
+ params: [address, { mint: usdc_mint }, { encoding: "jsonParsed" }]
1779
+ })
1780
+ });
1781
+ const data = await response.json();
1782
+ const accounts = data.result?.value || [];
1783
+ if (!accounts.length) return 0;
1784
+ let total = 0;
1785
+ for (const acct of accounts) {
1786
+ total += acct.account?.data?.parsed?.info?.tokenAmount?.uiAmount || 0;
1787
+ }
1788
+ return total;
1789
+ } catch {
1790
+ return 0;
1791
+ }
1792
+ }
1793
+ /** Edit an image using img2img (Solana payment). */
1794
+ async imageEdit(prompt, image, options) {
1795
+ const body = {
1796
+ model: options?.model || "openai/gpt-image-1",
1797
+ prompt,
1798
+ image,
1799
+ size: options?.size || "1024x1024",
1800
+ n: options?.n || 1
1801
+ };
1802
+ if (options?.mask !== void 0) body.mask = options.mask;
1803
+ const data = await this.requestWithPaymentRaw("/v1/images/image2image", body);
1804
+ return data;
1805
+ }
1806
+ /** Standalone search (Solana payment). */
1807
+ async search(query, options) {
1808
+ const body = { query, max_results: options?.maxResults || 10 };
1809
+ if (options?.sources !== void 0) body.sources = options.sources;
1810
+ if (options?.fromDate !== void 0) body.from_date = options.fromDate;
1811
+ if (options?.toDate !== void 0) body.to_date = options.toDate;
1812
+ const data = await this.requestWithPaymentRaw("/v1/search", body);
1813
+ return data;
1814
+ }
1815
+ // ============================================================
1816
+ // X/Twitter endpoints (powered by AttentionVC)
1817
+ // ============================================================
1818
+ async xUserLookup(usernames) {
1819
+ const names = Array.isArray(usernames) ? usernames : [usernames];
1820
+ const data = await this.requestWithPaymentRaw("/v1/x/users/lookup", { usernames: names });
1821
+ return data;
1822
+ }
1823
+ async xFollowers(username, cursor) {
1824
+ const body = { username };
1825
+ if (cursor !== void 0) body.cursor = cursor;
1826
+ const data = await this.requestWithPaymentRaw("/v1/x/users/followers", body);
1827
+ return data;
1828
+ }
1829
+ async xFollowings(username, cursor) {
1830
+ const body = { username };
1831
+ if (cursor !== void 0) body.cursor = cursor;
1832
+ const data = await this.requestWithPaymentRaw("/v1/x/users/followings", body);
1833
+ return data;
1834
+ }
1835
+ async xUserInfo(username) {
1836
+ const data = await this.requestWithPaymentRaw("/v1/x/users/info", { username });
1837
+ return data;
1838
+ }
1839
+ async xVerifiedFollowers(userId, cursor) {
1840
+ const body = { userId };
1841
+ if (cursor !== void 0) body.cursor = cursor;
1842
+ const data = await this.requestWithPaymentRaw("/v1/x/users/verified-followers", body);
1843
+ return data;
1844
+ }
1845
+ async xUserTweets(username, includeReplies = false, cursor) {
1846
+ const body = { username, includeReplies };
1847
+ if (cursor !== void 0) body.cursor = cursor;
1848
+ const data = await this.requestWithPaymentRaw("/v1/x/users/tweets", body);
1849
+ return data;
1850
+ }
1851
+ async xUserMentions(username, sinceTime, untilTime, cursor) {
1852
+ const body = { username };
1853
+ if (sinceTime !== void 0) body.sinceTime = sinceTime;
1854
+ if (untilTime !== void 0) body.untilTime = untilTime;
1855
+ if (cursor !== void 0) body.cursor = cursor;
1856
+ const data = await this.requestWithPaymentRaw("/v1/x/users/mentions", body);
1857
+ return data;
1858
+ }
1859
+ async xTweetLookup(tweetIds) {
1860
+ const ids = Array.isArray(tweetIds) ? tweetIds : [tweetIds];
1861
+ const data = await this.requestWithPaymentRaw("/v1/x/tweets/lookup", { tweet_ids: ids });
1862
+ return data;
1863
+ }
1864
+ async xTweetReplies(tweetId, queryType = "Latest", cursor) {
1865
+ const body = { tweetId, queryType };
1866
+ if (cursor !== void 0) body.cursor = cursor;
1867
+ const data = await this.requestWithPaymentRaw("/v1/x/tweets/replies", body);
1868
+ return data;
1869
+ }
1870
+ async xTweetThread(tweetId, cursor) {
1871
+ const body = { tweetId };
1872
+ if (cursor !== void 0) body.cursor = cursor;
1873
+ const data = await this.requestWithPaymentRaw("/v1/x/tweets/thread", body);
1874
+ return data;
1875
+ }
1876
+ async xSearch(query, queryType = "Latest", cursor) {
1877
+ const body = { query, queryType };
1878
+ if (cursor !== void 0) body.cursor = cursor;
1879
+ const data = await this.requestWithPaymentRaw("/v1/x/search", body);
1880
+ return data;
1881
+ }
1882
+ async xTrending() {
1883
+ const data = await this.requestWithPaymentRaw("/v1/x/trending", {});
1884
+ return data;
1885
+ }
1886
+ async xArticlesRising() {
1887
+ const data = await this.requestWithPaymentRaw("/v1/x/articles/rising", {});
1888
+ return data;
1889
+ }
1890
+ async xAuthorAnalytics(handle) {
1891
+ const data = await this.requestWithPaymentRaw("/v1/x/authors", { handle });
1892
+ return data;
1893
+ }
1894
+ async xCompareAuthors(handle1, handle2) {
1895
+ const data = await this.requestWithPaymentRaw("/v1/x/compare", { handle1, handle2 });
1896
+ return data;
1897
+ }
1898
+ // ── Prediction Markets (Powered by Predexon) ────────────────────────────
1899
+ async pm(path5, params) {
1900
+ return this.getWithPaymentRaw(`/v1/pm/${path5}`, params);
1901
+ }
1902
+ async pmQuery(path5, query) {
1903
+ return this.requestWithPaymentRaw(`/v1/pm/${path5}`, query);
1904
+ }
1905
+ /** Get session spending. */
1906
+ getSpending() {
1907
+ return { totalUsd: this.sessionTotalUsd, calls: this.sessionCalls };
1908
+ }
1909
+ /** True if using sol.blockrun.ai. */
1910
+ isSolana() {
1911
+ return this.apiUrl.includes("sol.blockrun.ai");
1912
+ }
1913
+ async requestWithPayment(endpoint, body) {
1914
+ const url = `${this.apiUrl}${endpoint}`;
1915
+ const response = await this.fetchWithTimeout(url, {
1916
+ method: "POST",
1917
+ headers: { "Content-Type": "application/json", "User-Agent": USER_AGENT2 },
1918
+ body: JSON.stringify(body)
1919
+ });
1920
+ if (response.status === 402) {
1921
+ return this.handlePaymentAndRetry(url, body, response);
1922
+ }
1923
+ if (!response.ok) {
1924
+ let errorBody;
1925
+ try {
1926
+ errorBody = await response.json();
1927
+ } catch {
1928
+ errorBody = { error: "Request failed" };
1929
+ }
1930
+ throw new APIError(`API error: ${response.status}`, response.status, sanitizeErrorResponse(errorBody));
1931
+ }
1932
+ return response.json();
1933
+ }
1934
+ async handlePaymentAndRetry(url, body, response) {
1935
+ let paymentHeader = response.headers.get("payment-required");
1936
+ if (!paymentHeader) {
1937
+ try {
1938
+ const respBody = await response.json();
1939
+ if (respBody.accepts || respBody.x402Version) {
1940
+ paymentHeader = btoa(JSON.stringify(respBody));
1941
+ }
1942
+ } catch {
1943
+ }
1944
+ }
1945
+ if (!paymentHeader) {
1946
+ throw new PaymentError("402 response but no payment requirements found");
1947
+ }
1948
+ const paymentRequired = parsePaymentRequired(paymentHeader);
1949
+ const details = extractPaymentDetails(paymentRequired, SOLANA_NETWORK);
1950
+ if (!details.network?.startsWith("solana:")) {
1951
+ throw new PaymentError(
1952
+ `Expected Solana payment network, got: ${details.network}. Use LLMClient for Base payments.`
1953
+ );
1954
+ }
1955
+ const feePayer = details.extra?.feePayer;
1956
+ if (!feePayer) throw new PaymentError("Missing feePayer in 402 extra field");
1957
+ const fromAddress = await this.getWalletAddress();
1958
+ const secretKey = await solanaKeyToBytes(this.privateKey);
1959
+ const extensions = paymentRequired.extensions;
1960
+ const paymentPayload = await createSolanaPaymentPayload(
1961
+ secretKey,
1962
+ fromAddress,
1963
+ details.recipient,
1964
+ details.amount,
1965
+ feePayer,
1966
+ {
1967
+ resourceUrl: validateResourceUrl(
1968
+ details.resource?.url || `${this.apiUrl}/v1/chat/completions`,
1969
+ this.apiUrl
1970
+ ),
1971
+ resourceDescription: details.resource?.description || "BlockRun Solana AI API call",
1972
+ maxTimeoutSeconds: details.maxTimeoutSeconds || 300,
1973
+ extra: details.extra,
1974
+ extensions,
1975
+ rpcUrl: this.rpcUrl
1976
+ }
1977
+ );
1978
+ const retryResponse = await this.fetchWithTimeout(url, {
1979
+ method: "POST",
1980
+ headers: {
1981
+ "Content-Type": "application/json",
1982
+ "User-Agent": USER_AGENT2,
1983
+ "PAYMENT-SIGNATURE": paymentPayload
1984
+ },
1985
+ body: JSON.stringify(body)
1986
+ });
1987
+ if (retryResponse.status === 402) {
1988
+ throw new PaymentError("Payment was rejected. Check your Solana USDC balance.");
1989
+ }
1990
+ if (!retryResponse.ok) {
1991
+ let errorBody;
1992
+ try {
1993
+ errorBody = await retryResponse.json();
1994
+ } catch {
1995
+ errorBody = { error: "Request failed" };
1996
+ }
1997
+ throw new APIError(`API error after payment: ${retryResponse.status}`, retryResponse.status, sanitizeErrorResponse(errorBody));
1998
+ }
1999
+ const costUsd = parseFloat(details.amount) / 1e6;
2000
+ this.sessionCalls += 1;
2001
+ this.sessionTotalUsd += costUsd;
2002
+ return retryResponse.json();
2003
+ }
2004
+ async requestWithPaymentRaw(endpoint, body) {
2005
+ const url = `${this.apiUrl}${endpoint}`;
2006
+ const response = await this.fetchWithTimeout(url, {
2007
+ method: "POST",
2008
+ headers: { "Content-Type": "application/json", "User-Agent": USER_AGENT2 },
2009
+ body: JSON.stringify(body)
2010
+ });
2011
+ if (response.status === 402) {
2012
+ return this.handlePaymentAndRetryRaw(url, body, response);
2013
+ }
2014
+ if (!response.ok) {
2015
+ let errorBody;
2016
+ try {
2017
+ errorBody = await response.json();
2018
+ } catch {
2019
+ errorBody = { error: "Request failed" };
2020
+ }
2021
+ throw new APIError(`API error: ${response.status}`, response.status, sanitizeErrorResponse(errorBody));
2022
+ }
2023
+ return response.json();
2024
+ }
2025
+ async handlePaymentAndRetryRaw(url, body, response) {
2026
+ let paymentHeader = response.headers.get("payment-required");
2027
+ if (!paymentHeader) {
2028
+ try {
2029
+ const respBody = await response.json();
2030
+ if (respBody.accepts || respBody.x402Version) {
2031
+ paymentHeader = btoa(JSON.stringify(respBody));
2032
+ }
2033
+ } catch {
2034
+ }
2035
+ }
2036
+ if (!paymentHeader) {
2037
+ throw new PaymentError("402 response but no payment requirements found");
2038
+ }
2039
+ const paymentRequired = parsePaymentRequired(paymentHeader);
2040
+ const details = extractPaymentDetails(paymentRequired, SOLANA_NETWORK);
2041
+ if (!details.network?.startsWith("solana:")) {
2042
+ throw new PaymentError(
2043
+ `Expected Solana payment network, got: ${details.network}. Use LLMClient for Base payments.`
2044
+ );
2045
+ }
2046
+ const feePayer = details.extra?.feePayer;
2047
+ if (!feePayer) throw new PaymentError("Missing feePayer in 402 extra field");
2048
+ const fromAddress = await this.getWalletAddress();
2049
+ const secretKey = await solanaKeyToBytes(this.privateKey);
2050
+ const extensions = paymentRequired.extensions;
2051
+ const paymentPayload = await createSolanaPaymentPayload(
2052
+ secretKey,
2053
+ fromAddress,
2054
+ details.recipient,
2055
+ details.amount,
2056
+ feePayer,
2057
+ {
2058
+ resourceUrl: validateResourceUrl(
2059
+ details.resource?.url || url,
2060
+ this.apiUrl
2061
+ ),
2062
+ resourceDescription: details.resource?.description || "BlockRun Solana AI API call",
2063
+ maxTimeoutSeconds: details.maxTimeoutSeconds || 300,
2064
+ extra: details.extra,
2065
+ extensions,
2066
+ rpcUrl: this.rpcUrl
2067
+ }
2068
+ );
2069
+ const retryResponse = await this.fetchWithTimeout(url, {
2070
+ method: "POST",
2071
+ headers: {
2072
+ "Content-Type": "application/json",
2073
+ "User-Agent": USER_AGENT2,
2074
+ "PAYMENT-SIGNATURE": paymentPayload
2075
+ },
2076
+ body: JSON.stringify(body)
2077
+ });
2078
+ if (retryResponse.status === 402) {
2079
+ throw new PaymentError("Payment was rejected. Check your Solana USDC balance.");
2080
+ }
2081
+ if (!retryResponse.ok) {
2082
+ let errorBody;
2083
+ try {
2084
+ errorBody = await retryResponse.json();
2085
+ } catch {
2086
+ errorBody = { error: "Request failed" };
2087
+ }
2088
+ throw new APIError(`API error after payment: ${retryResponse.status}`, retryResponse.status, sanitizeErrorResponse(errorBody));
2089
+ }
2090
+ const costUsd = parseFloat(details.amount) / 1e6;
2091
+ this.sessionCalls += 1;
2092
+ this.sessionTotalUsd += costUsd;
2093
+ return retryResponse.json();
2094
+ }
2095
+ async getWithPaymentRaw(endpoint, params) {
2096
+ const query = params ? "?" + new URLSearchParams(params).toString() : "";
2097
+ const url = `${this.apiUrl}${endpoint}${query}`;
2098
+ const response = await this.fetchWithTimeout(url, {
2099
+ method: "GET",
2100
+ headers: { "User-Agent": USER_AGENT2 }
2101
+ });
2102
+ if (response.status === 402) {
2103
+ return this.handleGetPaymentAndRetryRaw(url, endpoint, params, response);
2104
+ }
2105
+ if (!response.ok) {
2106
+ let errorBody;
2107
+ try {
2108
+ errorBody = await response.json();
2109
+ } catch {
2110
+ errorBody = { error: "Request failed" };
2111
+ }
2112
+ throw new APIError(`API error: ${response.status}`, response.status, sanitizeErrorResponse(errorBody));
2113
+ }
2114
+ return response.json();
2115
+ }
2116
+ async handleGetPaymentAndRetryRaw(url, endpoint, params, response) {
2117
+ let paymentHeader = response.headers.get("payment-required");
2118
+ if (!paymentHeader) {
2119
+ try {
2120
+ const respBody = await response.json();
2121
+ if (respBody.accepts || respBody.x402Version) {
2122
+ paymentHeader = btoa(JSON.stringify(respBody));
2123
+ }
2124
+ } catch {
2125
+ }
2126
+ }
2127
+ if (!paymentHeader) {
2128
+ throw new PaymentError("402 response but no payment requirements found");
2129
+ }
2130
+ const paymentRequired = parsePaymentRequired(paymentHeader);
2131
+ const details = extractPaymentDetails(paymentRequired, SOLANA_NETWORK);
2132
+ if (!details.network?.startsWith("solana:")) {
2133
+ throw new PaymentError(
2134
+ `Expected Solana payment network, got: ${details.network}. Use LLMClient for Base payments.`
2135
+ );
2136
+ }
2137
+ const feePayer = details.extra?.feePayer;
2138
+ if (!feePayer) throw new PaymentError("Missing feePayer in 402 extra field");
2139
+ const fromAddress = await this.getWalletAddress();
2140
+ const secretKey = await solanaKeyToBytes(this.privateKey);
2141
+ const extensions = paymentRequired.extensions;
2142
+ const paymentPayload = await createSolanaPaymentPayload(
2143
+ secretKey,
2144
+ fromAddress,
2145
+ details.recipient,
2146
+ details.amount,
2147
+ feePayer,
2148
+ {
2149
+ resourceUrl: validateResourceUrl(
2150
+ details.resource?.url || url,
2151
+ this.apiUrl
2152
+ ),
2153
+ resourceDescription: details.resource?.description || "BlockRun Solana AI API call",
2154
+ maxTimeoutSeconds: details.maxTimeoutSeconds || 300,
2155
+ extra: details.extra,
2156
+ extensions,
2157
+ rpcUrl: this.rpcUrl
2158
+ }
2159
+ );
2160
+ const query = params ? "?" + new URLSearchParams(params).toString() : "";
2161
+ const retryUrl = `${this.apiUrl}${endpoint}${query}`;
2162
+ const retryResponse = await this.fetchWithTimeout(retryUrl, {
2163
+ method: "GET",
2164
+ headers: {
2165
+ "User-Agent": USER_AGENT2,
2166
+ "PAYMENT-SIGNATURE": paymentPayload
2167
+ }
2168
+ });
2169
+ if (retryResponse.status === 402) {
2170
+ throw new PaymentError("Payment was rejected. Check your Solana USDC balance.");
2171
+ }
2172
+ if (!retryResponse.ok) {
2173
+ let errorBody;
2174
+ try {
2175
+ errorBody = await retryResponse.json();
2176
+ } catch {
2177
+ errorBody = { error: "Request failed" };
2178
+ }
2179
+ throw new APIError(`API error after payment: ${retryResponse.status}`, retryResponse.status, sanitizeErrorResponse(errorBody));
2180
+ }
2181
+ const costUsd = parseFloat(details.amount) / 1e6;
2182
+ this.sessionCalls += 1;
2183
+ this.sessionTotalUsd += costUsd;
2184
+ return retryResponse.json();
2185
+ }
2186
+ async fetchWithTimeout(url, options) {
2187
+ const controller = new AbortController();
2188
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
2189
+ try {
2190
+ return await fetch(url, { ...options, signal: controller.signal });
2191
+ } finally {
2192
+ clearTimeout(timeoutId);
2193
+ }
2194
+ }
2195
+ };
2196
+ function solanaClient(options = {}) {
2197
+ return new SolanaLLMClient({ ...options, apiUrl: SOLANA_API_URL });
2198
+ }
2199
+
2200
+ // src/cache.ts
2201
+ import * as fs3 from "fs";
2202
+ import * as path3 from "path";
2203
+ import * as os3 from "os";
2204
+ import * as crypto2 from "crypto";
2205
+ var CACHE_DIR = path3.join(os3.homedir(), ".blockrun", "cache");
2206
+ var DEFAULT_TTL = {
2207
+ "/v1/x/": 3600 * 1e3,
2208
+ "/v1/partner/": 3600 * 1e3,
2209
+ "/v1/pm/": 1800 * 1e3,
2210
+ "/v1/chat/": 0,
2211
+ "/v1/search": 900 * 1e3,
2212
+ "/v1/image": 0,
2213
+ "/v1/models": 86400 * 1e3
2214
+ };
2215
+ function getTtl(endpoint) {
2216
+ for (const [pattern, ttl] of Object.entries(DEFAULT_TTL)) {
2217
+ if (endpoint.includes(pattern)) return ttl;
2218
+ }
2219
+ return 3600 * 1e3;
2220
+ }
2221
+ function cacheKey(endpoint, body) {
2222
+ const keyData = JSON.stringify({ endpoint, body }, Object.keys({ endpoint, body }).sort());
2223
+ return crypto2.createHash("sha256").update(keyData).digest("hex").slice(0, 16);
2224
+ }
2225
+ function cachePath(key) {
2226
+ return path3.join(CACHE_DIR, `${key}.json`);
2227
+ }
2228
+ function getCached(key) {
2229
+ const filePath = cachePath(key);
2230
+ if (!fs3.existsSync(filePath)) return null;
2231
+ try {
2232
+ const raw = fs3.readFileSync(filePath, "utf-8");
2233
+ const entry = JSON.parse(raw);
2234
+ const ttl = entry.ttlMs ?? getTtl(entry.endpoint ?? "");
2235
+ if (ttl <= 0) return null;
2236
+ if (Date.now() - entry.cachedAt > ttl) {
2237
+ try {
2238
+ fs3.unlinkSync(filePath);
2239
+ } catch {
2240
+ }
2241
+ return null;
2242
+ }
2243
+ return entry.response;
2244
+ } catch {
2245
+ return null;
2246
+ }
2247
+ }
2248
+ function getCachedByRequest(endpoint, body) {
2249
+ const ttl = getTtl(endpoint);
2250
+ if (ttl <= 0) return null;
2251
+ const key = cacheKey(endpoint, body);
2252
+ return getCached(key);
2253
+ }
2254
+ function setCache(key, data, ttlMs) {
2255
+ if (ttlMs <= 0) return;
2256
+ try {
2257
+ fs3.mkdirSync(CACHE_DIR, { recursive: true });
2258
+ } catch {
2259
+ }
2260
+ const entry = {
2261
+ cachedAt: Date.now(),
2262
+ response: data,
2263
+ ttlMs
2264
+ };
2265
+ try {
2266
+ fs3.writeFileSync(cachePath(key), JSON.stringify(entry));
2267
+ } catch {
2268
+ }
2269
+ }
2270
+ function saveToCache(endpoint, body, response, costUsd = 0) {
2271
+ const ttl = getTtl(endpoint);
2272
+ if (ttl <= 0) return;
2273
+ try {
2274
+ fs3.mkdirSync(CACHE_DIR, { recursive: true });
2275
+ } catch {
2276
+ }
2277
+ const key = cacheKey(endpoint, body);
2278
+ const entry = {
2279
+ cachedAt: Date.now(),
2280
+ endpoint,
2281
+ body,
2282
+ response,
2283
+ costUsd
2284
+ };
2285
+ try {
2286
+ fs3.writeFileSync(cachePath(key), JSON.stringify(entry));
2287
+ } catch {
2288
+ }
2289
+ }
2290
+ function clearCache() {
2291
+ if (!fs3.existsSync(CACHE_DIR)) return 0;
2292
+ let count = 0;
2293
+ try {
2294
+ const files = fs3.readdirSync(CACHE_DIR);
2295
+ for (const file of files) {
2296
+ if (file.endsWith(".json")) {
2297
+ try {
2298
+ fs3.unlinkSync(path3.join(CACHE_DIR, file));
2299
+ count++;
2300
+ } catch {
2301
+ }
2302
+ }
2303
+ }
2304
+ } catch {
2305
+ }
2306
+ return count;
2307
+ }
2308
+
2309
+ // src/setup.ts
2310
+ function setupAgentWallet(options) {
2311
+ const { address, privateKey, isNew } = getOrCreateWallet();
2312
+ if (isNew && !options?.silent) {
2313
+ console.error(
2314
+ `
2315
+ BlockRun Agent Wallet Created!
2316
+ Address: ${address}
2317
+ Send USDC on Base to get started.
2318
+ `
2319
+ );
2320
+ }
2321
+ return new LLMClient({ privateKey });
2322
+ }
2323
+ async function setupAgentSolanaWallet(options) {
2324
+ const result = await getOrCreateSolanaWallet();
2325
+ if (result.isNew && !options?.silent) {
2326
+ console.error(
2327
+ `
2328
+ BlockRun Solana Agent Wallet Created!
2329
+ Address: ${result.address}
2330
+ Send USDC on Solana to get started.
2331
+ `
2332
+ );
2333
+ }
2334
+ return new SolanaLLMClient({ privateKey: result.privateKey });
2335
+ }
2336
+ async function status() {
2337
+ const client = setupAgentWallet({ silent: true });
2338
+ const address = client.getWalletAddress();
2339
+ const balance = await client.getBalance();
2340
+ console.log(`Wallet: ${address}`);
2341
+ console.log(`Balance: $${balance.toFixed(2)} USDC`);
2342
+ return { address, balance };
2343
+ }
2344
+
2345
+ // src/cost-log.ts
2346
+ import * as fs4 from "fs";
2347
+ import * as path4 from "path";
2348
+ import * as os4 from "os";
2349
+ var DATA_DIR = path4.join(os4.homedir(), ".blockrun", "data");
2350
+ var COST_LOG_FILE = path4.join(DATA_DIR, "costs.jsonl");
2351
+ function logCost(entry) {
2352
+ try {
2353
+ fs4.mkdirSync(DATA_DIR, { recursive: true });
2354
+ } catch {
2355
+ }
2356
+ try {
2357
+ fs4.appendFileSync(COST_LOG_FILE, JSON.stringify(entry) + "\n");
2358
+ } catch {
2359
+ }
2360
+ }
2361
+ function getCostSummary() {
2362
+ if (!fs4.existsSync(COST_LOG_FILE)) {
2363
+ return { totalUsd: 0, calls: 0, byModel: {} };
2364
+ }
2365
+ let totalUsd = 0;
2366
+ let calls = 0;
2367
+ const byModel = {};
2368
+ try {
2369
+ const content = fs4.readFileSync(COST_LOG_FILE, "utf-8").trim();
2370
+ if (!content) return { totalUsd: 0, calls: 0, byModel: {} };
2371
+ for (const line of content.split("\n")) {
2372
+ if (!line) continue;
2373
+ try {
2374
+ const entry = JSON.parse(line);
2375
+ totalUsd += entry.costUsd;
2376
+ calls += 1;
2377
+ byModel[entry.model] = (byModel[entry.model] || 0) + entry.costUsd;
2378
+ } catch {
2379
+ }
2380
+ }
2381
+ } catch {
2382
+ }
2383
+ return { totalUsd, calls, byModel };
2384
+ }
2385
+
2386
+ // src/openai-compat.ts
2387
+ var StreamingResponse = class {
2388
+ reader;
2389
+ decoder;
2390
+ buffer = "";
2391
+ model;
2392
+ id;
2393
+ constructor(response, model) {
2394
+ if (!response.body) {
2395
+ throw new Error("Response body is null");
2396
+ }
2397
+ this.reader = response.body.getReader();
2398
+ this.decoder = new TextDecoder();
2399
+ this.model = model;
2400
+ this.id = `chatcmpl-${Date.now()}`;
2401
+ }
2402
+ async *[Symbol.asyncIterator]() {
2403
+ try {
2404
+ while (true) {
2405
+ const { done, value } = await this.reader.read();
2406
+ if (done) break;
2407
+ this.buffer += this.decoder.decode(value, { stream: true });
2408
+ const lines = this.buffer.split("\n");
2409
+ this.buffer = lines.pop() || "";
2410
+ for (const line of lines) {
2411
+ const trimmed = line.trim();
2412
+ if (!trimmed || !trimmed.startsWith("data: ")) continue;
2413
+ const data = trimmed.slice(6);
2414
+ if (data === "[DONE]") return;
2415
+ try {
2416
+ const parsed = JSON.parse(data);
2417
+ yield this.transformChunk(parsed);
2418
+ } catch {
2419
+ }
2420
+ }
2421
+ }
2422
+ } finally {
2423
+ this.reader.releaseLock();
2424
+ }
2425
+ }
2426
+ transformChunk(data) {
2427
+ const choices = data.choices || [];
2428
+ return {
2429
+ id: data.id || this.id,
2430
+ object: "chat.completion.chunk",
2431
+ created: data.created || Math.floor(Date.now() / 1e3),
2432
+ model: data.model || this.model,
2433
+ choices: choices.map((choice, index) => ({
2434
+ index: choice.index ?? index,
2435
+ delta: {
2436
+ role: choice.delta?.role,
2437
+ content: choice.delta?.content
2438
+ },
2439
+ finish_reason: choice.finish_reason || null
2440
+ }))
2441
+ };
2442
+ }
2443
+ };
2444
+ var ChatCompletions = class {
2445
+ constructor(client, apiUrl, timeout) {
2446
+ this.client = client;
2447
+ this.apiUrl = apiUrl;
2448
+ this.timeout = timeout;
2449
+ }
2450
+ async create(params) {
2451
+ if (params.stream) {
2452
+ return this.createStream(params);
2453
+ }
2454
+ const response = await this.client.chatCompletion(
2455
+ params.model,
2456
+ params.messages,
2457
+ {
2458
+ maxTokens: params.max_tokens,
2459
+ temperature: params.temperature,
2460
+ topP: params.top_p,
2461
+ tools: params.tools,
2462
+ toolChoice: params.tool_choice
2463
+ }
2464
+ );
2465
+ return this.transformResponse(response);
2466
+ }
2467
+ async createStream(params) {
2468
+ const url = `${this.apiUrl}/v1/chat/completions`;
2469
+ const body = {
2470
+ model: params.model,
2471
+ messages: params.messages,
2472
+ max_tokens: params.max_tokens || 1024,
2473
+ temperature: params.temperature,
2474
+ top_p: params.top_p,
2475
+ stream: true
2476
+ };
2477
+ if (params.tools) {
2478
+ body.tools = params.tools;
2479
+ }
2480
+ if (params.tool_choice) {
2481
+ body.tool_choice = params.tool_choice;
2482
+ }
2483
+ const controller = new AbortController();
2484
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
2485
+ try {
2486
+ const response = await fetch(url, {
2487
+ method: "POST",
2488
+ headers: { "Content-Type": "application/json" },
2489
+ body: JSON.stringify(body),
2490
+ signal: controller.signal
2491
+ });
2492
+ if (response.status === 402) {
2493
+ const paymentHeader = response.headers.get("payment-required");
2494
+ if (!paymentHeader) {
2495
+ throw new Error("402 response but no payment requirements found");
2496
+ }
2497
+ throw new Error(
2498
+ "Streaming with automatic payment requires direct wallet access. Please use non-streaming mode or contact support for streaming setup."
2499
+ );
2500
+ }
2501
+ if (!response.ok) {
2502
+ throw new Error(`API error: ${response.status}`);
2503
+ }
2504
+ return new StreamingResponse(response, params.model);
2505
+ } finally {
2506
+ clearTimeout(timeoutId);
2507
+ }
2508
+ }
2509
+ transformResponse(response) {
2510
+ return {
2511
+ id: response.id || `chatcmpl-${Date.now()}`,
2512
+ object: "chat.completion",
2513
+ created: response.created || Math.floor(Date.now() / 1e3),
2514
+ model: response.model,
2515
+ choices: response.choices.map((choice, index) => ({
2516
+ index: choice.index ?? index,
2517
+ message: {
2518
+ role: "assistant",
2519
+ content: choice.message.content,
2520
+ tool_calls: choice.message.tool_calls
2521
+ },
2522
+ finish_reason: choice.finish_reason || "stop"
2523
+ })),
2524
+ usage: response.usage
2525
+ };
2526
+ }
2527
+ };
2528
+ var Chat = class {
2529
+ completions;
2530
+ constructor(client, apiUrl, timeout) {
2531
+ this.completions = new ChatCompletions(client, apiUrl, timeout);
2532
+ }
2533
+ };
2534
+ var OpenAI = class {
2535
+ chat;
2536
+ client;
2537
+ constructor(options = {}) {
2538
+ const privateKey = options.walletKey || options.privateKey;
2539
+ const apiUrl = options.baseURL || "https://blockrun.ai/api";
2540
+ const timeout = options.timeout || 6e4;
2541
+ this.client = new LLMClient({
2542
+ privateKey,
2543
+ apiUrl,
2544
+ timeout
2545
+ });
2546
+ this.chat = new Chat(this.client, apiUrl, timeout);
2547
+ }
2548
+ /**
2549
+ * Get the wallet address being used for payments.
2550
+ */
2551
+ getWalletAddress() {
2552
+ return this.client.getWalletAddress();
2553
+ }
2554
+ };
2555
+
2556
+ // src/anthropic-compat.ts
2557
+ import { privateKeyToAccount as privateKeyToAccount4 } from "viem/accounts";
2558
+ var AnthropicClient = class {
2559
+ _client = null;
2560
+ _clientPromise = null;
2561
+ _privateKey;
2562
+ _account;
2563
+ _apiUrl;
2564
+ _timeout;
2565
+ constructor(options = {}) {
2566
+ const wallet = getOrCreateWallet();
2567
+ const key = options.privateKey ?? wallet.privateKey;
2568
+ validatePrivateKey(key);
2569
+ this._privateKey = key;
2570
+ this._account = privateKeyToAccount4(this._privateKey);
2571
+ const apiUrl = options.apiUrl ?? "https://blockrun.ai/api";
2572
+ validateApiUrl(apiUrl);
2573
+ this._apiUrl = apiUrl.replace(/\/$/, "");
2574
+ this._timeout = options.timeout ?? 6e4;
2575
+ }
2576
+ async _getClient() {
2577
+ if (this._client) return this._client;
2578
+ if (this._clientPromise) return this._clientPromise;
2579
+ this._clientPromise = (async () => {
2580
+ const { default: Anthropic } = await import("@anthropic-ai/sdk");
2581
+ this._client = new Anthropic({
2582
+ baseURL: this._apiUrl,
2583
+ apiKey: "blockrun",
2584
+ fetch: this._x402Fetch.bind(this)
2585
+ });
2586
+ return this._client;
2587
+ })();
2588
+ return this._clientPromise;
2589
+ }
2590
+ async _x402Fetch(input, init) {
2591
+ const controller = new AbortController();
2592
+ const timeoutId = setTimeout(() => controller.abort(), this._timeout);
2593
+ try {
2594
+ const mergedInit = { ...init, signal: controller.signal };
2595
+ let response = await globalThis.fetch(input, mergedInit);
2596
+ if (response.status === 402) {
2597
+ let paymentHeader = response.headers.get("payment-required");
2598
+ if (!paymentHeader) {
2599
+ try {
2600
+ const respBody = await response.json();
2601
+ if (respBody.x402 || respBody.accepts) {
2602
+ paymentHeader = btoa(JSON.stringify(respBody));
2603
+ }
2604
+ } catch {
2605
+ }
2606
+ }
2607
+ if (!paymentHeader) {
2608
+ throw new Error("402 response but no payment requirements found");
2609
+ }
2610
+ const paymentRequired = parsePaymentRequired(paymentHeader);
2611
+ const details = extractPaymentDetails(paymentRequired);
2612
+ const extensions = paymentRequired.extensions;
2613
+ const paymentPayload = await createPaymentPayload(
2614
+ this._privateKey,
2615
+ this._account.address,
2616
+ details.recipient,
2617
+ details.amount,
2618
+ details.network || "eip155:8453",
2619
+ {
2620
+ resourceUrl: validateResourceUrl(
2621
+ details.resource?.url || `${this._apiUrl}/v1/messages`,
2622
+ this._apiUrl
2623
+ ),
2624
+ resourceDescription: details.resource?.description || "BlockRun AI API call",
2625
+ maxTimeoutSeconds: details.maxTimeoutSeconds || 300,
2626
+ extra: details.extra,
2627
+ extensions
2628
+ }
2629
+ );
2630
+ const newHeaders = new Headers(init?.headers);
2631
+ newHeaders.set("PAYMENT-SIGNATURE", paymentPayload);
2632
+ response = await globalThis.fetch(input, {
2633
+ ...init,
2634
+ headers: newHeaders,
2635
+ signal: controller.signal
2636
+ });
2637
+ if (response.status === 402) {
2638
+ throw new Error(
2639
+ "Payment was rejected. Check your wallet balance."
2640
+ );
2641
+ }
2642
+ }
2643
+ return response;
2644
+ } finally {
2645
+ clearTimeout(timeoutId);
2646
+ }
2647
+ }
2648
+ get messages() {
2649
+ const handler = {
2650
+ get: (_target, prop) => {
2651
+ return async (...args) => {
2652
+ const client = await this._getClient();
2653
+ const messages = client.messages;
2654
+ return messages[prop](...args);
2655
+ };
2656
+ }
2657
+ };
2658
+ return new Proxy({}, handler);
2659
+ }
2660
+ getWalletAddress() {
2661
+ return this._account.address;
2662
+ }
2663
+ };
2664
+ export {
2665
+ APIError,
2666
+ AnthropicClient,
2667
+ BASE_CHAIN_ID,
2668
+ BlockrunError,
2669
+ ImageClient,
2670
+ LLMClient,
2671
+ OpenAI,
2672
+ PaymentError,
2673
+ SOLANA_NETWORK,
2674
+ SOLANA_WALLET_FILE as SOLANA_WALLET_FILE_PATH,
2675
+ SolanaLLMClient,
2676
+ USDC_BASE,
2677
+ USDC_BASE_CONTRACT,
2678
+ USDC_SOLANA,
2679
+ WALLET_DIR_PATH,
2680
+ WALLET_FILE_PATH,
2681
+ clearCache,
2682
+ createPaymentPayload,
2683
+ createSolanaPaymentPayload,
2684
+ createSolanaWallet,
2685
+ createWallet,
2686
+ client_default as default,
2687
+ extractPaymentDetails,
2688
+ formatFundingMessageCompact,
2689
+ formatNeedsFundingMessage,
2690
+ formatWalletCreatedMessage,
2691
+ getCached,
2692
+ getCachedByRequest,
2693
+ getCostSummary,
2694
+ getEip681Uri,
2695
+ getOrCreateSolanaWallet,
2696
+ getOrCreateWallet,
2697
+ getPaymentLinks,
2698
+ getWalletAddress,
2699
+ loadSolanaWallet,
2700
+ loadWallet,
2701
+ logCost,
2702
+ parsePaymentRequired,
2703
+ saveSolanaWallet,
2704
+ saveToCache,
2705
+ saveWallet,
2706
+ scanSolanaWallets,
2707
+ scanWallets,
2708
+ setCache,
2709
+ setupAgentSolanaWallet,
2710
+ setupAgentWallet,
2711
+ solanaClient,
2712
+ solanaKeyToBytes,
2713
+ solanaPublicKey,
2714
+ status,
2715
+ testnetClient
2716
+ };