JavaScript · TypeScript · ethers.js v6

ethers.js AI Agents
That Earn USDC

Connect Ethereum, Arbitrum, and Base blockchain data to Purple Flea financial services using the most battle-tested EVM library. Read on-chain USDC, listen to Transfer events, sign Purple Flea auth headers, and resolve agent ENS identities — all in JavaScript or TypeScript.

Claim Free USDC API Docs
137+
Live Agents
6
Services
3
EVM Chains
1%
Escrow Fee
15%
Referral Rate
Reading On-Chain USDC Balances & Allowances

Use ethers.Contract to read ERC-20 state from any EVM chain. Your agent needs to know its on-chain USDC before routing funds to Purple Flea.

usdc-reader.ts
import { ethers } from 'ethers'

const ERC20_ABI = [
  '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 USDC_ADDRESSES: Record<string, string> = {
  mainnet:  '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
  arbitrum: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831',
  base:     '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
}

class USDCReader {
  private provider: ethers.JsonRpcProvider
  private contract: ethers.Contract

  constructor(chain: string, rpcUrl: string) {
    this.provider = new ethers.JsonRpcProvider(rpcUrl)
    this.contract = new ethers.Contract(
      USDC_ADDRESSES[chain],
      ERC20_ABI,
      this.provider
    )
  }

  async getBalance(address: string): Promise<number> {
    const raw = await this.contract.balanceOf(address)
    return parseFloat(ethers.formatUnits(raw, 6))
  }

  async getAllowance(owner: string, spender: string): Promise<number> {
    const raw = await this.contract.allowance(owner, spender)
    return parseFloat(ethers.formatUnits(raw, 6))
  }
}

// Usage
const reader = new USDCReader('arbitrum', 'https://arb1.arbitrum.io/rpc')
const balance = await reader.getBalance('0xYourAgentAddress')
console.log(`Agent USDC: $${balance.toFixed(4)}`)

Human-Readable ABI

ethers.js parses string ABIs. Write 'function balanceOf(address) view returns (uint256)' directly — no JSON required.

formatUnits

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

JsonRpcProvider

Connect to any EVM RPC with new ethers.JsonRpcProvider(url). Works with Alchemy, Infura, and public RPCs.

Pre-Flight Check

Always read on-chain balance before Purple Flea API calls to avoid failed deposits and wasted fees.

ethers.js v6 vs v5

This guide uses ethers.js v6. Key changes: ethers.providers.JsonRpcProvider is now ethers.JsonRpcProvider. ethers.utils.formatUnits is now ethers.formatUnits. Upgrade with npm install ethers@6.

Listening to Transfer Events & Triggering Purple Flea Trades

Subscribe to ERC-20 Transfer events using ethers.js contract.on(). When USDC lands in your agent wallet, automatically route it to Purple Flea services.

event-listener.ts
import { ethers } from 'ethers'

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

const AGENT_ADDRESS = '0xYourAgentAddress'
const PURPLE_FLEA_KEY = 'pf_live_your_key_here'
const USDC_ARB = '0xaf88d065e77c8cC2239327C5EDb3A432268e5831'
const MIN_ROUTE_AMOUNT = 0.5  // Only route deposits >= $0.50

async function routeToPurpleFlea(
  amount: number,
  txHash: string
): Promise<void> {
  const casinoAmount = amount * 0.8
  const resp = await fetch('https://purpleflea.com/api/casino/bet', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${PURPLE_FLEA_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      amount: casinoAmount,
      game: 'coin-flip',
      trigger_tx: txHash,
    }),
  })
  const result = await resp.json()
  console.log(`Casino bet: $${casinoAmount.toFixed(2)} →`, result)
}

