/** * Julie Security Network — Production-Ready TypeScript x402 Client (viem) * Target: https://julie.freysa.dev/api/audit * Network: Base Mainnet (ChainID 8453) * Settlement Asset: Base Native USDC (0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913) * PayTo Vault: 0x75F05Da87cDFf3f6bAcAc3C07CD49F1D7dC7b20C */ import { createPublicClient, http, Address, Hex } from 'viem'; import { base } from 'viem/chains'; import { privateKeyToAccount } from 'viem/accounts'; export const BASE_USDC_ADDRESS: Address = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'; export const SETTLEMENT_VAULT: Address = '0x75F05Da87cDFf3f6bAcAc3C07CD49F1D7dC7b20C'; export const DEFAULT_API_URL = 'https://julie.freysa.dev'; const NONCES_ABI = [ { constant: true, inputs: [{ name: 'owner', type: 'address' }], name: 'nonces', outputs: [{ name: '', type: 'uint256' }], type: 'function', }, ] as const; export interface ClientConfig { privateKey?: Hex; apiUrl?: string; autoSessionPass?: boolean; sessionTier?: 'starter' | 'pro' | 'institutional'; } export class JulieSecurityClient { private apiUrl: string; private autoSessionPass: boolean; private sessionTier: string; private account?: ReturnType; private publicClient: any; private sessionKey?: string; constructor(config: ClientConfig = {}) { this.apiUrl = (config.apiUrl || process.env.JULIE_API_URL || DEFAULT_API_URL).replace(/\/$/, ''); this.autoSessionPass = config.autoSessionPass ?? true; this.sessionTier = config.sessionTier || 'starter'; const pk = config.privateKey || (process.env.CLIENT_PRIVATE_KEY as Hex); if (pk) { this.account = privateKeyToAccount(pk); } this.publicClient = createPublicClient({ chain: base, transport: http(process.env.BASE_RPC_URL || 'https://mainnet.base.org'), }); } /** * Check remaining credits on an existing session key */ async getSessionBalance(): Promise { if (!this.sessionKey) return { isActive: false, creditsRemaining: 0 }; const res = await fetch(`${this.apiUrl}/api/session/balance`, { headers: { 'X-Session-Key': this.sessionKey }, }); return res.json(); } /** * Signs an EIP-712 Permit for Base Native USDC */ private async signPaymentAuthorization(challenge: any): Promise { if (!this.account) { throw new Error('Private key required to sign x402 payment.'); } const accepts = challenge.accepts?.[0] || {}; const recipient = accepts.payTo || SETTLEMENT_VAULT; const token = accepts.asset || BASE_USDC_ADDRESS; const amount = BigInt(accepts.amount || 10000); // 10,000 = $0.01; 1,000,000 = $1.00 // Fetch current on-chain nonce for USDC permit const nonce = await this.publicClient.readContract({ address: token, abi: NONCES_ABI, functionName: 'nonces', args: [this.account.address], }); const deadline = BigInt(Math.floor(Date.now() / 1000) + 3600); const domain = { name: 'USD Coin', version: '2', chainId: BigInt(8453), verifyingContract: token, }; const types = { Permit: [ { name: 'owner', type: 'address' }, { name: 'spender', type: 'address' }, { name: 'value', type: 'uint256' }, { name: 'nonce', type: 'uint256' }, { name: 'deadline', type: 'uint256' }, ], }; const message = { owner: this.account.address, spender: recipient, value: amount, nonce, deadline, }; const signature = await this.account.signTypedData({ domain, types, primaryType: 'Permit', message, }); const payload = { scheme: 'exact', network: 'eip155:8453', payer: this.account.address, payTo: recipient, amount: amount.toString(), signature, deadline: deadline.toString(), nonce: nonce.toString(), }; return Buffer.from(JSON.stringify(payload)).toString('base64'); } /** * Audits a contract, handling 402 challenges and session pass reuse automatically */ async audit(contract: Address, chain = 'base'): Promise { const t0 = performance.now(); const url = new URL(`${this.apiUrl}/api/audit`); url.searchParams.set('contract', contract); url.searchParams.set('chain', chain); const headers: Record = { Accept: 'application/json' }; if (this.sessionKey) { headers['X-Session-Key'] = this.sessionKey; } // Step 1: Probe const probeRes = await fetch(url.toString(), { headers }); const probeMs = performance.now() - t0; if (probeRes.status === 200) { const data = await probeRes.json(); data._perf = { status: 'cached_or_session', probeMs: Math.round(probeMs) }; return data; } if (probeRes.status === 402) { console.log(`[!] HTTP 402 received for ${contract.slice(0, 10)}... (${probeMs.toFixed(1)}ms)`); this.sessionKey = undefined; if (this.autoSessionPass) { url.searchParams.set('auto_pass', this.sessionTier); if (this.account) url.searchParams.set('payer', this.account.address); } let challenge: any; const b64 = probeRes.headers.get('payment-required') || probeRes.headers.get('x-payment-required'); if (b64) { challenge = JSON.parse(Buffer.from(b64, 'base64').toString('utf8')); } else { challenge = await probeRes.json(); } if (!this.account) { return { status: 'payment_required', httpStatus: 402, challenge, note: 'Set CLIENT_PRIVATE_KEY or pass privateKey to automate settlement.', }; } // Step 2: Sign const tSign = performance.now(); const paymentHeader = await this.signPaymentAuthorization(challenge); const signMs = performance.now() - tSign; // Step 3: Paid retry const paidHeaders: Record = { Accept: 'application/json', 'PAYMENT-SIGNATURE': paymentHeader, 'X-PAYMENT': paymentHeader, }; if (this.autoSessionPass) { paidHeaders['X-Auto-Session-Pass'] = this.sessionTier; } const tPaid = performance.now(); const paidRes = await fetch(url.toString(), { headers: paidHeaders }); const paidMs = performance.now() - tPaid; if (paidRes.status === 200) { const result = await paidRes.json(); const newKey = paidRes.headers.get('X-Session-Key') || result.session_key; if (newKey) { this.sessionKey = newKey; console.log(`[+] Session pass activated! Key cached: ${newKey.slice(0, 16)}...`); } result._perf = { status: 'paid_settled', totalMs: Math.round(performance.now() - t0), probeMs: Math.round(probeMs), signMs: Math.round(signMs), paidMs: Math.round(paidMs), }; return result; } else { throw new Error(`Payment failed: HTTP ${paidRes.status} - ${await paidRes.text()}`); } } throw new Error(`Unexpected status ${probeRes.status}: ${await probeRes.text()}`); } /** * Batch audit an array of contracts */ async batchAudit(contracts: Address[], chain = 'base'): Promise { const results = []; for (const c of contracts) { try { const res = await this.audit(c, chain); results.push({ contract: c, status: 'success', data: res }); } catch (err: any) { results.push({ contract: c, status: 'error', error: err.message }); } } return results; } }