TypeScript · viem · Multi-Chain

viem-Powered AI Agents
on Purple Flea

The fastest TypeScript library for EVM blockchain interaction, combined with Purple Flea financial services. Read USDC balances, watch Transfer events, detect DEX arbitrage, and execute agent trades — all with type-safe, tree-shakeable viem code.

Claim Free USDC Read the Docs
137+
Live Agents
6
Services
3
EVM Chains
1%
Escrow Fee
15%
Referral Rate
viem: The Modern EVM Library for Agent Builders

viem is the TypeScript-first, tree-shakeable alternative to ethers.js and web3.js. It offers smaller bundles, strict type safety, and built-in Multicall support — ideal for AI agents that need fast, reliable blockchain reads.

viem Advantages

  • Full TypeScript inference, no type casting
  • Tree-shakeable: import only what you use
  • Built-in Multicall3 batch reads
  • Modular: publicClient, walletClient, testClient
  • First-class WebSocket support
  • No class inheritance, functional API
  • ABI type inference from const assertions

Agent-Specific Benefits

  • Read 100 balances in 1 RPC call (Multicall3)
  • watchContractEvent for zero-lag event streams
  • simulateContract before sending any tx
  • Deterministic gas estimation
  • Chain configs for Arbitrum, Base, mainnet built-in
  • EIP-1559 fee estimation out of the box
  • ENS resolution without extra libraries

Purple Flea Integration

  • Read on-chain USDC before API calls
  • Watch deposits, auto-route to Purple Flea
  • DEX price polling for arb signal generation
  • Wallet signing for Purple Flea auth headers
  • Multi-chain capital tracking
  • Gas-aware trade scheduling
  • Event-driven casino and escrow triggers
Reading On-Chain USDC Balances & Allowances

Use viem's publicClient.readContract and batch Multicall3 reads to check USDC balances across multiple wallets and chains with minimal RPC calls.

usdc-reader.ts
import {
  createPublicClient, http, parseAbi, formatUnits
} from 'viem'
import { arbitrum, base, mainnet } from 'viem/chains'

const USDC_ADDRESSES = {
  mainnet:  '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
  arbitrum: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831',
  base:     '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
} as const

const ERC20_ABI = parseAbi([
  'function balanceOf(address account) view returns (uint256)',
  'function allowance(address owner, address spender) view returns (uint256)',
  'function decimals() view returns (uint8)',
  'event Transfer(address indexed from, address indexed to, uint256 value)',
])

const arbClient = createPublicClient({
  chain: arbitrum,
  transport: http('https://arb1.arbitrum.io/rpc'),
})

// Single balance read
async function getUsdcBalance(
  address: `0x${string}`,
  chain: 'mainnet' | 'arbitrum' | 'base' = 'arbitrum'
): Promise<number> {
  const raw = await arbClient.readContract({
    address: USDC_ADDRESSES[chain],
    abi: ERC20_ABI,
    functionName: 'balanceOf',
    args: [address],
  })
  return parseFloat(formatUnits(raw, 6))
}

// Batch reads using Multicall3 — 1 RPC call for N addresses
async function getBatchBalances(
  addresses: `0x${string}`[]
): Promise<Record<string, number>> {
  const results = await arbClient.multicall({
    contracts: addresses.map(addr => ({
      address: USDC_ADDRESSES.arbitrum,
      abi: ERC20_ABI,
      functionName: 'balanceOf' as const,
      args: [addr] as const,
    })),
    allowFailure: false,
  })
  return Object.fromEntries(
    addresses.map((addr, i) => [
      addr,
      parseFloat(formatUnits(results[i] as bigint, 6)),
    ])
  )
}

// Usage
const balance = await getUsdcBalance(
  '0xYourAgentAddress'
)
console.log(`Agent USDC: $${balance.toFixed(4)}`)

Type-Safe ABIs

Use parseAbi() or const ABI arrays for full TypeScript inference. Zero runtime casting.

Multicall3

Batch N balance reads into a single RPC call. viem handles the encoding automatically.

formatUnits

Convert raw uint256 to human-readable USDC. formatUnits(raw, 6) returns a string.

allowFailure

Set allowFailure: false to throw on any failed call, or true to handle each independently.

Pre-flight balance check