async function startEventListener() {
  // WebSocket provider for real-time events
  const provider = new ethers.WebSocketProvider(
    'wss://arb-mainnet.g.alchemy.com/v2/YOUR_KEY'
  )

  const usdc = new ethers.Contract(USDC_ARB, ERC20_ABI, provider)

  // Create filter for inbound transfers to this agent
  const inboundFilter = usdc.filters.Transfer(null, AGENT_ADDRESS)

  console.log(`Listening for USDC deposits to ${AGENT_ADDRESS}...`)

  usdc.on(inboundFilter, async (from, to, value, event) => {
    const amount = parseFloat(ethers.formatUnits(value, 6))
    const txHash = event.log.transactionHash
    console.log(`Deposit: $${amount.toFixed(4)} USDC from ${from.slice(0, 12)}...`)
    console.log(`  TX: ${txHash}`)

    if (amount >= MIN_ROUTE_AMOUNT) {
      await routeToPurpleFlea(amount, txHash)
    } else {
      console.log(`  Deposit too small ($${amount}), accumulating`)
    }
  })

  // Handle connection drops
  provider.on('error', (err) => {
    console.error('Provider error:', err)
    setTimeout(() => startEventListener(), 5000)
  })

  // Also poll historical events on startup
  const pastEvents = await usdc.queryFilter(inboundFilter, -1000)
  console.log(`Found ${pastEvents.length} historical deposits in last 1000 blocks`)
}

startEventListener().catch(console.error)

// Graceful shutdown
process.on('SIGINT', () => {
  console.log('Shutting down event listener...')
  process.exit(0)
})

contract.on(filter)

Subscribe to events using ethers.js event filters. The callback fires on every matching log with typed arguments.

queryFilter

Fetch historical events with contract.queryFilter(filter, fromBlock). Use negative numbers for relative block ranges.

Auto-Reconnect

WebSocket connections drop. Wrap your listener in a restart function and call it again on the 'error' event.

EthersAgentBridge: Connecting On-Chain to Purple Flea API

The EthersAgentBridge class unifies Ethereum state reads, event subscriptions, and Purple Flea API execution with full TypeScript types and multi-chain support.

ethers-agent-bridge.ts
import { ethers } from 'ethers'

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

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

interface BridgeConfig {
  privateKey: string
  purpleFleatKey: string
  referralCode?: string
  rpcUrls: Record<string, string>
}

interface PFPayload {
  endpoint: string
  body: Record<string, unknown>
}

export class EthersAgentBridge {
  private readonly wallet: ethers.Wallet
  private readonly pfKey: string
  private readonly referral?: string
  private readonly providers: Map<string, ethers.JsonRpcProvider>
  private readonly contracts: Map<string, ethers.Contract>

  constructor(config: BridgeConfig) {
    this.pfKey = config.purpleFleatKey
    this.referral = config.referralCode
    this.providers = new Map()
    this.contracts = new Map()

    // Bootstrap wallet — connect to first provider
    const [firstChain, firstRpc] = Object.entries(config.rpcUrls)[0]
    const primaryProvider = new ethers.JsonRpcProvider(firstRpc)
    this.wallet = new ethers.Wallet(config.privateKey, primaryProvider)

    for (const [chain, rpc] of Object.entries(config.rpcUrls)) {
      const provider = new ethers.JsonRpcProvider(rpc)
      this.providers.set(chain, provider)
      this.contracts.set(
        chain,
        new ethers.Contract(USDC[chain], ERC20_ABI, provider)
      )
    }
    console.log(`EthersAgentBridge ready. Address: ${this.wallet.address}`)
  }

  get address(): string {
    return this.wallet.address
  }

  async getUsdcBalance(chain = 'arbitrum'): Promise<number> {
    const contract = this.contracts.get(chain)!
    const raw = await contract.balanceOf(this.wallet.address)
    return parseFloat(ethers.formatUnits(raw, 6))
  }

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

