import { randomBytes } from 'node:crypto'; import { privateKeyToAccount } from 'viem/accounts'; // ── Config ───────────────────────────────────────────── const VENICE_API = 'https://api.venice.ai/api/v1'; const SIWE_DOMAIN = 'outerface.venice.ai'; const BASE_CHAIN_ID = 8453; const VENICE_PAY_TO = '0x2670B922ef37C7Df47158725C0CC407b5382293F'; const USDC_BASE = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'; let walletKey: `0x${string}` | null = null; let walletAddress: string | null = null; export function initVeniceWallet(privateKey: string, address: string): void { walletKey = privateKey as `0x${string}`; walletAddress = address; console.log(`[Venice x402] Wallet configured: ${address}`); } // ── SIWE Header ──────────────────────────────────────── export async function buildSiweHeader(uri: string): Promise { if (!walletKey) throw new Error('Venice wallet not configured'); const account = privateKeyToAccount(walletKey); const now = new Date(); const expiry = new Date(now.getTime() + 10 * 60 * 1000); const nonce = randomBytes(16).toString('hex'); const message = [ `${SIWE_DOMAIN} wants you to sign in with your Ethereum account:`, account.address, '', 'Sign in to Venice AI', '', `URI: ${uri}`, `Version: 1`, `Chain ID: ${BASE_CHAIN_ID}`, `Nonce: ${nonce}`, `Issued At: ${now.toISOString()}`, `Expiration Time: ${expiry.toISOString()}`, ].join('\n'); const signature = await account.signMessage({ message }); return Buffer.from(JSON.stringify({ address: account.address.toLowerCase(), message, signature, chainId: BASE_CHAIN_ID, timestamp: now.getTime(), }), 'utf-8').toString('base64'); } // ── Balance Check ────────────────────────────────────── export interface VeniceBalance { canConsume: boolean; balanceUsd: number; diemBalanceUsd: number; } export async function checkBalance(): Promise { if (!walletAddress) throw new Error('Venice wallet not configured'); const uri = `https://${SIWE_DOMAIN}/api/v1/x402/balance/${walletAddress}`; const header = await buildSiweHeader(uri); const res = await fetch(`${VENICE_API}/x402/balance/${walletAddress}`, { headers: { 'X-Sign-In-With-X': header }, }); if (!res.ok) { const text = await res.text(); throw new Error(`Venice balance check failed: ${res.status} ${text}`); } const data = await res.json() as any; return { canConsume: data.canConsume ?? false, balanceUsd: parseFloat(data.balanceUsd ?? '0'), diemBalanceUsd: parseFloat(data.diemBalanceUsd ?? '0'), }; } // ── Top-up via x402 ──────────────────────────────────── export async function topUp(amountUsd: number = 5): Promise<{ credited: number; newBalance: number } | null> { if (!walletKey) throw new Error('Venice wallet not configured'); try { const account = privateKeyToAccount(walletKey); const amountBaseUnits = BigInt(Math.round(amountUsd * 1e6)); const now = Math.floor(Date.now() / 1000); const validAfter = BigInt(now - 60); const validBefore = BigInt(now + 900); const nonce = `0x${Array.from(crypto.getRandomValues(new Uint8Array(32))).map(b => b.toString(16).padStart(2, '0')).join('')}`; // EIP-3009 transferWithAuthorization signature const signature = await account.signTypedData({ domain: { name: 'USD Coin', version: '2', chainId: BASE_CHAIN_ID, verifyingContract: USDC_BASE as `0x${string}` }, types: { TransferWithAuthorization: [ { name: 'from', type: 'address' }, { name: 'to', type: 'address' }, { name: 'value', type: 'uint256' }, { name: 'validAfter', type: 'uint256' }, { name: 'validBefore', type: 'uint256' }, { name: 'nonce', type: 'bytes32' }, ], }, primaryType: 'TransferWithAuthorization', message: { from: account.address, to: VENICE_PAY_TO as `0x${string}`, value: amountBaseUnits, validAfter, validBefore, nonce: nonce as `0x${string}`, }, }); // Build x402 payment header with 'accepted' field (Venice requires this) const header = Buffer.from(JSON.stringify({ x402Version: 2, scheme: 'exact', network: `eip155:${BASE_CHAIN_ID}`, accepted: { scheme: 'exact', network: `eip155:${BASE_CHAIN_ID}`, payTo: VENICE_PAY_TO, amount: amountBaseUnits.toString(), asset: USDC_BASE, extra: { name: 'USD Coin', version: '2' }, resource: `${VENICE_API}/x402/top-up`, description: 'Venice x402 top-up', mimeType: 'application/json', maxTimeoutSeconds: 300, }, payload: { signature, authorization: { from: account.address, to: VENICE_PAY_TO, value: amountBaseUnits.toString(), validAfter: validAfter.toString(), validBefore: validBefore.toString(), nonce, }, }, })).toString('base64'); const res = await fetch(`${VENICE_API}/x402/top-up`, { method: 'POST', headers: { 'X-402-Payment': header }, }); if (!res.ok) { const text = await res.text(); throw new Error(`Top-up failed: ${res.status} ${text}`); } const data = await res.json() as any; const credited = data.data?.amountCredited ?? amountUsd; const newBal = data.data?.newBalance ?? 0; console.log(`[Venice x402] Topped up $${credited} — new balance: $${newBal}`); return { credited, newBalance: newBal }; } catch (err) { console.error(`[Venice x402] Top-up failed: ${err}`); return null; } } // ── Inference via wallet auth ────────────────────────── export async function walletInference( model: string, systemPrompt: string, userContent: string, maxTokens: number = 200, ): Promise { const uri = `https://${SIWE_DOMAIN}/api/v1/chat/completions`; const header = await buildSiweHeader(uri); const res = await fetch(`${VENICE_API}/chat/completions`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Sign-In-With-X': header, }, body: JSON.stringify({ model, messages: [ { role: 'system', content: systemPrompt }, { role: 'user', content: userContent }, ], max_tokens: maxTokens, }), }); if (res.status === 402) { // Insufficient balance — try auto top-up console.log('[Venice x402] Balance insufficient, attempting auto top-up...'); const result = await topUp(5); if (!result) throw new Error('Auto top-up failed and Venice balance is insufficient'); // Retry inference after top-up const retryHeader = await buildSiweHeader(uri); const retryRes = await fetch(`${VENICE_API}/chat/completions`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Sign-In-With-X': retryHeader, }, body: JSON.stringify({ model, messages: [ { role: 'system', content: systemPrompt }, { role: 'user', content: userContent }, ], max_tokens: maxTokens, }), }); if (!retryRes.ok) { const text = await retryRes.text(); throw new Error(`Venice inference failed after top-up: ${retryRes.status} ${text}`); } const retryData = await retryRes.json() as any; return retryData.choices?.[0]?.message?.content ?? 'No summary generated.'; } if (!res.ok) { const text = await res.text(); throw new Error(`Venice inference failed: ${res.status} ${text}`); } const data = await res.json() as any; return data.choices?.[0]?.message?.content ?? 'No summary generated.'; }