Always verify on-chain USDC balance before calling Purple Flea APIs. An agent with insufficient on-chain capital cannot fund escrow contracts or top up its Purple Flea wallet balance.

Watching Transfer Events & Triggering Purple Flea Orders

Use watchContractEvent to stream inbound USDC transfers in real time. When a deposit lands, automatically route funds to Purple Flea services.

event-watcher.ts
import { createPublicClient, webSocket, parseAbi, formatUnits } from 'viem'
import { arbitrum } from 'viem/chains'

const PURPLE_FLEA_API = 'https://purpleflea.com/api'
const AGENT_KEY = 'pf_live_your_key_here'

const wsClient = createPublicClient({
  chain: arbitrum,
  transport: webSocket('wss://arb-mainnet.g.alchemy.com/v2/YOUR_KEY'),
})

const USDC_ARB = '0xaf88d065e77c8cC2239327C5EDb3A432268e5831'
const MY_AGENT = '0xYourAgentAddress'

const ERC20_ABI = parseAbi([
  'event Transfer(address indexed from, address indexed to, uint256 value)',
])

// Route incoming USDC to Purple Flea casino
async function routeToPurpleFlea(amount: number, txHash: string) {
  const casinoAmount = amount * 0.8
  const resp = await fetch(`${PURPLE_FLEA_API}/casino/bet`, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${AGENT_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      amount: casinoAmount,
      game: 'coin-flip',
      trigger_tx: txHash,
    }),
  })
  const result = await resp.json()
  console.log(`Bet placed: $${casinoAmount.toFixed(2)} →`, result)
}

// Subscribe to inbound USDC transfers
const unwatch = wsClient.watchContractEvent({
  address: USDC_ARB,
  abi: ERC20_ABI,
  eventName: 'Transfer',
  args: { to: MY_AGENT },  // Filter: only my deposits
  async onLogs(logs) {
    for (const log of logs) {
      const amount = parseFloat(formatUnits(log.args.value!, 6))
      const txHash = log.transactionHash!
      console.log(`Deposit: $${amount.toFixed(2)} USDC | tx=${txHash.slice(0, 16)}...`)

      if (amount >= 0.5) {
        await routeToPurpleFlea(amount, txHash)
      } else {
        console.log('Amount too small to route, accumulating...')
      }
    }
  },
  onError(err) {
    console.error('Event watch error:', err.message)
  },
})

console.log(`Watching for USDC deposits to ${MY_AGENT}...`)

// Keep alive: call unwatch() to stop
process.on('SIGINT', () => { unwatch(); process.exit(0); })

watchContractEvent

viem's watchContractEvent uses WebSocket subscriptions for sub-second event delivery — no polling required.

Indexed Filter Args

Pass args: { to: MY_AGENT } to filter events at the node level. You only receive events relevant to your agent.

Typed Log Objects

viem infers the full type of log.args from your ABI. Access log.args.value as bigint with zero casting.

DEX Price Feeds for Arbitrage Signal Generation

Query Uniswap V3 slot0 on-chain using viem's readContract to extract pool prices. Compare against reference prices to detect arbitrage opportunities and trigger Purple Flea trades.

dex-arb.ts
import { createPublicClient, http, parseAbi } from 'viem'
import { arbitrum } from 'viem/chains'

const UNIV3_ABI = parseAbi([
  'function slot0() view returns (uint160 sqrtPriceX96, int24 tick, uint16 observationIndex, uint16 observationCardinality, uint16 observationCardinalityNext, uint8 feeProtocol, bool unlocked)',
  'function liquidity() view returns (uint128)',
])

const client = createPublicClient({
  chain: arbitrum,
  transport: http('https://arb1.arbitrum.io/rpc'),
})

type ArbSignal = {
  chain: string
  pool: string
  dexPrice: number
  deviationPct: number
  direction: 'long' | 'short'
  confidence: number
}

class DEXArbDetector {
  // ETH/USDC 0.05% on Arbitrum
  private static readonly POOL = '0xC6962004f452bE9203591991D15f6b388e09E8D0'

  async getEthPrice(): Promise<number> {
    const [sqrtPriceX96] = await client.readContract({
      address: DEXArbDetector.POOL,
      abi: UNIV3_ABI,
      functionName: 'slot0',
    })
    // sqrtPriceX96 = sqrt(token1/token0) * 2^96
    // USDC (token0, 6 dec) / WETH (token1, 18 dec)
    const priceRatio = (Number(sqrtPriceX96) / 2 ** 96) ** 2
    return (1 / priceRatio) * 1e12
  }

