@blockrun/llm 1.4.1 → 1.4.3

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,2727 @@
1
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
+ }) : x)(function(x) {
4
+ if (typeof require !== "undefined") return require.apply(this, arguments);
5
+ throw Error('Dynamic require of "' + x + '" is not supported');
6
+ });
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("@solana/web3.js");
120
+ const { getAssociatedTokenAddress, createTransferCheckedInstruction, getMint } = await import("@solana/spl-token");
121
+ const { Keypair } = await import("@solana/web3.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 || []).map((m) => ({
808
+ id: m.id,
809
+ name: m.name || m.id,
810
+ provider: m.owned_by || "",
811
+ description: m.description || "",
812
+ inputPrice: m.pricing?.input ?? m.pricing?.flat ?? 0,
813
+ outputPrice: m.pricing?.output ?? 0,
814
+ contextWindow: m.context_window || 0,
815
+ maxOutput: m.max_output || 0,
816
+ categories: m.categories || [],
817
+ available: true
818
+ }));
819
+ }
820
+ /**
821
+ * List available image generation models with pricing.
822
+ */
823
+ async listImageModels() {
824
+ const response = await this.fetchWithTimeout(
825
+ `${this.apiUrl}/v1/images/models`,
826
+ { method: "GET" }
827
+ );
828
+ if (!response.ok) {
829
+ throw new APIError(
830
+ `Failed to list image models: ${response.status}`,
831
+ response.status
832
+ );
833
+ }
834
+ const data = await response.json();
835
+ return data.data || [];
836
+ }
837
+ /**
838
+ * List all available models (both LLM and image) with pricing.
839
+ *
840
+ * @returns Array of all models with 'type' field ('llm' or 'image')
841
+ *
842
+ * @example
843
+ * const models = await client.listAllModels();
844
+ * for (const model of models) {
845
+ * if (model.type === 'llm') {
846
+ * console.log(`LLM: ${model.id} - $${model.inputPrice}/M input`);
847
+ * } else {
848
+ * console.log(`Image: ${model.id} - $${model.pricePerImage}/image`);
849
+ * }
850
+ * }
851
+ */
852
+ async listAllModels() {
853
+ const llmModels = await this.listModels();
854
+ for (const model of llmModels) {
855
+ model.type = "llm";
856
+ }
857
+ const imageModels = await this.listImageModels();
858
+ for (const model of imageModels) {
859
+ model.type = "image";
860
+ }
861
+ return [...llmModels, ...imageModels];
862
+ }
863
+ /**
864
+ * Edit an image using img2img.
865
+ *
866
+ * @param prompt - Text description of the desired edit
867
+ * @param image - Base64-encoded image or URL of the source image
868
+ * @param options - Optional edit parameters
869
+ * @returns ImageResponse with edited image URLs
870
+ */
871
+ async imageEdit(prompt, image, options) {
872
+ const body = {
873
+ model: options?.model || "openai/gpt-image-1",
874
+ prompt,
875
+ image,
876
+ size: options?.size || "1024x1024",
877
+ n: options?.n || 1
878
+ };
879
+ if (options?.mask !== void 0) {
880
+ body.mask = options.mask;
881
+ }
882
+ const data = await this.requestWithPaymentRaw("/v1/images/image2image", body);
883
+ return data;
884
+ }
885
+ /**
886
+ * Standalone search (web, X/Twitter, news).
887
+ *
888
+ * @param query - Search query
889
+ * @param options - Optional search parameters
890
+ * @returns SearchResult with summary and citations
891
+ */
892
+ async search(query, options) {
893
+ const body = {
894
+ query,
895
+ max_results: options?.maxResults || 10
896
+ };
897
+ if (options?.sources !== void 0) body.sources = options.sources;
898
+ if (options?.fromDate !== void 0) body.from_date = options.fromDate;
899
+ if (options?.toDate !== void 0) body.to_date = options.toDate;
900
+ const data = await this.requestWithPaymentRaw("/v1/search", body);
901
+ return data;
902
+ }
903
+ /**
904
+ * Get USDC balance on Base network.
905
+ *
906
+ * Automatically detects mainnet vs testnet based on API URL.
907
+ *
908
+ * @returns USDC balance as a float (6 decimal places normalized)
909
+ *
910
+ * @example
911
+ * const balance = await client.getBalance();
912
+ * console.log(`Balance: $${balance.toFixed(2)} USDC`);
913
+ */
914
+ async getBalance() {
915
+ const isTestnet = this.isTestnet();
916
+ const usdcContract = isTestnet ? "0x036CbD53842c5426634e7929541eC2318f3dCF7e" : "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
917
+ 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"];
918
+ const selector = "0x70a08231";
919
+ const paddedAddress = this.account.address.slice(2).toLowerCase().padStart(64, "0");
920
+ const data = selector + paddedAddress;
921
+ const payload = {
922
+ jsonrpc: "2.0",
923
+ method: "eth_call",
924
+ params: [{ to: usdcContract, data }, "latest"],
925
+ id: 1
926
+ };
927
+ let lastError;
928
+ for (const rpc of rpcs) {
929
+ try {
930
+ const response = await fetch(rpc, {
931
+ method: "POST",
932
+ headers: { "Content-Type": "application/json" },
933
+ body: JSON.stringify(payload)
934
+ });
935
+ const result = await response.json();
936
+ const balanceRaw = parseInt(result.result || "0x0", 16);
937
+ return balanceRaw / 1e6;
938
+ } catch (e) {
939
+ lastError = e;
940
+ }
941
+ }
942
+ throw lastError || new Error("All RPCs failed");
943
+ }
944
+ // ============================================================
945
+ // X/Twitter endpoints (powered by AttentionVC)
946
+ // ============================================================
947
+ /**
948
+ * Look up X/Twitter user profiles by username.
949
+ *
950
+ * Powered by AttentionVC. $0.002 per user (min $0.02, max $0.20).
951
+ *
952
+ * @param usernames - Single username or array of usernames (without @)
953
+ */
954
+ async xUserLookup(usernames) {
955
+ const names = Array.isArray(usernames) ? usernames : [usernames];
956
+ const data = await this.requestWithPaymentRaw("/v1/x/users/lookup", { usernames: names });
957
+ return data;
958
+ }
959
+ /**
960
+ * Get followers of an X/Twitter user.
961
+ *
962
+ * Powered by AttentionVC. $0.05 per page (~200 accounts).
963
+ *
964
+ * @param username - X/Twitter username (without @)
965
+ * @param cursor - Pagination cursor from previous response
966
+ */
967
+ async xFollowers(username, cursor) {
968
+ const body = { username };
969
+ if (cursor !== void 0) body.cursor = cursor;
970
+ const data = await this.requestWithPaymentRaw("/v1/x/users/followers", body);
971
+ return data;
972
+ }
973
+ /**
974
+ * Get accounts an X/Twitter user is following.
975
+ *
976
+ * Powered by AttentionVC. $0.05 per page (~200 accounts).
977
+ *
978
+ * @param username - X/Twitter username (without @)
979
+ * @param cursor - Pagination cursor from previous response
980
+ */
981
+ async xFollowings(username, cursor) {
982
+ const body = { username };
983
+ if (cursor !== void 0) body.cursor = cursor;
984
+ const data = await this.requestWithPaymentRaw("/v1/x/users/followings", body);
985
+ return data;
986
+ }
987
+ /**
988
+ * Get detailed profile info for a single X/Twitter user.
989
+ *
990
+ * Powered by AttentionVC. $0.002 per request.
991
+ *
992
+ * @param username - X/Twitter username (without @)
993
+ */
994
+ async xUserInfo(username) {
995
+ const data = await this.requestWithPaymentRaw("/v1/x/users/info", { username });
996
+ return data;
997
+ }
998
+ /**
999
+ * Get verified (blue-check) followers of an X/Twitter user.
1000
+ *
1001
+ * Powered by AttentionVC. $0.048 per page.
1002
+ *
1003
+ * @param userId - X/Twitter user ID (not username)
1004
+ * @param cursor - Pagination cursor from previous response
1005
+ */
1006
+ async xVerifiedFollowers(userId, cursor) {
1007
+ const body = { userId };
1008
+ if (cursor !== void 0) body.cursor = cursor;
1009
+ const data = await this.requestWithPaymentRaw("/v1/x/users/verified-followers", body);
1010
+ return data;
1011
+ }
1012
+ /**
1013
+ * Get tweets posted by an X/Twitter user.
1014
+ *
1015
+ * Powered by AttentionVC. $0.032 per page.
1016
+ *
1017
+ * @param username - X/Twitter username (without @)
1018
+ * @param includeReplies - Include reply tweets (default: false)
1019
+ * @param cursor - Pagination cursor from previous response
1020
+ */
1021
+ async xUserTweets(username, includeReplies = false, cursor) {
1022
+ const body = { username, includeReplies };
1023
+ if (cursor !== void 0) body.cursor = cursor;
1024
+ const data = await this.requestWithPaymentRaw("/v1/x/users/tweets", body);
1025
+ return data;
1026
+ }
1027
+ /**
1028
+ * Get tweets that mention an X/Twitter user.
1029
+ *
1030
+ * Powered by AttentionVC. $0.032 per page.
1031
+ *
1032
+ * @param username - X/Twitter username (without @)
1033
+ * @param sinceTime - Start time filter (ISO8601 or Unix timestamp)
1034
+ * @param untilTime - End time filter (ISO8601 or Unix timestamp)
1035
+ * @param cursor - Pagination cursor from previous response
1036
+ */
1037
+ async xUserMentions(username, sinceTime, untilTime, cursor) {
1038
+ const body = { username };
1039
+ if (sinceTime !== void 0) body.sinceTime = sinceTime;
1040
+ if (untilTime !== void 0) body.untilTime = untilTime;
1041
+ if (cursor !== void 0) body.cursor = cursor;
1042
+ const data = await this.requestWithPaymentRaw("/v1/x/users/mentions", body);
1043
+ return data;
1044
+ }
1045
+ /**
1046
+ * Fetch full tweet data for up to 200 tweet IDs.
1047
+ *
1048
+ * Powered by AttentionVC. $0.16 per batch.
1049
+ *
1050
+ * @param tweetIds - Single tweet ID or array of tweet IDs (max 200)
1051
+ */
1052
+ async xTweetLookup(tweetIds) {
1053
+ const ids = Array.isArray(tweetIds) ? tweetIds : [tweetIds];
1054
+ const data = await this.requestWithPaymentRaw("/v1/x/tweets/lookup", { tweet_ids: ids });
1055
+ return data;
1056
+ }
1057
+ /**
1058
+ * Get replies to a specific tweet.
1059
+ *
1060
+ * Powered by AttentionVC. $0.032 per page.
1061
+ *
1062
+ * @param tweetId - The tweet ID to get replies for
1063
+ * @param queryType - Sort order: 'Latest' or 'Default'
1064
+ * @param cursor - Pagination cursor from previous response
1065
+ */
1066
+ async xTweetReplies(tweetId, queryType = "Latest", cursor) {
1067
+ const body = { tweetId, queryType };
1068
+ if (cursor !== void 0) body.cursor = cursor;
1069
+ const data = await this.requestWithPaymentRaw("/v1/x/tweets/replies", body);
1070
+ return data;
1071
+ }
1072
+ /**
1073
+ * Get the full thread context for a tweet.
1074
+ *
1075
+ * Powered by AttentionVC. $0.032 per page.
1076
+ *
1077
+ * @param tweetId - The tweet ID to get thread for
1078
+ * @param cursor - Pagination cursor from previous response
1079
+ */
1080
+ async xTweetThread(tweetId, cursor) {
1081
+ const body = { tweetId };
1082
+ if (cursor !== void 0) body.cursor = cursor;
1083
+ const data = await this.requestWithPaymentRaw("/v1/x/tweets/thread", body);
1084
+ return data;
1085
+ }
1086
+ /**
1087
+ * Search X/Twitter with advanced query operators.
1088
+ *
1089
+ * Powered by AttentionVC. $0.032 per page.
1090
+ *
1091
+ * @param query - Search query (supports Twitter search operators)
1092
+ * @param queryType - Sort order: 'Latest', 'Top', or 'Default'
1093
+ * @param cursor - Pagination cursor from previous response
1094
+ */
1095
+ async xSearch(query, queryType = "Latest", cursor) {
1096
+ const body = { query, queryType };
1097
+ if (cursor !== void 0) body.cursor = cursor;
1098
+ const data = await this.requestWithPaymentRaw("/v1/x/search", body);
1099
+ return data;
1100
+ }
1101
+ /**
1102
+ * Get current trending topics on X/Twitter.
1103
+ *
1104
+ * Powered by AttentionVC. $0.002 per request.
1105
+ */
1106
+ async xTrending() {
1107
+ const data = await this.requestWithPaymentRaw("/v1/x/trending", {});
1108
+ return data;
1109
+ }
1110
+ /**
1111
+ * Get rising/viral articles from X/Twitter.
1112
+ *
1113
+ * Powered by AttentionVC intelligence layer. $0.05 per request.
1114
+ */
1115
+ async xArticlesRising() {
1116
+ const data = await this.requestWithPaymentRaw("/v1/x/articles/rising", {});
1117
+ return data;
1118
+ }
1119
+ /**
1120
+ * Get author analytics and intelligence metrics for an X/Twitter user.
1121
+ *
1122
+ * Powered by AttentionVC intelligence layer. $0.02 per request.
1123
+ *
1124
+ * @param handle - X/Twitter handle (without @)
1125
+ */
1126
+ async xAuthorAnalytics(handle) {
1127
+ const data = await this.requestWithPaymentRaw("/v1/x/authors", { handle });
1128
+ return data;
1129
+ }
1130
+ /**
1131
+ * Compare two X/Twitter authors side-by-side with intelligence metrics.
1132
+ *
1133
+ * Powered by AttentionVC intelligence layer. $0.05 per request.
1134
+ *
1135
+ * @param handle1 - First X/Twitter handle (without @)
1136
+ * @param handle2 - Second X/Twitter handle (without @)
1137
+ */
1138
+ async xCompareAuthors(handle1, handle2) {
1139
+ const data = await this.requestWithPaymentRaw("/v1/x/compare", { handle1, handle2 });
1140
+ return data;
1141
+ }
1142
+ // ── Prediction Markets (Powered by Predexon) ────────────────────────────
1143
+ /**
1144
+ * Query Predexon prediction market data (GET endpoints).
1145
+ *
1146
+ * Access real-time data from Polymarket, Kalshi, dFlow, and Binance Futures.
1147
+ * Powered by Predexon. $0.001 per request.
1148
+ *
1149
+ * @param path - Endpoint path, e.g. "polymarket/events", "kalshi/markets/12345"
1150
+ * @param params - Query parameters passed to the endpoint
1151
+ *
1152
+ * @example
1153
+ * const events = await client.pm("polymarket/events");
1154
+ * const market = await client.pm("kalshi/markets/KXBTC-25MAR14");
1155
+ * const results = await client.pm("polymarket/search", { q: "bitcoin" });
1156
+ */
1157
+ async pm(path5, params) {
1158
+ return this.getWithPaymentRaw(`/v1/pm/${path5}`, params);
1159
+ }
1160
+ /**
1161
+ * Structured query for Predexon prediction market data (POST endpoints).
1162
+ *
1163
+ * For complex queries that require a JSON body. $0.005 per request.
1164
+ *
1165
+ * @param path - Endpoint path, e.g. "polymarket/query", "kalshi/query"
1166
+ * @param query - JSON body for the structured query
1167
+ *
1168
+ * @example
1169
+ * const data = await client.pmQuery("polymarket/query", { filter: "active", limit: 10 });
1170
+ */
1171
+ async pmQuery(path5, query) {
1172
+ return this.requestWithPaymentRaw(`/v1/pm/${path5}`, query);
1173
+ }
1174
+ /**
1175
+ * Get current session spending.
1176
+ *
1177
+ * @returns Object with totalUsd and calls count
1178
+ *
1179
+ * @example
1180
+ * const spending = client.getSpending();
1181
+ * console.log(`Spent $${spending.totalUsd.toFixed(4)} across ${spending.calls} calls`);
1182
+ */
1183
+ getSpending() {
1184
+ return {
1185
+ totalUsd: this.sessionTotalUsd,
1186
+ calls: this.sessionCalls
1187
+ };
1188
+ }
1189
+ /**
1190
+ * Get the wallet address being used for payments.
1191
+ */
1192
+ getWalletAddress() {
1193
+ return this.account.address;
1194
+ }
1195
+ /**
1196
+ * Check if client is configured for testnet.
1197
+ */
1198
+ isTestnet() {
1199
+ return this.apiUrl.includes("testnet.blockrun.ai");
1200
+ }
1201
+ };
1202
+ function testnetClient(options = {}) {
1203
+ return new LLMClient({
1204
+ ...options,
1205
+ apiUrl: TESTNET_API_URL
1206
+ });
1207
+ }
1208
+ var client_default = LLMClient;
1209
+
1210
+ // src/image.ts
1211
+ import { privateKeyToAccount as privateKeyToAccount2 } from "viem/accounts";
1212
+ var DEFAULT_API_URL2 = "https://blockrun.ai/api";
1213
+ var DEFAULT_MODEL = "google/nano-banana";
1214
+ var DEFAULT_SIZE = "1024x1024";
1215
+ var DEFAULT_TIMEOUT2 = 12e4;
1216
+ var ImageClient = class {
1217
+ account;
1218
+ privateKey;
1219
+ apiUrl;
1220
+ timeout;
1221
+ sessionTotalUsd = 0;
1222
+ sessionCalls = 0;
1223
+ /**
1224
+ * Initialize the BlockRun Image client.
1225
+ *
1226
+ * @param options - Client configuration options
1227
+ */
1228
+ constructor(options = {}) {
1229
+ const envKey = typeof process !== "undefined" && process.env ? process.env.BLOCKRUN_WALLET_KEY || process.env.BASE_CHAIN_WALLET_KEY : void 0;
1230
+ const privateKey = options.privateKey || envKey;
1231
+ if (!privateKey) {
1232
+ throw new Error(
1233
+ "Private key required. Pass privateKey in options or set BLOCKRUN_WALLET_KEY environment variable."
1234
+ );
1235
+ }
1236
+ validatePrivateKey(privateKey);
1237
+ this.privateKey = privateKey;
1238
+ this.account = privateKeyToAccount2(privateKey);
1239
+ const apiUrl = options.apiUrl || DEFAULT_API_URL2;
1240
+ validateApiUrl(apiUrl);
1241
+ this.apiUrl = apiUrl.replace(/\/$/, "");
1242
+ this.timeout = options.timeout || DEFAULT_TIMEOUT2;
1243
+ }
1244
+ /**
1245
+ * Generate an image from a text prompt.
1246
+ *
1247
+ * @param prompt - Text description of the image to generate
1248
+ * @param options - Optional generation parameters
1249
+ * @returns ImageResponse with generated image URLs
1250
+ *
1251
+ * @example
1252
+ * const result = await client.generate('A sunset over mountains');
1253
+ * console.log(result.data[0].url);
1254
+ */
1255
+ async generate(prompt, options) {
1256
+ const body = {
1257
+ model: options?.model || DEFAULT_MODEL,
1258
+ prompt,
1259
+ size: options?.size || DEFAULT_SIZE,
1260
+ n: options?.n || 1
1261
+ };
1262
+ if (options?.quality) {
1263
+ body.quality = options.quality;
1264
+ }
1265
+ return this.requestWithPayment("/v1/images/generations", body);
1266
+ }
1267
+ /**
1268
+ * Edit an image using img2img.
1269
+ *
1270
+ * @param prompt - Text description of the desired edit
1271
+ * @param image - Base64-encoded image or URL of the source image
1272
+ * @param options - Optional edit parameters
1273
+ * @returns ImageResponse with edited image URLs
1274
+ *
1275
+ * @example
1276
+ * const result = await client.edit('Make it a painting', imageBase64);
1277
+ * console.log(result.data[0].url);
1278
+ */
1279
+ async edit(prompt, image, options) {
1280
+ const body = {
1281
+ model: options?.model || "openai/gpt-image-1",
1282
+ prompt,
1283
+ image,
1284
+ size: options?.size || "1024x1024",
1285
+ n: options?.n || 1
1286
+ };
1287
+ if (options?.mask !== void 0) {
1288
+ body.mask = options.mask;
1289
+ }
1290
+ return this.requestWithPayment("/v1/images/image2image", body);
1291
+ }
1292
+ /**
1293
+ * List available image generation models with pricing.
1294
+ */
1295
+ async listImageModels() {
1296
+ const response = await this.fetchWithTimeout(
1297
+ `${this.apiUrl}/v1/images/models`,
1298
+ { method: "GET" }
1299
+ );
1300
+ if (!response.ok) {
1301
+ throw new APIError(
1302
+ `Failed to list image models: ${response.status}`,
1303
+ response.status
1304
+ );
1305
+ }
1306
+ const data = await response.json();
1307
+ return data.data || [];
1308
+ }
1309
+ /**
1310
+ * Make a request with automatic x402 payment handling.
1311
+ */
1312
+ async requestWithPayment(endpoint, body) {
1313
+ const url = `${this.apiUrl}${endpoint}`;
1314
+ const response = await this.fetchWithTimeout(url, {
1315
+ method: "POST",
1316
+ headers: { "Content-Type": "application/json" },
1317
+ body: JSON.stringify(body)
1318
+ });
1319
+ if (response.status === 402) {
1320
+ return this.handlePaymentAndRetry(url, body, response);
1321
+ }
1322
+ if (!response.ok) {
1323
+ let errorBody;
1324
+ try {
1325
+ errorBody = await response.json();
1326
+ } catch {
1327
+ errorBody = { error: "Request failed" };
1328
+ }
1329
+ throw new APIError(
1330
+ `API error: ${response.status}`,
1331
+ response.status,
1332
+ sanitizeErrorResponse(errorBody)
1333
+ );
1334
+ }
1335
+ return response.json();
1336
+ }
1337
+ /**
1338
+ * Handle 402 response: parse requirements, sign payment, retry.
1339
+ */
1340
+ async handlePaymentAndRetry(url, body, response) {
1341
+ let paymentHeader = response.headers.get("payment-required");
1342
+ if (!paymentHeader) {
1343
+ try {
1344
+ const respBody = await response.json();
1345
+ if (respBody.x402 || respBody.accepts) {
1346
+ paymentHeader = btoa(JSON.stringify(respBody));
1347
+ }
1348
+ } catch {
1349
+ }
1350
+ }
1351
+ if (!paymentHeader) {
1352
+ throw new PaymentError("402 response but no payment requirements found");
1353
+ }
1354
+ const paymentRequired = parsePaymentRequired(paymentHeader);
1355
+ const details = extractPaymentDetails(paymentRequired);
1356
+ const extensions = paymentRequired.extensions;
1357
+ const paymentPayload = await createPaymentPayload(
1358
+ this.privateKey,
1359
+ this.account.address,
1360
+ details.recipient,
1361
+ details.amount,
1362
+ details.network || "eip155:8453",
1363
+ {
1364
+ resourceUrl: validateResourceUrl(
1365
+ details.resource?.url || `${this.apiUrl}/v1/images/generations`,
1366
+ this.apiUrl
1367
+ ),
1368
+ resourceDescription: details.resource?.description || "BlockRun Image Generation",
1369
+ maxTimeoutSeconds: details.maxTimeoutSeconds || 300,
1370
+ extra: details.extra,
1371
+ extensions
1372
+ }
1373
+ );
1374
+ const retryResponse = await this.fetchWithTimeout(url, {
1375
+ method: "POST",
1376
+ headers: {
1377
+ "Content-Type": "application/json",
1378
+ "PAYMENT-SIGNATURE": paymentPayload
1379
+ },
1380
+ body: JSON.stringify(body)
1381
+ });
1382
+ if (retryResponse.status === 402) {
1383
+ throw new PaymentError(
1384
+ "Payment was rejected. Check your wallet balance."
1385
+ );
1386
+ }
1387
+ if (!retryResponse.ok) {
1388
+ let errorBody;
1389
+ try {
1390
+ errorBody = await retryResponse.json();
1391
+ } catch {
1392
+ errorBody = { error: "Request failed" };
1393
+ }
1394
+ throw new APIError(
1395
+ `API error after payment: ${retryResponse.status}`,
1396
+ retryResponse.status,
1397
+ sanitizeErrorResponse(errorBody)
1398
+ );
1399
+ }
1400
+ return retryResponse.json();
1401
+ }
1402
+ /**
1403
+ * Fetch with timeout.
1404
+ */
1405
+ async fetchWithTimeout(url, options) {
1406
+ const controller = new AbortController();
1407
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
1408
+ try {
1409
+ const response = await fetch(url, {
1410
+ ...options,
1411
+ signal: controller.signal
1412
+ });
1413
+ return response;
1414
+ } finally {
1415
+ clearTimeout(timeoutId);
1416
+ }
1417
+ }
1418
+ /**
1419
+ * Get the wallet address being used for payments.
1420
+ */
1421
+ getWalletAddress() {
1422
+ return this.account.address;
1423
+ }
1424
+ /**
1425
+ * Get session spending information.
1426
+ */
1427
+ getSpending() {
1428
+ return {
1429
+ totalUsd: this.sessionTotalUsd,
1430
+ calls: this.sessionCalls
1431
+ };
1432
+ }
1433
+ };
1434
+
1435
+ // src/wallet.ts
1436
+ import { privateKeyToAccount as privateKeyToAccount3, generatePrivateKey } from "viem/accounts";
1437
+ import * as fs from "fs";
1438
+ import * as path from "path";
1439
+ import * as os from "os";
1440
+ var USDC_BASE_CONTRACT = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
1441
+ var BASE_CHAIN_ID2 = "8453";
1442
+ var WALLET_DIR = path.join(os.homedir(), ".blockrun");
1443
+ var WALLET_FILE = path.join(WALLET_DIR, ".session");
1444
+ function createWallet() {
1445
+ const privateKey = generatePrivateKey();
1446
+ const account = privateKeyToAccount3(privateKey);
1447
+ return {
1448
+ address: account.address,
1449
+ privateKey
1450
+ };
1451
+ }
1452
+ function saveWallet(privateKey) {
1453
+ if (!fs.existsSync(WALLET_DIR)) {
1454
+ fs.mkdirSync(WALLET_DIR, { recursive: true });
1455
+ }
1456
+ fs.writeFileSync(WALLET_FILE, privateKey, { mode: 384 });
1457
+ return WALLET_FILE;
1458
+ }
1459
+ function scanWallets() {
1460
+ const home = os.homedir();
1461
+ const results = [];
1462
+ try {
1463
+ const entries = fs.readdirSync(home, { withFileTypes: true });
1464
+ for (const entry of entries) {
1465
+ if (!entry.name.startsWith(".") || !entry.isDirectory()) continue;
1466
+ const walletFile = path.join(home, entry.name, "wallet.json");
1467
+ if (!fs.existsSync(walletFile)) continue;
1468
+ try {
1469
+ const data = JSON.parse(fs.readFileSync(walletFile, "utf-8"));
1470
+ const pk = data.privateKey || "";
1471
+ const addr = data.address || "";
1472
+ if (pk && addr) {
1473
+ const mtime = fs.statSync(walletFile).mtimeMs;
1474
+ results.push({ mtime, privateKey: pk, address: addr });
1475
+ }
1476
+ } catch {
1477
+ continue;
1478
+ }
1479
+ }
1480
+ } catch {
1481
+ }
1482
+ results.sort((a, b) => b.mtime - a.mtime);
1483
+ return results.map(({ privateKey, address }) => ({ privateKey, address }));
1484
+ }
1485
+ function loadWallet() {
1486
+ const wallets = scanWallets();
1487
+ if (wallets.length > 0) return wallets[0].privateKey;
1488
+ if (fs.existsSync(WALLET_FILE)) {
1489
+ const key = fs.readFileSync(WALLET_FILE, "utf-8").trim();
1490
+ if (key) return key;
1491
+ }
1492
+ const legacyFile = path.join(WALLET_DIR, "wallet.key");
1493
+ if (fs.existsSync(legacyFile)) {
1494
+ const key = fs.readFileSync(legacyFile, "utf-8").trim();
1495
+ if (key) return key;
1496
+ }
1497
+ return null;
1498
+ }
1499
+ function getOrCreateWallet() {
1500
+ const envKey = typeof process !== "undefined" && process.env ? process.env.BLOCKRUN_WALLET_KEY || process.env.BASE_CHAIN_WALLET_KEY : void 0;
1501
+ if (envKey) {
1502
+ const account = privateKeyToAccount3(envKey);
1503
+ return { address: account.address, privateKey: envKey, isNew: false };
1504
+ }
1505
+ const fileKey = loadWallet();
1506
+ if (fileKey) {
1507
+ const account = privateKeyToAccount3(fileKey);
1508
+ return { address: account.address, privateKey: fileKey, isNew: false };
1509
+ }
1510
+ const { address, privateKey } = createWallet();
1511
+ saveWallet(privateKey);
1512
+ return { address, privateKey, isNew: true };
1513
+ }
1514
+ function getWalletAddress() {
1515
+ const envKey = typeof process !== "undefined" && process.env ? process.env.BLOCKRUN_WALLET_KEY || process.env.BASE_CHAIN_WALLET_KEY : void 0;
1516
+ if (envKey) {
1517
+ return privateKeyToAccount3(envKey).address;
1518
+ }
1519
+ const fileKey = loadWallet();
1520
+ if (fileKey) {
1521
+ return privateKeyToAccount3(fileKey).address;
1522
+ }
1523
+ return null;
1524
+ }
1525
+ function getEip681Uri(address, amountUsdc = 1) {
1526
+ const amountWei = Math.floor(amountUsdc * 1e6);
1527
+ return `ethereum:${USDC_BASE_CONTRACT}@${BASE_CHAIN_ID2}/transfer?address=${address}&uint256=${amountWei}`;
1528
+ }
1529
+ function getPaymentLinks(address) {
1530
+ return {
1531
+ basescan: `https://basescan.org/address/${address}`,
1532
+ walletLink: `ethereum:${USDC_BASE_CONTRACT}@${BASE_CHAIN_ID2}/transfer?address=${address}`,
1533
+ ethereum: `ethereum:${address}@${BASE_CHAIN_ID2}`,
1534
+ blockrun: `https://blockrun.ai/fund?address=${address}`
1535
+ };
1536
+ }
1537
+ function formatWalletCreatedMessage(address) {
1538
+ const links = getPaymentLinks(address);
1539
+ return `
1540
+ I'm your BlockRun Agent! I can access GPT-4, Grok, image generation, and more.
1541
+
1542
+ Please send $1-5 USDC on Base to start:
1543
+
1544
+ ${address}
1545
+
1546
+ What is Base? Base is Coinbase's blockchain network.
1547
+ You can buy USDC on Coinbase and send it directly to me.
1548
+
1549
+ What $1 USDC gets you:
1550
+ - ~1,000 GPT-4o calls
1551
+ - ~100 image generations
1552
+ - ~10,000 DeepSeek calls
1553
+
1554
+ Quick links:
1555
+ - Check my balance: ${links.basescan}
1556
+ - Get USDC: https://www.coinbase.com or https://bridge.base.org
1557
+
1558
+ Questions? care@blockrun.ai | Issues? github.com/BlockRunAI/blockrun-llm-ts/issues
1559
+
1560
+ Key stored securely in ~/.blockrun/
1561
+ Your private key never leaves your machine - only signatures are sent.
1562
+ `;
1563
+ }
1564
+ function formatNeedsFundingMessage(address) {
1565
+ const links = getPaymentLinks(address);
1566
+ return `
1567
+ I've run out of funds! Please send more USDC on Base to continue helping you.
1568
+
1569
+ Send to my address:
1570
+ ${address}
1571
+
1572
+ Check my balance: ${links.basescan}
1573
+
1574
+ What $1 USDC gets you: ~1,000 GPT-4o calls or ~100 images.
1575
+ Questions? care@blockrun.ai | Issues? github.com/BlockRunAI/blockrun-llm-ts/issues
1576
+
1577
+ Your private key never leaves your machine - only signatures are sent.
1578
+ `;
1579
+ }
1580
+ function formatFundingMessageCompact(address) {
1581
+ const links = getPaymentLinks(address);
1582
+ return `I need a little top-up to keep helping you! Send USDC on Base to: ${address}
1583
+ Check my balance: ${links.basescan}`;
1584
+ }
1585
+ var WALLET_FILE_PATH = WALLET_FILE;
1586
+ var WALLET_DIR_PATH = WALLET_DIR;
1587
+
1588
+ // src/solana-wallet.ts
1589
+ import * as fs2 from "fs";
1590
+ import * as path2 from "path";
1591
+ import * as os2 from "os";
1592
+ var WALLET_DIR2 = path2.join(os2.homedir(), ".blockrun");
1593
+ var SOLANA_WALLET_FILE = path2.join(WALLET_DIR2, ".solana-session");
1594
+ function createSolanaWallet() {
1595
+ const { Keypair } = __require("@solana/web3.js");
1596
+ const bs58 = __require("bs58");
1597
+ const keypair = Keypair.generate();
1598
+ return {
1599
+ address: keypair.publicKey.toBase58(),
1600
+ privateKey: bs58.default?.encode(keypair.secretKey) ?? bs58.encode(keypair.secretKey)
1601
+ };
1602
+ }
1603
+ async function solanaKeyToBytes(privateKey) {
1604
+ try {
1605
+ const bs58 = await import("bs58");
1606
+ const bytes = (bs58.default ?? bs58).decode(privateKey);
1607
+ if (bytes.length !== 64) {
1608
+ throw new Error(`Invalid Solana key length: expected 64 bytes, got ${bytes.length}`);
1609
+ }
1610
+ return bytes;
1611
+ } catch (err) {
1612
+ const msg = err instanceof Error ? err.message : String(err);
1613
+ throw new Error(`Invalid Solana private key: ${msg}`);
1614
+ }
1615
+ }
1616
+ async function solanaPublicKey(privateKey) {
1617
+ const { Keypair } = await import("@solana/web3.js");
1618
+ const bytes = await solanaKeyToBytes(privateKey);
1619
+ return Keypair.fromSecretKey(bytes).publicKey.toBase58();
1620
+ }
1621
+ function saveSolanaWallet(privateKey) {
1622
+ if (!fs2.existsSync(WALLET_DIR2)) fs2.mkdirSync(WALLET_DIR2, { recursive: true });
1623
+ fs2.writeFileSync(SOLANA_WALLET_FILE, privateKey, { mode: 384 });
1624
+ return SOLANA_WALLET_FILE;
1625
+ }
1626
+ function scanSolanaWallets() {
1627
+ const home = os2.homedir();
1628
+ const results = [];
1629
+ try {
1630
+ const entries = fs2.readdirSync(home, { withFileTypes: true });
1631
+ for (const entry of entries) {
1632
+ if (!entry.name.startsWith(".") || !entry.isDirectory()) continue;
1633
+ const solanaWalletFile = path2.join(home, entry.name, "solana-wallet.json");
1634
+ if (fs2.existsSync(solanaWalletFile)) {
1635
+ try {
1636
+ const data = JSON.parse(fs2.readFileSync(solanaWalletFile, "utf-8"));
1637
+ const pk = data.privateKey || "";
1638
+ const addr = data.address || "";
1639
+ if (pk && addr) {
1640
+ const mtime = fs2.statSync(solanaWalletFile).mtimeMs;
1641
+ results.push({ mtime, secretKey: pk, publicKey: addr });
1642
+ }
1643
+ } catch {
1644
+ }
1645
+ }
1646
+ if (entry.name === ".brcc") {
1647
+ const brccWalletFile = path2.join(home, entry.name, "wallet.json");
1648
+ if (fs2.existsSync(brccWalletFile)) {
1649
+ try {
1650
+ const data = JSON.parse(fs2.readFileSync(brccWalletFile, "utf-8"));
1651
+ const pk = data.privateKey || "";
1652
+ const addr = data.address || "";
1653
+ if (pk && addr) {
1654
+ const mtime = fs2.statSync(brccWalletFile).mtimeMs;
1655
+ results.push({ mtime, secretKey: pk, publicKey: addr });
1656
+ }
1657
+ } catch {
1658
+ }
1659
+ }
1660
+ }
1661
+ }
1662
+ } catch {
1663
+ }
1664
+ results.sort((a, b) => b.mtime - a.mtime);
1665
+ return results.map(({ secretKey, publicKey }) => ({ secretKey, publicKey }));
1666
+ }
1667
+ function loadSolanaWallet() {
1668
+ const wallets = scanSolanaWallets();
1669
+ if (wallets.length > 0) return wallets[0].secretKey;
1670
+ if (fs2.existsSync(SOLANA_WALLET_FILE)) {
1671
+ const key = fs2.readFileSync(SOLANA_WALLET_FILE, "utf-8").trim();
1672
+ if (key) return key;
1673
+ }
1674
+ return null;
1675
+ }
1676
+ async function getOrCreateSolanaWallet() {
1677
+ const envKey = typeof process !== "undefined" && process.env ? process.env.SOLANA_WALLET_KEY : void 0;
1678
+ if (envKey) {
1679
+ const address2 = await solanaPublicKey(envKey);
1680
+ return { privateKey: envKey, address: address2, isNew: false };
1681
+ }
1682
+ const wallets = scanSolanaWallets();
1683
+ if (wallets.length > 0) {
1684
+ return { privateKey: wallets[0].secretKey, address: wallets[0].publicKey, isNew: false };
1685
+ }
1686
+ if (fs2.existsSync(SOLANA_WALLET_FILE)) {
1687
+ const fileKey = fs2.readFileSync(SOLANA_WALLET_FILE, "utf-8").trim();
1688
+ if (fileKey) {
1689
+ const address2 = await solanaPublicKey(fileKey);
1690
+ return { privateKey: fileKey, address: address2, isNew: false };
1691
+ }
1692
+ }
1693
+ const { address, privateKey } = createSolanaWallet();
1694
+ saveSolanaWallet(privateKey);
1695
+ return { address, privateKey, isNew: true };
1696
+ }
1697
+
1698
+ // src/solana-client.ts
1699
+ var SOLANA_API_URL = "https://sol.blockrun.ai/api";
1700
+ var DEFAULT_MAX_TOKENS2 = 1024;
1701
+ var DEFAULT_TIMEOUT3 = 6e4;
1702
+ var SDK_VERSION2 = "0.3.0";
1703
+ var USER_AGENT2 = `blockrun-ts/${SDK_VERSION2}`;
1704
+ var SolanaLLMClient = class {
1705
+ static SOLANA_API_URL = SOLANA_API_URL;
1706
+ privateKey;
1707
+ apiUrl;
1708
+ rpcUrl;
1709
+ timeout;
1710
+ sessionTotalUsd = 0;
1711
+ sessionCalls = 0;
1712
+ addressCache = null;
1713
+ constructor(options = {}) {
1714
+ const envKey = typeof process !== "undefined" && process.env ? process.env.SOLANA_WALLET_KEY : void 0;
1715
+ const privateKey = options.privateKey || envKey;
1716
+ if (!privateKey) {
1717
+ throw new Error(
1718
+ "Private key required. Pass privateKey in options or set SOLANA_WALLET_KEY environment variable."
1719
+ );
1720
+ }
1721
+ this.privateKey = privateKey;
1722
+ const apiUrl = options.apiUrl || SOLANA_API_URL;
1723
+ validateApiUrl(apiUrl);
1724
+ this.apiUrl = apiUrl.replace(/\/$/, "");
1725
+ this.rpcUrl = options.rpcUrl || "https://api.mainnet-beta.solana.com";
1726
+ this.timeout = options.timeout || DEFAULT_TIMEOUT3;
1727
+ }
1728
+ /** Get Solana wallet address (public key in base58). */
1729
+ async getWalletAddress() {
1730
+ if (!this.addressCache) {
1731
+ this.addressCache = await solanaPublicKey(this.privateKey);
1732
+ }
1733
+ return this.addressCache;
1734
+ }
1735
+ /** Simple 1-line chat. */
1736
+ async chat(model, prompt, options) {
1737
+ const messages = [];
1738
+ if (options?.system) messages.push({ role: "system", content: options.system });
1739
+ messages.push({ role: "user", content: prompt });
1740
+ const result = await this.chatCompletion(model, messages, {
1741
+ maxTokens: options?.maxTokens,
1742
+ temperature: options?.temperature,
1743
+ topP: options?.topP,
1744
+ search: options?.search,
1745
+ searchParameters: options?.searchParameters
1746
+ });
1747
+ return result.choices[0].message.content || "";
1748
+ }
1749
+ /** Full chat completion (OpenAI-compatible). */
1750
+ async chatCompletion(model, messages, options) {
1751
+ const body = {
1752
+ model,
1753
+ messages,
1754
+ max_tokens: options?.maxTokens || DEFAULT_MAX_TOKENS2
1755
+ };
1756
+ if (options?.temperature !== void 0) body.temperature = options.temperature;
1757
+ if (options?.topP !== void 0) body.top_p = options.topP;
1758
+ if (options?.searchParameters !== void 0) body.search_parameters = options.searchParameters;
1759
+ else if (options?.search === true) body.search_parameters = { mode: "on" };
1760
+ if (options?.tools !== void 0) body.tools = options.tools;
1761
+ if (options?.toolChoice !== void 0) body.tool_choice = options.toolChoice;
1762
+ return this.requestWithPayment("/v1/chat/completions", body);
1763
+ }
1764
+ /** List available models. */
1765
+ async listModels() {
1766
+ const response = await this.fetchWithTimeout(`${this.apiUrl}/v1/models`, { method: "GET" });
1767
+ if (!response.ok) {
1768
+ throw new APIError(`Failed to list models: ${response.status}`, response.status);
1769
+ }
1770
+ const data = await response.json();
1771
+ return data.data || [];
1772
+ }
1773
+ /**
1774
+ * Get Solana USDC balance.
1775
+ *
1776
+ * @returns USDC balance as a float
1777
+ */
1778
+ async getBalance() {
1779
+ const usdc_mint = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
1780
+ const address = await this.getWalletAddress();
1781
+ try {
1782
+ const response = await fetch(this.rpcUrl, {
1783
+ method: "POST",
1784
+ headers: { "Content-Type": "application/json" },
1785
+ body: JSON.stringify({
1786
+ jsonrpc: "2.0",
1787
+ id: 1,
1788
+ method: "getTokenAccountsByOwner",
1789
+ params: [address, { mint: usdc_mint }, { encoding: "jsonParsed" }]
1790
+ })
1791
+ });
1792
+ const data = await response.json();
1793
+ const accounts = data.result?.value || [];
1794
+ if (!accounts.length) return 0;
1795
+ let total = 0;
1796
+ for (const acct of accounts) {
1797
+ total += acct.account?.data?.parsed?.info?.tokenAmount?.uiAmount || 0;
1798
+ }
1799
+ return total;
1800
+ } catch {
1801
+ return 0;
1802
+ }
1803
+ }
1804
+ /** Edit an image using img2img (Solana payment). */
1805
+ async imageEdit(prompt, image, options) {
1806
+ const body = {
1807
+ model: options?.model || "openai/gpt-image-1",
1808
+ prompt,
1809
+ image,
1810
+ size: options?.size || "1024x1024",
1811
+ n: options?.n || 1
1812
+ };
1813
+ if (options?.mask !== void 0) body.mask = options.mask;
1814
+ const data = await this.requestWithPaymentRaw("/v1/images/image2image", body);
1815
+ return data;
1816
+ }
1817
+ /** Standalone search (Solana payment). */
1818
+ async search(query, options) {
1819
+ const body = { query, max_results: options?.maxResults || 10 };
1820
+ if (options?.sources !== void 0) body.sources = options.sources;
1821
+ if (options?.fromDate !== void 0) body.from_date = options.fromDate;
1822
+ if (options?.toDate !== void 0) body.to_date = options.toDate;
1823
+ const data = await this.requestWithPaymentRaw("/v1/search", body);
1824
+ return data;
1825
+ }
1826
+ // ============================================================
1827
+ // X/Twitter endpoints (powered by AttentionVC)
1828
+ // ============================================================
1829
+ async xUserLookup(usernames) {
1830
+ const names = Array.isArray(usernames) ? usernames : [usernames];
1831
+ const data = await this.requestWithPaymentRaw("/v1/x/users/lookup", { usernames: names });
1832
+ return data;
1833
+ }
1834
+ async xFollowers(username, cursor) {
1835
+ const body = { username };
1836
+ if (cursor !== void 0) body.cursor = cursor;
1837
+ const data = await this.requestWithPaymentRaw("/v1/x/users/followers", body);
1838
+ return data;
1839
+ }
1840
+ async xFollowings(username, cursor) {
1841
+ const body = { username };
1842
+ if (cursor !== void 0) body.cursor = cursor;
1843
+ const data = await this.requestWithPaymentRaw("/v1/x/users/followings", body);
1844
+ return data;
1845
+ }
1846
+ async xUserInfo(username) {
1847
+ const data = await this.requestWithPaymentRaw("/v1/x/users/info", { username });
1848
+ return data;
1849
+ }
1850
+ async xVerifiedFollowers(userId, cursor) {
1851
+ const body = { userId };
1852
+ if (cursor !== void 0) body.cursor = cursor;
1853
+ const data = await this.requestWithPaymentRaw("/v1/x/users/verified-followers", body);
1854
+ return data;
1855
+ }
1856
+ async xUserTweets(username, includeReplies = false, cursor) {
1857
+ const body = { username, includeReplies };
1858
+ if (cursor !== void 0) body.cursor = cursor;
1859
+ const data = await this.requestWithPaymentRaw("/v1/x/users/tweets", body);
1860
+ return data;
1861
+ }
1862
+ async xUserMentions(username, sinceTime, untilTime, cursor) {
1863
+ const body = { username };
1864
+ if (sinceTime !== void 0) body.sinceTime = sinceTime;
1865
+ if (untilTime !== void 0) body.untilTime = untilTime;
1866
+ if (cursor !== void 0) body.cursor = cursor;
1867
+ const data = await this.requestWithPaymentRaw("/v1/x/users/mentions", body);
1868
+ return data;
1869
+ }
1870
+ async xTweetLookup(tweetIds) {
1871
+ const ids = Array.isArray(tweetIds) ? tweetIds : [tweetIds];
1872
+ const data = await this.requestWithPaymentRaw("/v1/x/tweets/lookup", { tweet_ids: ids });
1873
+ return data;
1874
+ }
1875
+ async xTweetReplies(tweetId, queryType = "Latest", cursor) {
1876
+ const body = { tweetId, queryType };
1877
+ if (cursor !== void 0) body.cursor = cursor;
1878
+ const data = await this.requestWithPaymentRaw("/v1/x/tweets/replies", body);
1879
+ return data;
1880
+ }
1881
+ async xTweetThread(tweetId, cursor) {
1882
+ const body = { tweetId };
1883
+ if (cursor !== void 0) body.cursor = cursor;
1884
+ const data = await this.requestWithPaymentRaw("/v1/x/tweets/thread", body);
1885
+ return data;
1886
+ }
1887
+ async xSearch(query, queryType = "Latest", cursor) {
1888
+ const body = { query, queryType };
1889
+ if (cursor !== void 0) body.cursor = cursor;
1890
+ const data = await this.requestWithPaymentRaw("/v1/x/search", body);
1891
+ return data;
1892
+ }
1893
+ async xTrending() {
1894
+ const data = await this.requestWithPaymentRaw("/v1/x/trending", {});
1895
+ return data;
1896
+ }
1897
+ async xArticlesRising() {
1898
+ const data = await this.requestWithPaymentRaw("/v1/x/articles/rising", {});
1899
+ return data;
1900
+ }
1901
+ async xAuthorAnalytics(handle) {
1902
+ const data = await this.requestWithPaymentRaw("/v1/x/authors", { handle });
1903
+ return data;
1904
+ }
1905
+ async xCompareAuthors(handle1, handle2) {
1906
+ const data = await this.requestWithPaymentRaw("/v1/x/compare", { handle1, handle2 });
1907
+ return data;
1908
+ }
1909
+ // ── Prediction Markets (Powered by Predexon) ────────────────────────────
1910
+ async pm(path5, params) {
1911
+ return this.getWithPaymentRaw(`/v1/pm/${path5}`, params);
1912
+ }
1913
+ async pmQuery(path5, query) {
1914
+ return this.requestWithPaymentRaw(`/v1/pm/${path5}`, query);
1915
+ }
1916
+ /** Get session spending. */
1917
+ getSpending() {
1918
+ return { totalUsd: this.sessionTotalUsd, calls: this.sessionCalls };
1919
+ }
1920
+ /** True if using sol.blockrun.ai. */
1921
+ isSolana() {
1922
+ return this.apiUrl.includes("sol.blockrun.ai");
1923
+ }
1924
+ async requestWithPayment(endpoint, body) {
1925
+ const url = `${this.apiUrl}${endpoint}`;
1926
+ const response = await this.fetchWithTimeout(url, {
1927
+ method: "POST",
1928
+ headers: { "Content-Type": "application/json", "User-Agent": USER_AGENT2 },
1929
+ body: JSON.stringify(body)
1930
+ });
1931
+ if (response.status === 402) {
1932
+ return this.handlePaymentAndRetry(url, body, response);
1933
+ }
1934
+ if (!response.ok) {
1935
+ let errorBody;
1936
+ try {
1937
+ errorBody = await response.json();
1938
+ } catch {
1939
+ errorBody = { error: "Request failed" };
1940
+ }
1941
+ throw new APIError(`API error: ${response.status}`, response.status, sanitizeErrorResponse(errorBody));
1942
+ }
1943
+ return response.json();
1944
+ }
1945
+ async handlePaymentAndRetry(url, body, response) {
1946
+ let paymentHeader = response.headers.get("payment-required");
1947
+ if (!paymentHeader) {
1948
+ try {
1949
+ const respBody = await response.json();
1950
+ if (respBody.accepts || respBody.x402Version) {
1951
+ paymentHeader = btoa(JSON.stringify(respBody));
1952
+ }
1953
+ } catch {
1954
+ }
1955
+ }
1956
+ if (!paymentHeader) {
1957
+ throw new PaymentError("402 response but no payment requirements found");
1958
+ }
1959
+ const paymentRequired = parsePaymentRequired(paymentHeader);
1960
+ const details = extractPaymentDetails(paymentRequired, SOLANA_NETWORK);
1961
+ if (!details.network?.startsWith("solana:")) {
1962
+ throw new PaymentError(
1963
+ `Expected Solana payment network, got: ${details.network}. Use LLMClient for Base payments.`
1964
+ );
1965
+ }
1966
+ const feePayer = details.extra?.feePayer;
1967
+ if (!feePayer) throw new PaymentError("Missing feePayer in 402 extra field");
1968
+ const fromAddress = await this.getWalletAddress();
1969
+ const secretKey = await solanaKeyToBytes(this.privateKey);
1970
+ const extensions = paymentRequired.extensions;
1971
+ const paymentPayload = await createSolanaPaymentPayload(
1972
+ secretKey,
1973
+ fromAddress,
1974
+ details.recipient,
1975
+ details.amount,
1976
+ feePayer,
1977
+ {
1978
+ resourceUrl: validateResourceUrl(
1979
+ details.resource?.url || `${this.apiUrl}/v1/chat/completions`,
1980
+ this.apiUrl
1981
+ ),
1982
+ resourceDescription: details.resource?.description || "BlockRun Solana AI API call",
1983
+ maxTimeoutSeconds: details.maxTimeoutSeconds || 300,
1984
+ extra: details.extra,
1985
+ extensions,
1986
+ rpcUrl: this.rpcUrl
1987
+ }
1988
+ );
1989
+ const retryResponse = await this.fetchWithTimeout(url, {
1990
+ method: "POST",
1991
+ headers: {
1992
+ "Content-Type": "application/json",
1993
+ "User-Agent": USER_AGENT2,
1994
+ "PAYMENT-SIGNATURE": paymentPayload
1995
+ },
1996
+ body: JSON.stringify(body)
1997
+ });
1998
+ if (retryResponse.status === 402) {
1999
+ throw new PaymentError("Payment was rejected. Check your Solana USDC balance.");
2000
+ }
2001
+ if (!retryResponse.ok) {
2002
+ let errorBody;
2003
+ try {
2004
+ errorBody = await retryResponse.json();
2005
+ } catch {
2006
+ errorBody = { error: "Request failed" };
2007
+ }
2008
+ throw new APIError(`API error after payment: ${retryResponse.status}`, retryResponse.status, sanitizeErrorResponse(errorBody));
2009
+ }
2010
+ const costUsd = parseFloat(details.amount) / 1e6;
2011
+ this.sessionCalls += 1;
2012
+ this.sessionTotalUsd += costUsd;
2013
+ return retryResponse.json();
2014
+ }
2015
+ async requestWithPaymentRaw(endpoint, body) {
2016
+ const url = `${this.apiUrl}${endpoint}`;
2017
+ const response = await this.fetchWithTimeout(url, {
2018
+ method: "POST",
2019
+ headers: { "Content-Type": "application/json", "User-Agent": USER_AGENT2 },
2020
+ body: JSON.stringify(body)
2021
+ });
2022
+ if (response.status === 402) {
2023
+ return this.handlePaymentAndRetryRaw(url, body, response);
2024
+ }
2025
+ if (!response.ok) {
2026
+ let errorBody;
2027
+ try {
2028
+ errorBody = await response.json();
2029
+ } catch {
2030
+ errorBody = { error: "Request failed" };
2031
+ }
2032
+ throw new APIError(`API error: ${response.status}`, response.status, sanitizeErrorResponse(errorBody));
2033
+ }
2034
+ return response.json();
2035
+ }
2036
+ async handlePaymentAndRetryRaw(url, body, response) {
2037
+ let paymentHeader = response.headers.get("payment-required");
2038
+ if (!paymentHeader) {
2039
+ try {
2040
+ const respBody = await response.json();
2041
+ if (respBody.accepts || respBody.x402Version) {
2042
+ paymentHeader = btoa(JSON.stringify(respBody));
2043
+ }
2044
+ } catch {
2045
+ }
2046
+ }
2047
+ if (!paymentHeader) {
2048
+ throw new PaymentError("402 response but no payment requirements found");
2049
+ }
2050
+ const paymentRequired = parsePaymentRequired(paymentHeader);
2051
+ const details = extractPaymentDetails(paymentRequired, SOLANA_NETWORK);
2052
+ if (!details.network?.startsWith("solana:")) {
2053
+ throw new PaymentError(
2054
+ `Expected Solana payment network, got: ${details.network}. Use LLMClient for Base payments.`
2055
+ );
2056
+ }
2057
+ const feePayer = details.extra?.feePayer;
2058
+ if (!feePayer) throw new PaymentError("Missing feePayer in 402 extra field");
2059
+ const fromAddress = await this.getWalletAddress();
2060
+ const secretKey = await solanaKeyToBytes(this.privateKey);
2061
+ const extensions = paymentRequired.extensions;
2062
+ const paymentPayload = await createSolanaPaymentPayload(
2063
+ secretKey,
2064
+ fromAddress,
2065
+ details.recipient,
2066
+ details.amount,
2067
+ feePayer,
2068
+ {
2069
+ resourceUrl: validateResourceUrl(
2070
+ details.resource?.url || url,
2071
+ this.apiUrl
2072
+ ),
2073
+ resourceDescription: details.resource?.description || "BlockRun Solana AI API call",
2074
+ maxTimeoutSeconds: details.maxTimeoutSeconds || 300,
2075
+ extra: details.extra,
2076
+ extensions,
2077
+ rpcUrl: this.rpcUrl
2078
+ }
2079
+ );
2080
+ const retryResponse = await this.fetchWithTimeout(url, {
2081
+ method: "POST",
2082
+ headers: {
2083
+ "Content-Type": "application/json",
2084
+ "User-Agent": USER_AGENT2,
2085
+ "PAYMENT-SIGNATURE": paymentPayload
2086
+ },
2087
+ body: JSON.stringify(body)
2088
+ });
2089
+ if (retryResponse.status === 402) {
2090
+ throw new PaymentError("Payment was rejected. Check your Solana USDC balance.");
2091
+ }
2092
+ if (!retryResponse.ok) {
2093
+ let errorBody;
2094
+ try {
2095
+ errorBody = await retryResponse.json();
2096
+ } catch {
2097
+ errorBody = { error: "Request failed" };
2098
+ }
2099
+ throw new APIError(`API error after payment: ${retryResponse.status}`, retryResponse.status, sanitizeErrorResponse(errorBody));
2100
+ }
2101
+ const costUsd = parseFloat(details.amount) / 1e6;
2102
+ this.sessionCalls += 1;
2103
+ this.sessionTotalUsd += costUsd;
2104
+ return retryResponse.json();
2105
+ }
2106
+ async getWithPaymentRaw(endpoint, params) {
2107
+ const query = params ? "?" + new URLSearchParams(params).toString() : "";
2108
+ const url = `${this.apiUrl}${endpoint}${query}`;
2109
+ const response = await this.fetchWithTimeout(url, {
2110
+ method: "GET",
2111
+ headers: { "User-Agent": USER_AGENT2 }
2112
+ });
2113
+ if (response.status === 402) {
2114
+ return this.handleGetPaymentAndRetryRaw(url, endpoint, params, response);
2115
+ }
2116
+ if (!response.ok) {
2117
+ let errorBody;
2118
+ try {
2119
+ errorBody = await response.json();
2120
+ } catch {
2121
+ errorBody = { error: "Request failed" };
2122
+ }
2123
+ throw new APIError(`API error: ${response.status}`, response.status, sanitizeErrorResponse(errorBody));
2124
+ }
2125
+ return response.json();
2126
+ }
2127
+ async handleGetPaymentAndRetryRaw(url, endpoint, params, response) {
2128
+ let paymentHeader = response.headers.get("payment-required");
2129
+ if (!paymentHeader) {
2130
+ try {
2131
+ const respBody = await response.json();
2132
+ if (respBody.accepts || respBody.x402Version) {
2133
+ paymentHeader = btoa(JSON.stringify(respBody));
2134
+ }
2135
+ } catch {
2136
+ }
2137
+ }
2138
+ if (!paymentHeader) {
2139
+ throw new PaymentError("402 response but no payment requirements found");
2140
+ }
2141
+ const paymentRequired = parsePaymentRequired(paymentHeader);
2142
+ const details = extractPaymentDetails(paymentRequired, SOLANA_NETWORK);
2143
+ if (!details.network?.startsWith("solana:")) {
2144
+ throw new PaymentError(
2145
+ `Expected Solana payment network, got: ${details.network}. Use LLMClient for Base payments.`
2146
+ );
2147
+ }
2148
+ const feePayer = details.extra?.feePayer;
2149
+ if (!feePayer) throw new PaymentError("Missing feePayer in 402 extra field");
2150
+ const fromAddress = await this.getWalletAddress();
2151
+ const secretKey = await solanaKeyToBytes(this.privateKey);
2152
+ const extensions = paymentRequired.extensions;
2153
+ const paymentPayload = await createSolanaPaymentPayload(
2154
+ secretKey,
2155
+ fromAddress,
2156
+ details.recipient,
2157
+ details.amount,
2158
+ feePayer,
2159
+ {
2160
+ resourceUrl: validateResourceUrl(
2161
+ details.resource?.url || url,
2162
+ this.apiUrl
2163
+ ),
2164
+ resourceDescription: details.resource?.description || "BlockRun Solana AI API call",
2165
+ maxTimeoutSeconds: details.maxTimeoutSeconds || 300,
2166
+ extra: details.extra,
2167
+ extensions,
2168
+ rpcUrl: this.rpcUrl
2169
+ }
2170
+ );
2171
+ const query = params ? "?" + new URLSearchParams(params).toString() : "";
2172
+ const retryUrl = `${this.apiUrl}${endpoint}${query}`;
2173
+ const retryResponse = await this.fetchWithTimeout(retryUrl, {
2174
+ method: "GET",
2175
+ headers: {
2176
+ "User-Agent": USER_AGENT2,
2177
+ "PAYMENT-SIGNATURE": paymentPayload
2178
+ }
2179
+ });
2180
+ if (retryResponse.status === 402) {
2181
+ throw new PaymentError("Payment was rejected. Check your Solana USDC balance.");
2182
+ }
2183
+ if (!retryResponse.ok) {
2184
+ let errorBody;
2185
+ try {
2186
+ errorBody = await retryResponse.json();
2187
+ } catch {
2188
+ errorBody = { error: "Request failed" };
2189
+ }
2190
+ throw new APIError(`API error after payment: ${retryResponse.status}`, retryResponse.status, sanitizeErrorResponse(errorBody));
2191
+ }
2192
+ const costUsd = parseFloat(details.amount) / 1e6;
2193
+ this.sessionCalls += 1;
2194
+ this.sessionTotalUsd += costUsd;
2195
+ return retryResponse.json();
2196
+ }
2197
+ async fetchWithTimeout(url, options) {
2198
+ const controller = new AbortController();
2199
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
2200
+ try {
2201
+ return await fetch(url, { ...options, signal: controller.signal });
2202
+ } finally {
2203
+ clearTimeout(timeoutId);
2204
+ }
2205
+ }
2206
+ };
2207
+ function solanaClient(options = {}) {
2208
+ return new SolanaLLMClient({ ...options, apiUrl: SOLANA_API_URL });
2209
+ }
2210
+
2211
+ // src/cache.ts
2212
+ import * as fs3 from "fs";
2213
+ import * as path3 from "path";
2214
+ import * as os3 from "os";
2215
+ import * as crypto2 from "crypto";
2216
+ var CACHE_DIR = path3.join(os3.homedir(), ".blockrun", "cache");
2217
+ var DEFAULT_TTL = {
2218
+ "/v1/x/": 3600 * 1e3,
2219
+ "/v1/partner/": 3600 * 1e3,
2220
+ "/v1/pm/": 1800 * 1e3,
2221
+ "/v1/chat/": 0,
2222
+ "/v1/search": 900 * 1e3,
2223
+ "/v1/image": 0,
2224
+ "/v1/models": 86400 * 1e3
2225
+ };
2226
+ function getTtl(endpoint) {
2227
+ for (const [pattern, ttl] of Object.entries(DEFAULT_TTL)) {
2228
+ if (endpoint.includes(pattern)) return ttl;
2229
+ }
2230
+ return 3600 * 1e3;
2231
+ }
2232
+ function cacheKey(endpoint, body) {
2233
+ const keyData = JSON.stringify({ endpoint, body }, Object.keys({ endpoint, body }).sort());
2234
+ return crypto2.createHash("sha256").update(keyData).digest("hex").slice(0, 16);
2235
+ }
2236
+ function cachePath(key) {
2237
+ return path3.join(CACHE_DIR, `${key}.json`);
2238
+ }
2239
+ function getCached(key) {
2240
+ const filePath = cachePath(key);
2241
+ if (!fs3.existsSync(filePath)) return null;
2242
+ try {
2243
+ const raw = fs3.readFileSync(filePath, "utf-8");
2244
+ const entry = JSON.parse(raw);
2245
+ const ttl = entry.ttlMs ?? getTtl(entry.endpoint ?? "");
2246
+ if (ttl <= 0) return null;
2247
+ if (Date.now() - entry.cachedAt > ttl) {
2248
+ try {
2249
+ fs3.unlinkSync(filePath);
2250
+ } catch {
2251
+ }
2252
+ return null;
2253
+ }
2254
+ return entry.response;
2255
+ } catch {
2256
+ return null;
2257
+ }
2258
+ }
2259
+ function getCachedByRequest(endpoint, body) {
2260
+ const ttl = getTtl(endpoint);
2261
+ if (ttl <= 0) return null;
2262
+ const key = cacheKey(endpoint, body);
2263
+ return getCached(key);
2264
+ }
2265
+ function setCache(key, data, ttlMs) {
2266
+ if (ttlMs <= 0) return;
2267
+ try {
2268
+ fs3.mkdirSync(CACHE_DIR, { recursive: true });
2269
+ } catch {
2270
+ }
2271
+ const entry = {
2272
+ cachedAt: Date.now(),
2273
+ response: data,
2274
+ ttlMs
2275
+ };
2276
+ try {
2277
+ fs3.writeFileSync(cachePath(key), JSON.stringify(entry));
2278
+ } catch {
2279
+ }
2280
+ }
2281
+ function saveToCache(endpoint, body, response, costUsd = 0) {
2282
+ const ttl = getTtl(endpoint);
2283
+ if (ttl <= 0) return;
2284
+ try {
2285
+ fs3.mkdirSync(CACHE_DIR, { recursive: true });
2286
+ } catch {
2287
+ }
2288
+ const key = cacheKey(endpoint, body);
2289
+ const entry = {
2290
+ cachedAt: Date.now(),
2291
+ endpoint,
2292
+ body,
2293
+ response,
2294
+ costUsd
2295
+ };
2296
+ try {
2297
+ fs3.writeFileSync(cachePath(key), JSON.stringify(entry));
2298
+ } catch {
2299
+ }
2300
+ }
2301
+ function clearCache() {
2302
+ if (!fs3.existsSync(CACHE_DIR)) return 0;
2303
+ let count = 0;
2304
+ try {
2305
+ const files = fs3.readdirSync(CACHE_DIR);
2306
+ for (const file of files) {
2307
+ if (file.endsWith(".json")) {
2308
+ try {
2309
+ fs3.unlinkSync(path3.join(CACHE_DIR, file));
2310
+ count++;
2311
+ } catch {
2312
+ }
2313
+ }
2314
+ }
2315
+ } catch {
2316
+ }
2317
+ return count;
2318
+ }
2319
+
2320
+ // src/setup.ts
2321
+ function setupAgentWallet(options) {
2322
+ const { address, privateKey, isNew } = getOrCreateWallet();
2323
+ if (isNew && !options?.silent) {
2324
+ console.error(
2325
+ `
2326
+ BlockRun Agent Wallet Created!
2327
+ Address: ${address}
2328
+ Send USDC on Base to get started.
2329
+ `
2330
+ );
2331
+ }
2332
+ return new LLMClient({ privateKey });
2333
+ }
2334
+ async function setupAgentSolanaWallet(options) {
2335
+ const result = await getOrCreateSolanaWallet();
2336
+ if (result.isNew && !options?.silent) {
2337
+ console.error(
2338
+ `
2339
+ BlockRun Solana Agent Wallet Created!
2340
+ Address: ${result.address}
2341
+ Send USDC on Solana to get started.
2342
+ `
2343
+ );
2344
+ }
2345
+ return new SolanaLLMClient({ privateKey: result.privateKey });
2346
+ }
2347
+ async function status() {
2348
+ const client = setupAgentWallet({ silent: true });
2349
+ const address = client.getWalletAddress();
2350
+ const balance = await client.getBalance();
2351
+ console.log(`Wallet: ${address}`);
2352
+ console.log(`Balance: $${balance.toFixed(2)} USDC`);
2353
+ return { address, balance };
2354
+ }
2355
+
2356
+ // src/cost-log.ts
2357
+ import * as fs4 from "fs";
2358
+ import * as path4 from "path";
2359
+ import * as os4 from "os";
2360
+ var DATA_DIR = path4.join(os4.homedir(), ".blockrun", "data");
2361
+ var COST_LOG_FILE = path4.join(DATA_DIR, "costs.jsonl");
2362
+ function logCost(entry) {
2363
+ try {
2364
+ fs4.mkdirSync(DATA_DIR, { recursive: true });
2365
+ } catch {
2366
+ }
2367
+ try {
2368
+ fs4.appendFileSync(COST_LOG_FILE, JSON.stringify(entry) + "\n");
2369
+ } catch {
2370
+ }
2371
+ }
2372
+ function getCostSummary() {
2373
+ if (!fs4.existsSync(COST_LOG_FILE)) {
2374
+ return { totalUsd: 0, calls: 0, byModel: {} };
2375
+ }
2376
+ let totalUsd = 0;
2377
+ let calls = 0;
2378
+ const byModel = {};
2379
+ try {
2380
+ const content = fs4.readFileSync(COST_LOG_FILE, "utf-8").trim();
2381
+ if (!content) return { totalUsd: 0, calls: 0, byModel: {} };
2382
+ for (const line of content.split("\n")) {
2383
+ if (!line) continue;
2384
+ try {
2385
+ const entry = JSON.parse(line);
2386
+ totalUsd += entry.costUsd;
2387
+ calls += 1;
2388
+ byModel[entry.model] = (byModel[entry.model] || 0) + entry.costUsd;
2389
+ } catch {
2390
+ }
2391
+ }
2392
+ } catch {
2393
+ }
2394
+ return { totalUsd, calls, byModel };
2395
+ }
2396
+
2397
+ // src/openai-compat.ts
2398
+ var StreamingResponse = class {
2399
+ reader;
2400
+ decoder;
2401
+ buffer = "";
2402
+ model;
2403
+ id;
2404
+ constructor(response, model) {
2405
+ if (!response.body) {
2406
+ throw new Error("Response body is null");
2407
+ }
2408
+ this.reader = response.body.getReader();
2409
+ this.decoder = new TextDecoder();
2410
+ this.model = model;
2411
+ this.id = `chatcmpl-${Date.now()}`;
2412
+ }
2413
+ async *[Symbol.asyncIterator]() {
2414
+ try {
2415
+ while (true) {
2416
+ const { done, value } = await this.reader.read();
2417
+ if (done) break;
2418
+ this.buffer += this.decoder.decode(value, { stream: true });
2419
+ const lines = this.buffer.split("\n");
2420
+ this.buffer = lines.pop() || "";
2421
+ for (const line of lines) {
2422
+ const trimmed = line.trim();
2423
+ if (!trimmed || !trimmed.startsWith("data: ")) continue;
2424
+ const data = trimmed.slice(6);
2425
+ if (data === "[DONE]") return;
2426
+ try {
2427
+ const parsed = JSON.parse(data);
2428
+ yield this.transformChunk(parsed);
2429
+ } catch {
2430
+ }
2431
+ }
2432
+ }
2433
+ } finally {
2434
+ this.reader.releaseLock();
2435
+ }
2436
+ }
2437
+ transformChunk(data) {
2438
+ const choices = data.choices || [];
2439
+ return {
2440
+ id: data.id || this.id,
2441
+ object: "chat.completion.chunk",
2442
+ created: data.created || Math.floor(Date.now() / 1e3),
2443
+ model: data.model || this.model,
2444
+ choices: choices.map((choice, index) => ({
2445
+ index: choice.index ?? index,
2446
+ delta: {
2447
+ role: choice.delta?.role,
2448
+ content: choice.delta?.content
2449
+ },
2450
+ finish_reason: choice.finish_reason || null
2451
+ }))
2452
+ };
2453
+ }
2454
+ };
2455
+ var ChatCompletions = class {
2456
+ constructor(client, apiUrl, timeout) {
2457
+ this.client = client;
2458
+ this.apiUrl = apiUrl;
2459
+ this.timeout = timeout;
2460
+ }
2461
+ async create(params) {
2462
+ if (params.stream) {
2463
+ return this.createStream(params);
2464
+ }
2465
+ const response = await this.client.chatCompletion(
2466
+ params.model,
2467
+ params.messages,
2468
+ {
2469
+ maxTokens: params.max_tokens,
2470
+ temperature: params.temperature,
2471
+ topP: params.top_p,
2472
+ tools: params.tools,
2473
+ toolChoice: params.tool_choice
2474
+ }
2475
+ );
2476
+ return this.transformResponse(response);
2477
+ }
2478
+ async createStream(params) {
2479
+ const url = `${this.apiUrl}/v1/chat/completions`;
2480
+ const body = {
2481
+ model: params.model,
2482
+ messages: params.messages,
2483
+ max_tokens: params.max_tokens || 1024,
2484
+ temperature: params.temperature,
2485
+ top_p: params.top_p,
2486
+ stream: true
2487
+ };
2488
+ if (params.tools) {
2489
+ body.tools = params.tools;
2490
+ }
2491
+ if (params.tool_choice) {
2492
+ body.tool_choice = params.tool_choice;
2493
+ }
2494
+ const controller = new AbortController();
2495
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
2496
+ try {
2497
+ const response = await fetch(url, {
2498
+ method: "POST",
2499
+ headers: { "Content-Type": "application/json" },
2500
+ body: JSON.stringify(body),
2501
+ signal: controller.signal
2502
+ });
2503
+ if (response.status === 402) {
2504
+ const paymentHeader = response.headers.get("payment-required");
2505
+ if (!paymentHeader) {
2506
+ throw new Error("402 response but no payment requirements found");
2507
+ }
2508
+ throw new Error(
2509
+ "Streaming with automatic payment requires direct wallet access. Please use non-streaming mode or contact support for streaming setup."
2510
+ );
2511
+ }
2512
+ if (!response.ok) {
2513
+ throw new Error(`API error: ${response.status}`);
2514
+ }
2515
+ return new StreamingResponse(response, params.model);
2516
+ } finally {
2517
+ clearTimeout(timeoutId);
2518
+ }
2519
+ }
2520
+ transformResponse(response) {
2521
+ return {
2522
+ id: response.id || `chatcmpl-${Date.now()}`,
2523
+ object: "chat.completion",
2524
+ created: response.created || Math.floor(Date.now() / 1e3),
2525
+ model: response.model,
2526
+ choices: response.choices.map((choice, index) => ({
2527
+ index: choice.index ?? index,
2528
+ message: {
2529
+ role: "assistant",
2530
+ content: choice.message.content,
2531
+ tool_calls: choice.message.tool_calls
2532
+ },
2533
+ finish_reason: choice.finish_reason || "stop"
2534
+ })),
2535
+ usage: response.usage
2536
+ };
2537
+ }
2538
+ };
2539
+ var Chat = class {
2540
+ completions;
2541
+ constructor(client, apiUrl, timeout) {
2542
+ this.completions = new ChatCompletions(client, apiUrl, timeout);
2543
+ }
2544
+ };
2545
+ var OpenAI = class {
2546
+ chat;
2547
+ client;
2548
+ constructor(options = {}) {
2549
+ const privateKey = options.walletKey || options.privateKey;
2550
+ const apiUrl = options.baseURL || "https://blockrun.ai/api";
2551
+ const timeout = options.timeout || 6e4;
2552
+ this.client = new LLMClient({
2553
+ privateKey,
2554
+ apiUrl,
2555
+ timeout
2556
+ });
2557
+ this.chat = new Chat(this.client, apiUrl, timeout);
2558
+ }
2559
+ /**
2560
+ * Get the wallet address being used for payments.
2561
+ */
2562
+ getWalletAddress() {
2563
+ return this.client.getWalletAddress();
2564
+ }
2565
+ };
2566
+
2567
+ // src/anthropic-compat.ts
2568
+ import { privateKeyToAccount as privateKeyToAccount4 } from "viem/accounts";
2569
+ var AnthropicClient = class {
2570
+ _client = null;
2571
+ _clientPromise = null;
2572
+ _privateKey;
2573
+ _account;
2574
+ _apiUrl;
2575
+ _timeout;
2576
+ constructor(options = {}) {
2577
+ const wallet = getOrCreateWallet();
2578
+ const key = options.privateKey ?? wallet.privateKey;
2579
+ validatePrivateKey(key);
2580
+ this._privateKey = key;
2581
+ this._account = privateKeyToAccount4(this._privateKey);
2582
+ const apiUrl = options.apiUrl ?? "https://blockrun.ai/api";
2583
+ validateApiUrl(apiUrl);
2584
+ this._apiUrl = apiUrl.replace(/\/$/, "");
2585
+ this._timeout = options.timeout ?? 6e4;
2586
+ }
2587
+ async _getClient() {
2588
+ if (this._client) return this._client;
2589
+ if (this._clientPromise) return this._clientPromise;
2590
+ this._clientPromise = (async () => {
2591
+ const { default: Anthropic } = await import("@anthropic-ai/sdk");
2592
+ this._client = new Anthropic({
2593
+ baseURL: this._apiUrl,
2594
+ apiKey: "blockrun",
2595
+ fetch: this._x402Fetch.bind(this)
2596
+ });
2597
+ return this._client;
2598
+ })();
2599
+ return this._clientPromise;
2600
+ }
2601
+ async _x402Fetch(input, init) {
2602
+ const controller = new AbortController();
2603
+ const timeoutId = setTimeout(() => controller.abort(), this._timeout);
2604
+ try {
2605
+ const mergedInit = { ...init, signal: controller.signal };
2606
+ let response = await globalThis.fetch(input, mergedInit);
2607
+ if (response.status === 402) {
2608
+ let paymentHeader = response.headers.get("payment-required");
2609
+ if (!paymentHeader) {
2610
+ try {
2611
+ const respBody = await response.json();
2612
+ if (respBody.x402 || respBody.accepts) {
2613
+ paymentHeader = btoa(JSON.stringify(respBody));
2614
+ }
2615
+ } catch {
2616
+ }
2617
+ }
2618
+ if (!paymentHeader) {
2619
+ throw new Error("402 response but no payment requirements found");
2620
+ }
2621
+ const paymentRequired = parsePaymentRequired(paymentHeader);
2622
+ const details = extractPaymentDetails(paymentRequired);
2623
+ const extensions = paymentRequired.extensions;
2624
+ const paymentPayload = await createPaymentPayload(
2625
+ this._privateKey,
2626
+ this._account.address,
2627
+ details.recipient,
2628
+ details.amount,
2629
+ details.network || "eip155:8453",
2630
+ {
2631
+ resourceUrl: validateResourceUrl(
2632
+ details.resource?.url || `${this._apiUrl}/v1/messages`,
2633
+ this._apiUrl
2634
+ ),
2635
+ resourceDescription: details.resource?.description || "BlockRun AI API call",
2636
+ maxTimeoutSeconds: details.maxTimeoutSeconds || 300,
2637
+ extra: details.extra,
2638
+ extensions
2639
+ }
2640
+ );
2641
+ const newHeaders = new Headers(init?.headers);
2642
+ newHeaders.set("PAYMENT-SIGNATURE", paymentPayload);
2643
+ response = await globalThis.fetch(input, {
2644
+ ...init,
2645
+ headers: newHeaders,
2646
+ signal: controller.signal
2647
+ });
2648
+ if (response.status === 402) {
2649
+ throw new Error(
2650
+ "Payment was rejected. Check your wallet balance."
2651
+ );
2652
+ }
2653
+ }
2654
+ return response;
2655
+ } finally {
2656
+ clearTimeout(timeoutId);
2657
+ }
2658
+ }
2659
+ get messages() {
2660
+ const handler = {
2661
+ get: (_target, prop) => {
2662
+ return async (...args) => {
2663
+ const client = await this._getClient();
2664
+ const messages = client.messages;
2665
+ return messages[prop](...args);
2666
+ };
2667
+ }
2668
+ };
2669
+ return new Proxy({}, handler);
2670
+ }
2671
+ getWalletAddress() {
2672
+ return this._account.address;
2673
+ }
2674
+ };
2675
+ export {
2676
+ APIError,
2677
+ AnthropicClient,
2678
+ BASE_CHAIN_ID,
2679
+ BlockrunError,
2680
+ ImageClient,
2681
+ LLMClient,
2682
+ OpenAI,
2683
+ PaymentError,
2684
+ SOLANA_NETWORK,
2685
+ SOLANA_WALLET_FILE as SOLANA_WALLET_FILE_PATH,
2686
+ SolanaLLMClient,
2687
+ USDC_BASE,
2688
+ USDC_BASE_CONTRACT,
2689
+ USDC_SOLANA,
2690
+ WALLET_DIR_PATH,
2691
+ WALLET_FILE_PATH,
2692
+ clearCache,
2693
+ createPaymentPayload,
2694
+ createSolanaPaymentPayload,
2695
+ createSolanaWallet,
2696
+ createWallet,
2697
+ client_default as default,
2698
+ extractPaymentDetails,
2699
+ formatFundingMessageCompact,
2700
+ formatNeedsFundingMessage,
2701
+ formatWalletCreatedMessage,
2702
+ getCached,
2703
+ getCachedByRequest,
2704
+ getCostSummary,
2705
+ getEip681Uri,
2706
+ getOrCreateSolanaWallet,
2707
+ getOrCreateWallet,
2708
+ getPaymentLinks,
2709
+ getWalletAddress,
2710
+ loadSolanaWallet,
2711
+ loadWallet,
2712
+ logCost,
2713
+ parsePaymentRequired,
2714
+ saveSolanaWallet,
2715
+ saveToCache,
2716
+ saveWallet,
2717
+ scanSolanaWallets,
2718
+ scanWallets,
2719
+ setCache,
2720
+ setupAgentSolanaWallet,
2721
+ setupAgentWallet,
2722
+ solanaClient,
2723
+ solanaKeyToBytes,
2724
+ solanaPublicKey,
2725
+ status,
2726
+ testnetClient
2727
+ };