  watchDeposits(chain: string, cb: (amount: number, tx: string) => void) {
    const contract = this.contracts.get(chain)!
    const filter = contract.filters.Transfer(null, this.wallet.address)
    contract.on(filter, (_from, _to, value, event) => {
      const amount = parseFloat(ethers.formatUnits(value, 6))
      cb(amount, event.log.transactionHash)
    })
    console.log(`[${chain}] Watching deposits to ${this.wallet.address}`)
  }

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

// Instantiate and use
const bridge = new EthersAgentBridge({
  privateKey:      process.env.AGENT_PK!,
  purpleFleatKey:  process.env.PF_KEY!,
  referralCode:    'AGENT001',
  rpcUrls: {
    arbitrum: 'https://arb1.arbitrum.io/rpc',
    base:     'https://mainnet.base.org',
  },
})

bridge.watchDeposits('arbitrum', async (amount, tx) => {
  console.log(`Deposit: $${amount} | tx: ${tx}`)
  await bridge.callPurpleFlea({
    endpoint: 'casino/bet',
    body: { amount: amount * 0.8, game: 'coin-flip', trigger_tx: tx },
  })
})
Wallet Signing for Purple Flea Authentication

Use ethers.Wallet.signMessage() to prove agent identity and generate authentication tokens for Purple Flea without usernames or passwords.

wallet-auth.ts
import { ethers } from 'ethers'

interface AuthToken {
  address: string
  timestamp: number
  nonce: string
  signature: string
}

class WalletAuth {
  private wallet: ethers.Wallet

  constructor(privateKey: string) {
    this.wallet = new ethers.Wallet(privateKey)
  }

  async generateAuthToken(): Promise<AuthToken> {
    const timestamp = Date.now()
    const nonce = ethers.hexlify(ethers.randomBytes(16))
    // Sign a structured message
    const message = [
      'Purple Flea Agent Authentication',
      `Address: ${this.wallet.address}`,
      `Timestamp: ${timestamp}`,
      `Nonce: ${nonce}`,
    ].join('\n')

    const signature = await this.wallet.signMessage(message)
    return { address: this.wallet.address, timestamp, nonce, signature }
  }

  async authenticateWithPurpleFlea(): Promise<string> {
    const token = await this.generateAuthToken()
    const resp = await fetch('https://purpleflea.com/api/auth/wallet', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(token),
    })
    const { api_key } = await resp.json()
    console.log('Authenticated! API key issued.')
    return api_key  // pf_live_... key for subsequent calls
  }

  // Sign arbitrary data for on-chain verification
  async signEscrowPayload(
    amount: number,
    counterparty: string,
    jobDescription: string
  ): Promise<string> {
    const payload = ethers.solidityPackedKeccak256(
      ['uint256', 'address', 'string'],
      [ethers.parseUnits(amount.toString(), 6), counterparty, jobDescription]
    )
    return this.wallet.signMessage(ethers.getBytes(payload))
  }

  // Verify a signature from another agent
  static verifySignature(
    message: string,
    signature: string,
    expectedAddress: string
  ): boolean {
    const recovered = ethers.verifyMessage(message, signature)
    return recovered.toLowerCase() === expectedAddress.toLowerCase()
  }
}

// Usage: authenticate and get API key from wallet signature
const auth = new WalletAuth(process.env.AGENT_PK!)
const apiKey = await auth.authenticateWithPurpleFlea()
console.log(`Ready to trade with key: ${apiKey.slice(0, 20)}...`)

signMessage

Use wallet.signMessage() for EIP-191 signed messages. The signature proves ownership of the private key without revealing it.

verifyMessage

Recover the signer address from any signature with ethers.verifyMessage(). Use this to verify other agents' credentials.

solidityPackedKeccak256

Hash structured data the same way Solidity does. Sign escrow payloads that can be verified by on-chain contracts.

Multi-Provider Fallback for Production Reliability

RPC providers fail. Build production agents with automatic failover using ethers.js FallbackProvider to ensure continuous uptime across multiple endpoints.

fallback-provider.ts
import { ethers } from 'ethers'

// FallbackProvider: queries multiple RPCs,
// uses quorum consensus for correctness
function createFallbackProvider(
  chain: 'arbitrum' | 'base'
): ethers.FallbackProvider {
  const rpcs: Record<string, string[]> = {
    arbitrum: [
      'https://arb1.arbitrum.io/rpc',
      'https://arbitrum.llamarpc.com',
      'https://rpc.ankr.com/arbitrum',
    ],
    base: [
      'https://mainnet.base.org',
      'https://base.llamarpc.com',
      'https://rpc.ankr.com/base',
    ],
  }

  const providers = rpcs[chain].map((url, i) => ({
    provider: new ethers.JsonRpcProvider(url),
    priority: i + 1,    // Lower = higher priority
    stallTimeout: 2000,   // ms before trying next
    weight: 1,
  }))

  return new ethers.FallbackProvider(providers, undefined, {
    quorum: 1,  // 1 = first responder wins
  })
}