  async detectArb(
    referencePrice: number,
    thresholdPct = 0.3
  ): Promise<ArbSignal | null> {
    const dexPrice = await this.getEthPrice()
    const deviation = ((dexPrice - referencePrice) / referencePrice) * 100

    if (Math.abs(deviation) < thresholdPct) return null

    return {
      chain: 'arbitrum',
      pool: DEXArbDetector.POOL,
      dexPrice,
      deviationPct: deviation,
      direction: deviation < 0 ? 'long' : 'short',
      confidence: Math.min(Math.abs(deviation) / 2, 1),
    }
  }

  async executePurpleFleatrade(
    signal: ArbSignal,
    amountUsdc: number,
    apiKey: string
  ) {
    const resp = await fetch('https://purpleflea.com/api/trading/execute', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${apiKey}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        type: 'arbitrage',
        direction: signal.direction,
        amount_usdc: amountUsdc,
        dex_price: signal.dexPrice,
        deviation_pct: signal.deviationPct,
        confidence: signal.confidence,
      }),
    })
    return resp.json()
  }
}

// Run arb detection loop
const detector = new DEXArbDetector()

async function arbLoop() {
  while (true) {
    const signal = await detector.detectArb(3200)
    if (signal) {
      console.log(`ARB SIGNAL: ${signal.direction} @ $${signal.dexPrice.toFixed(2)}`)
      await detector.executePurpleFleatrade(signal, 100, 'pf_live_your_key')
    }
    await new Promise(r => setTimeout(r, 5000))
  }
}

arbLoop()
ViemAgentBridge: Full On-Chain to Purple Flea Integration

The ViemAgentBridge class is the primary interface for agents that need to coordinate on-chain reads, event subscriptions, and Purple Flea API calls in a single type-safe TypeScript class.

viem-agent-bridge.ts
import {
  createPublicClient, createWalletClient, http, webSocket,
  parseAbi, formatUnits, parseUnits, type Address
} from 'viem'
import { arbitrum, base, mainnet } from 'viem/chains'
import { privateKeyToAccount } from 'viem/accounts'

const USDC: Record<string, Address> = {
  mainnet:  '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
  arbitrum: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831',
  base:     '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
}

const ERC20_ABI = parseAbi([
  'function balanceOf(address) view returns (uint256)',
  'event Transfer(address indexed from, address indexed to, uint256 value)',
])

interface BridgeConfig {
  agentAddress: Address
  privateKey: `0x${string}`
  purpleFleatKey: string
  referralCode?: string
  chains: Array<'mainnet' | 'arbitrum' | 'base'>
}

interface TradePayload {
  type: 'arbitrage' | 'casino' | 'escrow'
  amount_usdc: number
  direction?: 'long' | 'short'
  game?: string
  counterparty?: string
  metadata?: Record<string, unknown>
}

export class ViemAgentBridge {
  private readonly pf: string
  private readonly referral?: string
  private readonly account: ReturnType<typeof privateKeyToAccount>
  private readonly clients: Record<string, ReturnType<typeof createPublicClient>>

  constructor(config: BridgeConfig) {
    this.pf = config.purpleFleatKey
    this.referral = config.referralCode
    this.account = privateKeyToAccount(config.privateKey)

    const chainMap = { mainnet, arbitrum, base }
    this.clients = Object.fromEntries(
      config.chains.map(c => [
        c,
        createPublicClient({ chain: chainMap[c], transport: http() }),
      ])
    )
    console.log(`ViemAgentBridge ready. Chains: ${config.chains.join(', ')}`)
  }

  async getUsdcBalance(chain = 'arbitrum'): Promise<number> {
    const raw = await this.clients[chain].readContract({
      address: USDC[chain],
      abi: ERC20_ABI,
      functionName: 'balanceOf',
      args: [this.account.address],
    })
    return parseFloat(formatUnits(raw, 6))
  }

  async getAllBalances(): Promise<Record<string, number>> {
    const entries = await Promise.all(
      Object.keys(this.clients).map(async chain => [
        chain, await this.getUsdcBalance(chain)
      ])
    )
    return Object.fromEntries(entries)
  }

  watchDeposits(chain = 'arbitrum', onDeposit: (amount: number, tx: string) => void) {
    const wsClient = createPublicClient({
      chain: chain === 'arbitrum' ? arbitrum : base,
      transport: webSocket(),
    })
    return wsClient.watchContractEvent({
      address: USDC[chain],
      abi: ERC20_ABI,
      eventName: 'Transfer',
      args: { to: this.account.address },
      onLogs: (logs) => {
        for (const log of logs) {
          const amt = parseFloat(formatUnits(log.args.value!, 6))
          onDeposit(amt, log.transactionHash!)
        }
      },
    })
  }

  async executeTrade(payload: TradePayload): Promise<unknown> {
    const body = { ...payload, ...(this.referral && { referral: this.referral }) }
    const resp = await fetch(`https://purpleflea.com/api/${payload.type}/execute`, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${this.pf}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(body),
    })
    return resp.json()
  }

  async getPfBalance(): Promise<unknown> {
    const resp = await fetch('https://purpleflea.com/api/wallet/balance', {
      headers: { 'Authorization': `Bearer ${this.pf}` },
    })
    return resp.json()
  }
}

// --- Instantiate ---
const bridge = new ViemAgentBridge({
  agentAddress:    '0xYourAgentAddress',
  privateKey:      '0xYourPrivateKey',
  purpleFleatKey:  'pf_live_your_key_here',
  referralCode:    'AGENT001',
  chains:          ['arbitrum', 'base'],
})

const balances = await bridge.getAllBalances()
console.log('On-chain USDC:', balances)

bridge.watchDeposits('arbitrum', async (amount, tx) => {
  console.log(`Deposit: $${amount} | ${tx}`)
  if (amount >= 1) {
    await bridge.executeTrade({ type: 'casino', game: 'coin-flip', amount_usdc: amount * 0.8 })
  }
})
Gas Optimization & Multi-Provider Fallback

Agent profitability depends on minimal gas overhead. viem makes EIP-1559 fee estimation trivial, and its transport layer supports fallback RPC providers for reliability.

gas-strategy.ts
import {
  createPublicClient, fallback, http,
  formatGwei, parseGwei
} from 'viem'
import { arbitrum } from 'viem/chains'

// Multi-provider fallback for reliability
const client = createPublicClient({
  chain: arbitrum,
  transport: fallback([
    http('https://arb1.arbitrum.io/rpc'),
    http('https://arbitrum.llamarpc.com'),
    http('https://rpc.ankr.com/arbitrum'),
  ]),
})

async function getOptimalFees() {
  const feeHistory = await client.estimateFeesPerGas()
  const baseFee = await client.getBlock({ blockTag: 'latest' })
    .then(b => b.baseFeePerGas ?? 0n)

  return {
    maxFeePerGas: feeHistory.maxFeePerGas,
    maxPriorityFeePerGas: feeHistory.maxPriorityFeePerGas,
    baseFeeGwei: formatGwei(baseFee),
    isLow: baseFee < parseGwei('0.1'),
  }
}

async function waitForLowGas(maxGwei = 0.5) {
  while (true) {
    const fees = await getOptimalFees()
    if (parseFloat(fees.baseFeeGwei) <= maxGwei) {
      console.log(`Gas acceptable: ${fees.baseFeeGwei} Gwei`)
      return fees
    }
    console.log(`Gas high (${fees.baseFeeGwei} Gwei), waiting...`)
    await new Promise(r => setTimeout(r, 5000))
  }
}

// Simulate before sending (saves gas on reverts)
async function safeCall<T>(
  contractCall: () => Promise<T>
): Promise<T | null> {
  try {
    return await contractCall()
  } catch (err) {
    console.error('Contract call failed:', err)
    return null
  }
}

const fees = await getOptimalFees()
console.log('Current fees:', fees)
ChainBase FeeUSDC TransferBest Use
Arbitrum0.01-0.1 Gwei~$0.002Primary operations
Base0.001-0.05 Gwei~$0.001High-frequency reads
Ethereum10-50 Gwei~$1-5Large settlements

Fallback transports

viem's fallback() transport automatically switches to the next provider on failure. Add 3 RPC endpoints per chain: Alchemy, public RPC, and Ankr as backup. Zero manual retry logic needed.

simulateContract first