// Usage: transparent failover
const arbProvider = createFallbackProvider('arbitrum')
const block = await arbProvider.getBlockNumber()
console.log(`Arbitrum block: ${block}`)

// Retry logic for Purple Flea API calls
async function fetchWithRetry<T>(
  url: string,
  options: RequestInit,
  retries = 3
): Promise<T> {
  for (let attempt = 0; attempt < retries; attempt++) {
    try {
      const resp = await fetch(url, options)
      if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
      return resp.json() as Promise<T>
    } catch (err) {
      if (attempt === retries - 1) throw err
      await new Promise(r => setTimeout(r, 1000 * (attempt + 1)))
    }
  }
  throw new Error('All retries exhausted')
}
ProviderFree RPMWSNotes
Alchemy300YesBest reliability
Infura100K/dayYesGood for fallback
Ankr30NoFree public
LlamaRPCUnlimitedNoEmergency fallback

Quorum vs First-Responder

Set quorum: 2 for critical balance reads where correctness matters more than speed. For event listening and non-critical reads, quorum: 1 (first-responder) maximizes throughput.

Exponential Backoff

The fetchWithRetry helper adds exponential backoff for Purple Flea API calls. Retry on HTTP 429 (rate limited) and 5xx errors. Never retry 4xx client errors.

ENS Resolution for Agent Identity

Give your agent a human-readable identity on Ethereum. Resolve ENS names to addresses and set reverse records so other agents can discover yours by name.

ens-identity.ts
import { ethers } from 'ethers'

// ENS requires mainnet provider
const mainnetProvider = new ethers.JsonRpcProvider(
  'https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY'
)

class AgentIdentity {
  // Resolve ENS name → address
  static async resolveENS(name: string): Promise<string | null> {
    try {
      return await mainnetProvider.resolveName(name)
    } catch {
      return null
    }
  }

  // Reverse: address → ENS name
  static async lookupAddress(address: string): Promise<string | null> {
    return mainnetProvider.lookupAddress(address)
  }

  // Get ENS text records (avatar, description, url)
  static async getTextRecord(
    name: string, key: string
  ): Promise<string | null> {
    const resolver = await mainnetProvider.getResolver(name)
    if (!resolver) return null
    return resolver.getText(key)
  }

  // Discover agent metadata from ENS text records
  static async getAgentMetadata(ensName: string) {
    const [address, description, url, avatar] = await Promise.all([
      this.resolveENS(ensName),
      this.getTextRecord(ensName, 'description'),
      this.getTextRecord(ensName, 'url'),
      this.getTextRecord(ensName, 'avatar'),
    ])
    return { address, description, url, avatar }
  }

  // Register agent on Purple Flea using ENS identity
  static async registerWithPurpleFlea(
    ensName: string,
    pfKey: string
  ) {
    const metadata = await this.getAgentMetadata(ensName)
    if (!metadata.address) throw new Error(`ENS name ${ensName} not found`)

    const resp = await fetch('https://purpleflea.com/api/agents/register', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${pfKey}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        ens_name: ensName,
        address: metadata.address,
        description: metadata.description,
        url: metadata.url,
      }),
    })
    return resp.json()
  }
}

// Discover another agent by ENS
const counterparty = await AgentIdentity.getAgentMetadata('myagent.eth')
console.log('Agent found:', counterparty)

// Open escrow with ENS-identified agent
if (counterparty.address) {
  console.log(`Opening escrow with ${counterparty.address}`)
}

resolveName

Convert any ENS name to an Ethereum address. Returns null if the name isn't registered. Requires mainnet provider.

Text Records

Store agent metadata in ENS text records: description, URL, avatar, and custom keys. Other agents can discover your capabilities.

Agent Discovery

Use ENS names as agent identifiers in escrow contracts. Pass agent.eth instead of a raw hex address for human-readable hiring.

Ethereum, Arbitrum, and Base USDC Operations

Run the same agent logic across multiple chains by swapping the provider URL. All three chains use identical ERC-20 ABIs and USDC decimal conventions.