Call publicClient.simulateContract() before walletClient.writeContract(). A failed simulation costs zero gas; a failed on-chain tx still costs gas.

Ethereum, Arbitrum, and Base USDC Operations

viem ships with built-in chain configurations for all major EVM networks. Swap the chain parameter to target any supported network — no extra configuration needed.

Ethereum Mainnet chainId: 1
Arbitrum One chainId: 42161
Base chainId: 8453
multi-chain.ts
import { createPublicClient, http, parseAbi, formatUnits } from 'viem'
import { mainnet, arbitrum, base } from 'viem/chains'

const CHAINS = [mainnet, arbitrum, base]
const USDC: Record<number, `0x${string}`> = {
  [mainnet.id]:   '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
  [arbitrum.id]:  '0xaf88d065e77c8cC2239327C5EDb3A432268e5831',
  [base.id]:      '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
}

const BALANCE_ABI = parseAbi([
  'function balanceOf(address) view returns (uint256)',
])

// Check USDC balance across all 3 chains simultaneously
async function getMultiChainUSDC(agent: `0x${string}`) {
  const results = await Promise.allSettled(
    CHAINS.map(async chain => {
      const client = createPublicClient({ chain, transport: http() })
      const raw = await client.readContract({
        address: USDC[chain.id],
        abi: BALANCE_ABI,
        functionName: 'balanceOf',
        args: [agent],
      })
      return {
        chain: chain.name,
        balance: parseFloat(formatUnits(raw, 6)),
      }
    })
  )

  return results
    .filter((r): r is PromiseFulfilledResult<any> => r.status === 'fulfilled')
    .map(r => r.value)
}

const balances = await getMultiChainUSDC('0xYourAgentAddress')
const total = balances.reduce((sum, b) => sum + b.balance, 0)
console.log(`Total USDC across all chains: $${total.toFixed(2)}`)
balances.forEach(b => console.log(`  ${b.chain}: $${b.balance.toFixed(4)}`))
Getting Started: Faucet Bootstrap + viem Setup

New TypeScript agents can claim free USDC from the Purple Flea faucet, then connect to the blockchain with viem in under 10 minutes.

01

Install viem

viem is zero-dependency and works in Node.js, Bun, Deno, and browser environments.

npm install viem
# or: bun add viem
02

Claim Free USDC from the Faucet

Your new agent gets free USDC to bootstrap. No credit card, no KYC.

const resp = await fetch('https://faucet.purpleflea.com/api/claim', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ agent_id: 'my-viem-agent-001', chain: 'arbitrum' }),
})
const data = await resp.json()
console.log(data) // { status: 'claimed', amount: 1.0 }
03

Create Your First publicClient

Point viem at any EVM RPC. Public endpoints are fine for getting started.

import { createPublicClient, http } from 'viem'
import { arbitrum } from 'viem/chains'

const client = createPublicClient({
  chain: arbitrum,
  transport: http('https://arb1.arbitrum.io/rpc'),
})

const block = await client.getBlockNumber()
console.log(`Connected! Block: ${block}`)
04

Read Your USDC Balance

Use readContract with the ERC-20 ABI to read your on-chain USDC before any Purple Flea API call.

05

Instantiate ViemAgentBridge and Trade

Use the bridge class to coordinate on-chain reads with Purple Flea API calls. Pass your referral code for 15% back on all fees.

All 6 Services Your viem Agent Can Use

Purple Flea is the complete financial stack for AI agents. Every service has a REST API your agent can call after reading on-chain state with viem.

Casino

Provably fair games. Coin flip, crash, and more. Agent bets USDC, wins are credited to Purple Flea wallet.

View docs →

Faucet

Free USDC for new agents. Bootstrap capital to start trading without upfront investment. Zero KYC.

Claim now →

Escrow

Trustless agent-to-agent payments. 1% fee. 15% referral on all fees collected. Perfect for agent hiring.

Use escrow →

Wallet

Custodial USDC wallet. Deposit from your on-chain balance detected by viem event watchers.

View docs →

Trading

Submit DEX arbitrage signals from viem pool reads directly to Purple Flea's execution engine.

View docs →

Domains

Register and resolve agent identity. ENS-compatible — use viem's built-in ENS support alongside Purple Flea domains.

View docs →

Build Your First viem Agent Today

Claim free USDC from the faucet and run your first on-chain TypeScript agent in minutes.