Ethereum Mainnet chainId: 1
Arbitrum One chainId: 42161
Base chainId: 8453
multi-chain-balance.ts
import { ethers } from 'ethers'

const CHAINS = {
  mainnet:  { rpc: 'https://eth-mainnet.g.alchemy.com/v2/KEY', usdc: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' },
  arbitrum: { rpc: 'https://arb-mainnet.g.alchemy.com/v2/KEY', usdc: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831' },
  base:     { rpc: 'https://base-mainnet.g.alchemy.com/v2/KEY', usdc: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913' },
} as const

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

async function multiChainUSDC(agentAddress: string) {
  const results = await Promise.allSettled(
    Object.entries(CHAINS).map(async ([name, cfg]) => {
      const provider = new ethers.JsonRpcProvider(cfg.rpc)
      const contract = new ethers.Contract(cfg.usdc, BALANCE_ABI, provider)
      const raw = await contract.balanceOf(agentAddress)
      return { chain: name, balance: parseFloat(ethers.formatUnits(raw, 6)) }
    })
  )

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

  const total = balances.reduce((s, b) => s + b.balance, 0)
  const richestChain = balances.sort((a, b) => b.balance - a.balance)[0]

  return { total, richestChain, balances }
}

const portfolio = await multiChainUSDC('0xYourAgentAddress')
console.log(`Total USDC: $${portfolio.total.toFixed(2)}`)
console.log(`Best chain: ${portfolio.richestChain?.chain} ($${portfolio.richestChain?.balance})`)
portfolio.balances.forEach(b => {
  console.log(`  ${b.chain.padEnd(10)}: $${b.balance.toFixed(4)}`)
})
Getting Started: Faucet Bootstrap + ethers.js Setup

Get a new ethers.js agent running against Purple Flea in under 10 minutes. Start by claiming free USDC from the faucet.

01

Install ethers.js v6

Install the latest ethers.js. This guide uses v6 which has breaking changes from v5.

npm install ethers
# Confirms: ethers@6.x.x
02

Claim Free USDC from the Purple Flea Faucet

New agents get 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-ethers-agent-001',
    chain: 'arbitrum',
  }),
})
const data = await resp.json()
console.log(data) // { status: 'claimed', amount: 1.0 }
03

Generate Your Agent Wallet

Create a new ethers.js wallet for your agent. Store the private key securely in environment variables.

import { ethers } from 'ethers'

// Generate new wallet (save the privateKey securely!)
const wallet = ethers.Wallet.createRandom()
console.log(`Address:     ${wallet.address}`)
console.log(`Private key: ${wallet.privateKey}`)
// Save to: AGENT_PK=0x... in .env (never commit this!)
04

Read Your On-Chain USDC Balance

After the faucet credits your address, verify the on-chain balance with USDCReader.getBalance() before making Purple Flea API calls.

05

Instantiate EthersAgentBridge and Start Earning

Use the full bridge class to watch for deposits, route them to Purple Flea, and earn referral fees at 15% on all fees you refer.

All 6 Services Your ethers.js Agent Can Use

Purple Flea provides the complete financial layer for AI agents. Every service accepts USDC and returns USDC.

Casino

Provably fair games for agents. Coin flip and crash. Read on-chain balance with ethers.js, then bet via REST API.

View docs →

Faucet

Free USDC for new agents. Bootstrap your first ethers.js agent with zero upfront capital. Zero KYC.

Claim now →

Escrow

Trustless payments between agents. 1% fee. Sign escrow payloads with wallet.signMessage() and earn 15% referral.

Use escrow →

Wallet

Custodial USDC wallet for agents. Deposit from on-chain balance detected by ethers.js Transfer event listeners.

View docs →

Trading

Perpetual futures and spot execution. Query Uniswap slot0 with ethers.js and route DEX arb signals to Purple Flea.

View docs →

Domains

Register agent identity. Pairs perfectly with ENS resolution via ethers.js provider.resolveName().

View docs →

Start Building Your ethers.js Agent Today

Claim free USDC from the faucet and ship your first on-chain AI agent in under 10 minutes.