bluepages.fyi

// API Docs

Programmatic access to bluepages.fyi

Overview

Query crypto address ↔ Twitter mappings via REST API. Two payment options:

🔑 Option 1: API Keys (Recommended)
  • Prepaid credits — faster, no wallet interaction needed
  • 20% cheaper than x402 pay-per-request
  • 2x rate limits (60 req/min vs 30)
  • Credits never expire
  • Get your API key →
💳 Option 2: x402 Protocol
  • Pay-per-request with USDC on Base mainnet
  • Requires wallet with USDC + EIP-712 signing
  • Good for occasional use or testing
🤖 Are you an AI/LLM?
Use our MCP Server for native integration with Claude and other AI assistants. No HTTP calls needed — the AI can call tools directly.
Jump to MCP Setup → | Full MCP Documentation →
🤖 AI Agent Quick Start

Have a private key + USDC on Base? Here's everything you need.

Prerequisites:
  • Private key for an Ethereum wallet
  • USDC on Base mainnet (at least $0.10 for testing)
  • Node.js 18+ or Python 3.10+

📋 Complete JavaScript Script (copy-paste ready):

// batch-lookup.js - Look up Twitter handles for a list of addresses
// Install: npm install x402-fetch@^1.1.0 viem

import { wrapFetchWithPayment } from 'x402-fetch';
import { createWalletClient, http } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { base } from 'viem/chains';

// CONFIG - Set these!
const PRIVATE_KEY = process.env.PRIVATE_KEY; // Your private key (with 0x prefix)
const ADDRESSES = [
  '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', // vitalik.eth
  '0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B',
  // Add more addresses here (up to 50 per batch)
];

// Setup x402 payment (max $3 per request - batch/data costs $2)
const account = privateKeyToAccount(PRIVATE_KEY);
const walletClient = createWalletClient({
  account,
  chain: base,
  transport: http()
});
const fetchWithPayment = wrapFetchWithPayment(fetch, walletClient, BigInt(3_000_000));

// Rate limit: x402 allows 30 req/min = 2 second delay
const sleep = (ms) => new Promise(r => setTimeout(r, ms));

async function lookupAddresses(addresses) {
  const results = {};
  
  // Process in batches of 50
  for (let i = 0; i < addresses.length; i += 50) {
    const batch = addresses.slice(i, i + 50);
    console.log(`Processing batch ${Math.floor(i/50) + 1}...`);
    
    // Step 1: Check which addresses have data ($0.04)
    const checkRes = await fetchWithPayment('https://bluepages.fyi/batch/check', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ addresses: batch })
    });
    const checkData = await checkRes.json();
    
    // Find addresses that have data
    const foundAddresses = [];
    for (const [addr, info] of Object.entries(checkData.results?.addresses || {})) {
      if (info.exists) {
        foundAddresses.push(addr);
        results[addr] = { exists: true, twitter: info.twitter, farcaster: info.farcaster };
      } else {
        results[addr] = { exists: false };
      }
    }
    
    // Step 2: Get full data for found addresses ($2.00)
    if (foundAddresses.length > 0) {
      await sleep(2000); // Respect rate limit
      
      const dataRes = await fetchWithPayment('https://bluepages.fyi/batch/data', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ addresses: foundAddresses })
      });
      const dataJson = await dataRes.json();
      
      // Extract Twitter handles from response
      for (const [addr, info] of Object.entries(dataJson.results?.addresses || {})) {
        if (info.found && info.primary) {
          results[addr] = {
            exists: true,
            twitter: info.primary.twitter,
            source: info.primary.metadata?.source,
            alternates: info.alternates?.length || 0
          };
        }
      }
    }
    
    // Rate limit between batches
    if (i + 50 < addresses.length) {
      await sleep(2000);
    }
  }
  
  return results;
}

// Run it
lookupAddresses(ADDRESSES).then(results => {
  console.log('\n=== Results ===');
  for (const [addr, data] of Object.entries(results)) {
    if (data.twitter) {
      console.log(`${addr.slice(0,10)}... → ${data.twitter} (${data.source})`);
    } else if (data.exists) {
      console.log(`${addr.slice(0,10)}... → has data but no Twitter`);
    } else {
      console.log(`${addr.slice(0,10)}... → not found`);
    }
  }
});

🐍 Complete Python Script:

# batch_lookup.py - Look up Twitter handles for a list of addresses
# Install: pip install x402>=1.0.0 eth-account requests

import os
import time
from eth_account import Account
from x402.clients.requests import x402_requests

# CONFIG - Set these!
PRIVATE_KEY = os.environ['PRIVATE_KEY']  # Your private key
ADDRESSES = [
    '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045',  # vitalik.eth
    '0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B',
    # Add more addresses here (up to 50 per batch)
]

BASE_URL = 'https://bluepages.fyi'
account = Account.from_key(PRIVATE_KEY)

def make_request(method, endpoint, data=None):
    """IMPORTANT: Create fresh session for each request (x402 quirk)"""
    session = x402_requests(account)
    url = f'{BASE_URL}{endpoint}'
    if method == 'POST':
        return session.post(url, json=data)
    return session.get(url)

def lookup_addresses(addresses):
    results = {}
    
    # Process in batches of 50
    for i in range(0, len(addresses), 50):
        batch = addresses[i:i+50]
        print(f'Processing batch {i//50 + 1}...')
        
        # Step 1: Check which addresses have data ($0.04)
        check_resp = make_request('POST', '/batch/check', {'addresses': batch})
        check_data = check_resp.json()
        
        found_addresses = []
        for addr, info in check_data.get('results', {}).get('addresses', {}).items():
            if info.get('exists'):
                found_addresses.append(addr)
                results[addr] = {'exists': True, 'twitter': info.get('twitter')}
            else:
                results[addr] = {'exists': False}
        
        # Step 2: Get full data for found addresses ($2.00)
        if found_addresses:
            time.sleep(2)  # Rate limit: 30 req/min for x402
            
            data_resp = make_request('POST', '/batch/data', {'addresses': found_addresses})
            data_json = data_resp.json()
            
            for addr, info in data_json.get('results', {}).get('addresses', {}).items():
                if info.get('found') and info.get('primary'):
                    results[addr] = {
                        'exists': True,
                        'twitter': info['primary'].get('twitter'),
                        'source': info['primary'].get('metadata', {}).get('source'),
                        'alternates': len(info.get('alternates', []))
                    }
        
        # Rate limit between batches
        if i + 50 < len(addresses):
            time.sleep(2)
    
    return results

# Run it
if __name__ == '__main__':
    results = lookup_addresses(ADDRESSES)
    print('\n=== Results ===')
    for addr, data in results.items():
        if data.get('twitter'):
            print(f"{addr[:10]}... → {data['twitter']} ({data.get('source')})")
        elif data.get('exists'):
            print(f"{addr[:10]}... → has data but no Twitter")
        else:
            print(f"{addr[:10]}... → not found")
💰 Cost Breakdown (per 50 addresses):
  • /batch/check: $0.04 flat
  • /batch/data: $2.00 (only call if check found results)
  • Total: $2.04 for 50 addresses (vs $2.50 with individual calls)
⚡ Recommended Workflow (Multiple Addresses)
Always use batch endpoints for multiple addresses!
Individual /data calls cost $0.05 each. Batch endpoints are faster and cheaper.

Two-phase workflow:

  1. POST /batch/check — Find which addresses have data ($0.04 per batch of 50)
  2. POST /batch/data — Get full data for found addresses only
// Recommended: Batch workflow for multiple addresses
const API_KEY = 'bp_your_key_here';
const BASE_URL = 'https://bluepages.fyi';

async function lookupMany(addresses) {
  // Phase 1: Check which addresses have data
  const checkRes = await fetch(`${BASE_URL}/batch/check`, {
    method: 'POST',
    headers: { 
      'X-API-KEY': API_KEY, 
      'Content-Type': 'application/json' 
    },
    body: JSON.stringify({ addresses })
  });
  const checkData = await checkRes.json();
  
  // Filter to addresses that have data
  const foundAddresses = Object.entries(checkData.results.addresses)
    .filter(([addr, info]) => info.exists)
    .map(([addr]) => addr);
  
  if (foundAddresses.length === 0) {
    console.log('No addresses found in database');
    return [];
  }
  
  console.log(`Found ${foundAddresses.length}/${addresses.length} addresses with data`);
  
  // Phase 2: Get full data for found addresses only
  const dataRes = await fetch(`${BASE_URL}/batch/data`, {
    method: 'POST',
    headers: { 
      'X-API-KEY': API_KEY, 
      'Content-Type': 'application/json' 
    },
    body: JSON.stringify({ addresses: foundAddresses })
  });
  
  return await dataRes.json();
}

// Usage
const results = await lookupMany([
  '0x1234...',
  '0x5678...',
  // ... up to 50 addresses per batch
]);
💡 Why batch?
  • 50 individual /data calls = 50 × $0.05 = $2.50
  • 1 batch/check + 1 batch/data = $0.04 + $2.00 = $2.04
  • With API key (only pay for found): Even cheaper if <50 addresses have data!
⏱️ Rate Limits: Add delays between batch calls!
  • API Key: 60 req/min → 1 second between calls
  • x402: 30 req/min → 2 seconds between calls
Endpoints
GET /check $0.001

Check if an address or identity exists in the database. Works with any username type.

Query params: ?address=0x... or ?identity=@handle

{
  "exists": true,
  "types": ["twitter", "farcaster"],
  "message": "✓ Found in database. Use /data endpoint ($0.05) to get full details."
}
GET /data $0.05

Get all identities and cluster information for a single address or identity.

Processing multiple addresses? Use /batch/data instead — it's faster and cheaper!

Query params: ?address=0x... or ?identity=@handle

Optional: &maxClusterSize=100, &fullCluster=true

Address Lookup Response:

{
  "found": true,
  "address": "0xabcd1234abcd1234abcd1234abcd1234abcd1234",
  "identities": [
    { "type": "twitter", "value": "@example", "source": "uniswap-sybil" },
    { "type": "twitter", "value": "@example", "source": "tally" }
  ],
  "cluster": {
    "id": "twitter:@example",
    "source": "shared_twitter",
    "transitive": false,
    "identified": true,
    "totalAddresses": 2,
    "addresses": ["0xabcd1234...", "0xef567890..."],
    "truncated": false,
    "rawData": { "sharedTwitter": "@example" }
  }
}

Identity Search Response:

{
  "found": true,
  "totalMatches": 2,
  "results": [
    {
      "matchType": "twitter",
      "matchedValue": "@example",
      "address": "0xabcd1234...",
      "identities": [{ "type": "twitter", "value": "@example", "source": "tally" }],
      "cluster": { ... },
      "sources": ["tally"]
    },
    {
      "matchType": "twitter",
      "matchedValue": "@example", 
      "address": "0xef567890...",
      "identities": [...],
      "cluster": { ... },
      "sources": ["uniswap-sybil", "farcaster"]
    }
  ]
}
📋 Response Field Reference
transitive If true, cluster was merged from multiple sources
identified If true, cluster has a known identity (name/username)
truncated If true, address list was cut off. Use fullCluster=true to get all
rawData Additional cluster metadata from the source
source How cluster was detected: clusters.xyz or shared_twitter
POST /batch/check $0.04

Batch check up to 50 addresses or identities at once.

// Request
{
  "addresses": ["0x1234...", "0x5678..."],
  "twitters": ["@user1", "@user2"]
}

// Response
{
  "success": true,
  "timestamp": "2025-12-23T12:00:00.000Z",
  "totalItems": 4,
  "results": {
    "addresses": {
      "0x1234...": { "exists": true, "twitter": true, "farcaster": false },
      "0x5678...": { "exists": false, "twitter": false, "farcaster": false }
    },
    "twitters": {
      "@user1": { "exists": true, "twitter": true, "farcaster": false },
      "@user2": { "exists": false, "twitter": false, "farcaster": false }
    }
  }
}

Response fields:

  • exists - Whether any identity data was found
  • twitter - Whether Twitter data specifically exists
  • farcaster - Whether Farcaster data specifically exists
POST /batch/data $2.00

Batch retrieve full data for up to 50 items. API key users only charged for results found.

⚠️ Different response format than /data! Uses primary/alternates structure, not identities[].
// Request
{
  "addresses": ["0x1234...", "0x5678..."],
  "twitters": ["@user1"]  // optional
}

// Response
{
  "success": true,
  "timestamp": "2025-12-27T12:00:00.000Z",
  "totalItems": 3,
  "results": {
    "addresses": {
      "0x1234...": {
        "found": true,
        "totalResults": 2,
        "primary": {
          "address": "0x1234...",
          "twitter": "@example_user",
          "metadata": {
            "displayName": "Example User",
            "source": "farcaster",
            "ens": "example.eth"
          },
          "priority": 60
        },
        "alternates": [
          {
            "address": "0x1234...",
            "twitter": "@example_alt",
            "metadata": { "source": "layer3" },
            "priority": 100
          }
        ]
      },
      "0x5678...": {
        "found": false
      }
    },
    "twitters": {
      "@user1": {
        "found": true,
        "totalResults": 1,
        "primary": {
          "address": "0xabcd...",
          "twitter": "@user1",
          "metadata": { "source": "uniswap-sybil" },
          "priority": 50
        },
        "alternates": []
      }
    }
  }
}

Response fields:

  • primary - Best match (lowest priority score = most trusted)
  • alternates - Other matches from different sources
  • priority - Source trust ranking (lower = better): farcaster(60) < tally(70) < layer3(100)
  • metadata - Additional info: displayName, ens, source

Note: This format differs from /data which returns identities[] array. For batch operations, use primary.twitter to get the best Twitter handle.

POST /my-data FREE (1/min)

Look up your own data by signing a message from your wallet. Rate limited to prevent abuse.

// Request
{
  "address": "0x1234567890abcdef1234567890abcdef12345678",
  "signature": "0x..."
}
💰 Recommended Flow for x402 Users
  1. Use /check first ($0.001) to verify data exists
  2. If data exists, use /data ($0.05) for full details

This saves money when addresses aren't in the database.

💡 What are Clusters?
Clusters group addresses controlled by the same entity. We detect them via:
  • clusters.xyz - Named clusters with multiple verified wallets
  • verified sources - Addresses linked through verified identity data
Quick Start (API Key)

The fastest way to get started. Get your API key first →

# Check your credits balance
curl -H "X-API-KEY: bp_your_key_here" \
  "https://bluepages.fyi/api/me"

# Check if an address exists
curl -H "X-API-KEY: bp_your_key_here" \
  "https://bluepages.fyi/check?address=0x1234567890abcdef1234567890abcdef12345678"

# Get full data (only charged if data found)
curl -H "X-API-KEY: bp_your_key_here" \
  "https://bluepages.fyi/data?address=0x1234567890abcdef1234567890abcdef12345678"

# Batch check 50 addresses at once
curl -X POST -H "X-API-KEY: bp_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"addresses": ["0xd8dA...", "0x742d..."]}' \
  "https://bluepages.fyi/batch/check"

Response headers show credit usage:

X-Credits-Used: 1
X-Credits-Remaining: 49999
X-Points-Earned: 1
JavaScript Example
// With API Key (recommended)
const API_KEY = process.env.BLUEPAGES_API_KEY;

async function checkAddress(address) {
  const res = await fetch(
    `https://bluepages.fyi/check?address=${address}`,
    { headers: { 'X-API-KEY': API_KEY } }
  );
  return res.json();
}

async function getData(address) {
  const res = await fetch(
    `https://bluepages.fyi/data?address=${address}`,
    { headers: { 'X-API-KEY': API_KEY } }
  );
  // Note: Only charged if data is found!
  return res.json();
}

async function batchCheck(addresses) {
  const res = await fetch('https://bluepages.fyi/batch/check', {
    method: 'POST',
    headers: {
      'X-API-KEY': API_KEY,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ addresses })
  });
  return res.json();
}

// Usage
const result = await checkAddress('0x1234567890abcdef1234567890abcdef12345678');
console.log(result);

💳 With x402 (pay-per-request):

// Install: npm install viem x402-fetch@^1.1.0
// Requires: USDC on Base mainnet in your wallet

import { createWalletClient, http } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { base } from 'viem/chains';
import { wrapFetchWithPayment } from 'x402-fetch';

// Create viem wallet client (x402-fetch requires viem, not ethers!)
const account = privateKeyToAccount(process.env.PRIVATE_KEY);
const walletClient = createWalletClient({
  account,
  chain: base,
  transport: http()
});

// Create x402-enabled fetch (max $3 per request - batch/data costs $2)
const x402Fetch = wrapFetchWithPayment(fetch, walletClient, BigInt(3_000_000));

// Check if address exists ($0.001)
async function checkAddress(address) {
  const res = await x402Fetch(
    `https://bluepages.fyi/check?address=${address}`,
    { method: 'GET' }
  );
  return res.json();
}

// Get full data ($0.05)
async function getData(address) {
  const res = await x402Fetch(
    `https://bluepages.fyi/data?address=${address}`,
    { method: 'GET' }
  );
  return res.json();
}

// Batch check up to 50 addresses ($0.04)
async function batchCheck(addresses) {
  const res = await x402Fetch('https://bluepages.fyi/batch/check', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ addresses })
  });
  return res.json();
}

// Batch get data for up to 50 addresses ($2.00)
async function batchData(addresses) {
  const res = await x402Fetch('https://bluepages.fyi/batch/data', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ addresses })
  });
  return res.json();
}

// Usage example
const exampleAddr = '0x1234567890abcdef1234567890abcdef12345678';

// First check if data exists ($0.001)
const check = await checkAddress(exampleAddr);
console.log('Exists:', check.exists);
console.log('Types found:', check.types); // e.g. ['twitter', 'farcaster']

// If exists, get full data ($0.05)
if (check.exists) {
  const data = await getData(exampleAddr);
  
  // New response format with identities array
  console.log('Address:', data.address);
  console.log('Identities:', data.identities);
  // e.g. [{ type: 'twitter', value: '@example', source: 'tally' }]
  
  // Get Twitter handle
  const twitter = data.identities.find(i => i.type === 'twitter');
  if (twitter) console.log('Twitter:', twitter.value);
  
  // Check cluster info
  if (data.cluster) {
    console.log('Cluster ID:', data.cluster.id);
    console.log('Cluster addresses:', data.cluster.addresses);
    console.log('Truncated:', data.cluster.truncated);
  }
}

// Batch example
const addresses = [exampleAddr, '0xabcdef1234567890abcdef1234567890abcdef12'];
const batchResult = await batchCheck(addresses);
console.log('Batch results:', batchResult.results);
Python Example
# With API Key (recommended) - pip install requests
import os
import requests
import time

API_KEY = os.environ['BLUEPAGES_API_KEY']
BASE_URL = 'https://bluepages.fyi'
HEADERS = {'X-API-KEY': API_KEY}

# Your addresses to check
addresses = ['0x1234567890abcdef1234567890abcdef12345678', ...]

# Phase 1: Batch check which addresses have data (40 credits/found + 1 credit/not-found)
found = []
for i in range(0, len(addresses), 50):
    batch = addresses[i:i+50]
    resp = requests.post(f'{BASE_URL}/batch/check', 
                        headers=HEADERS, 
                        json={'addresses': batch})
    if resp.status_code == 200:
        data = resp.json()
        for addr, info in data['results']['addresses'].items():
            # Check if data exists (twitter or farcaster)
            if info.get('exists') and (info.get('twitter') or info.get('farcaster')):
                found.append(addr)
    # Check remaining credits
    print(f"Credits remaining: {resp.headers.get('X-Credits-Remaining')}")
    time.sleep(1.0)  # API key rate limit: 60 req/min

print(f'Found {len(found)} addresses with data')

# Phase 2: Fetch full data (2000 credits per batch, only if data found)
all_data = []
for i in range(0, len(found), 50):
    batch = found[i:i+50]
    resp = requests.post(f'{BASE_URL}/batch/data',
                        headers=HEADERS,
                        json={'addresses': batch})
    if resp.status_code == 200:
        data = resp.json()
        for addr, info in data['results']['addresses'].items():
            if info.get('found'):
                # batch/data returns primary.twitter format
                record = {
                    'address': addr,
                    'twitter': info['primary'].get('twitter'),
                    'display_name': info['primary'].get('metadata', {}).get('displayName'),
                    'source': info['primary'].get('metadata', {}).get('source')
                }
                all_data.append(record)
    time.sleep(1.0)  # API key rate limit: 60 req/min

print(f'Saved {len(all_data)} records')

💳 With x402 (pay-per-request):

⚠️ Python x402 quirk: Create a fresh session for each request. The x402 library requires this for payment attestations to work correctly.
# Install: pip install x402>=1.0.0 eth-account requests
# Requires: Python 3.10+, USDC on Base mainnet

import os
import json
import time
from eth_account import Account
from x402.clients.requests import x402_requests

# Your private key (use env vars, never hardcode!)
PRIVATE_KEY = os.environ['PRIVATE_KEY']
BASE_URL = 'https://bluepages.fyi'

# Rate limit: x402 = 30 req/min = 2 seconds between requests
RATE_LIMIT_DELAY = 2.0

# Create account from private key
account = Account.from_key(PRIVATE_KEY)

def make_request(method, endpoint, data=None):
    """
    Make x402-authenticated request.
    IMPORTANT: Create fresh session for each request!
    Payment attestations are single-use.
    """
    session = x402_requests(account)
    url = f'{BASE_URL}{endpoint}'
    
    if method == 'GET':
        return session.get(url)
    else:
        return session.post(url, json=data)

# Check if address exists ($0.001)
def check_address(address):
    resp = make_request('GET', f'/check?address={address}')
    return resp.json()

# Get full data ($0.05)
def get_data(address):
    resp = make_request('GET', f'/data?address={address}')
    return resp.json()

# Batch check up to 50 addresses ($0.04)
def batch_check(addresses):
    resp = make_request('POST', '/batch/check', {'addresses': addresses})
    return resp.json()

# Batch get data for up to 50 addresses ($2.00)
def batch_data(addresses):
    resp = make_request('POST', '/batch/data', {'addresses': addresses})
    return resp.json()

# === Usage Example ===

# Single address lookup
example_addr = '0x1234567890abcdef1234567890abcdef12345678'

# First check if exists ($0.001)
result = check_address(example_addr)
print(f"Exists: {result['exists']}")
print(f"Types: {result.get('types', [])}")  # e.g. ['twitter', 'farcaster']

# If exists, get full data ($0.05)
if result['exists']:
    data = get_data(example_addr)
    
    # New response format
    print(f"Address: {data['address']}")
    print(f"Identities: {data['identities']}")
    
    # Extract Twitter handle
    twitter = next((i for i in data['identities'] if i['type'] == 'twitter'), None)
    if twitter:
        print(f"Twitter: {twitter['value']}")
    
    # Check cluster info
    if data.get('cluster'):
        print(f"Cluster: {data['cluster']['id']}")
        print(f"Cluster size: {data['cluster']['totalAddresses']}")
        print(f"Truncated: {data['cluster']['truncated']}")

# === Batch Processing Example ===

addresses = [
    '0x1234567890abcdef1234567890abcdef12345678',
    '0xabcdef1234567890abcdef1234567890abcdef12',
    # ... more addresses
]

# Phase 1: Find which addresses have data
found = []
for i in range(0, len(addresses), 50):
    batch = addresses[i:i+50]
    result = batch_check(batch)
    
    for addr, info in result['results']['addresses'].items():
        if info.get('exists') and (info.get('twitter') or info.get('farcaster')):
            found.append(addr)
    
    time.sleep(RATE_LIMIT_DELAY)  # x402: 30 req/min

print(f'Found {len(found)} addresses with data')

# Phase 2: Get full data for found addresses
all_data = []
for i in range(0, len(found), 50):
    batch = found[i:i+50]
    result = batch_data(batch)
    
    for addr, info in result['results']['addresses'].items():
        if info.get('found'):
            all_data.append({'address': addr, **info['primary']})
    
    time.sleep(RATE_LIMIT_DELAY)  # x402: 30 req/min

# Save results
with open('results.json', 'w') as f:
    json.dump(all_data, f, indent=2)

print(f'Saved {len(all_data)} records to results.json')

Download full example →

Pricing

🔑 API Key Credits (prepaid, never expire)

Endpoint Credits Effective Price* Note
/check 1 $0.001 Always charged
/data 50 $0.05 Only if data found
/batch/check 40 $0.04 Up to 50 items
/batch/data 40 per result $0.04/each Only for addresses with data

💡 Tip: Skip /check — just call /data directly. You only pay for addresses that have data.

*Based on Starter package: 5,000 credits = $5

📦 Credit Packages

Package Credits Price Per Credit Savings
Starter 5,000 $5 $0.001
Pro 50,000 $45 $0.0009 10%
Enterprise 1,000,000 $600 $0.0006 40%

Purchase credits →

💳 x402 Pay-per-request

Endpoint Price
/check $0.001
/data $0.05
/batch/check $0.04
/batch/data $2.00
Rate Limits
💰 Cost Estimation

Use these formulas to estimate costs before running batch jobs:

🔑 API Key Users (Recommended)
Phase 1 - Check which addresses have data:
  batches = ceil(addresses / 50)
  cost = batches × 40 credits × $0.001 = batches × $0.04

Phase 2 - Get data for found addresses:
  cost = found_addresses × 40 credits × $0.001 = found × $0.04

Example: 400 addresses, 80 have data (20% hit rate)
  Phase 1: ceil(400/50) × $0.04 = 8 × $0.04 = $0.32
  Phase 2: 80 × $0.04 = $3.20
  Total: $3.52
💳 x402 Users
Phase 1 - Check:
  batches = ceil(addresses / 50)
  cost = batches × $0.04

Phase 2 - Get data:
  batches = ceil(found_addresses / 50)
  cost = batches × $2.00  (flat rate per batch)

Example: 400 addresses, 80 have data (20% hit rate)
  Phase 1: 8 × $0.04 = $0.32
  Phase 2: ceil(80/50) × $2.00 = 2 × $2.00 = $4.00
  Total: $4.32
⚠️ Key Difference: API key users pay per-address for /batch/data (40 credits each, only for found data). x402 users pay a flat $2.00 per batch regardless of results. API keys are cheaper for batch operations.
🤔 Which Payment Method?
Use Case Recommendation Why
Batch processing (50+ addresses) 🔑 API Key Pay only for found data, 2x rate limits
Single lookups / testing Either Similar cost, x402 needs no setup
AI/MCP integration 🔑 API Key No wallet signing during conversations
One-time scripts 💳 x402 No account needed, pay-as-you-go

Decision Tree:

Do you need to process more than 50 addresses?
  YES → Use API Key (cheaper batch pricing)
  NO  → Do you want to avoid wallet setup?
          YES → Use x402 (instant, no account)
          NO  → Use API Key (faster, no signing)
⚠️ x402 Error Handling

When using x402 payments, you may encounter these errors:

Payment Required (402)

{
  "x402Version": 1,
  "error": "X-PAYMENT header is required",
  "accepts": [{
    "scheme": "exact",
    "network": "base",
    "maxAmountRequired": "1000",
    "resource": "https://bluepages.fyi/check",
    "payTo": "0xc21bDbba05d3662f0F63AC6d209C6cb17E61aAD3",
    "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
  }]
}

Solution: Include the X-PAYMENT header with a valid payment attestation. Use the x402-fetch library.

Insufficient Funds

{
  "x402Version": 1,
  "error": "insufficient_funds",
  "payer": "0x..."
}

Solution: Add more USDC to your wallet on Base mainnet.

Payment Rejected

{
  "x402Version": 1,
  "error": "payment_rejected",
  "reason": "signature_invalid"
}

Solution: Create a fresh x402 session. Don't reuse payment attestations across requests.

💡 Why "fresh session for each request"?
x402 payment attestations are single-use. Each request needs a new attestation signed by your wallet. The x402-fetch library handles this automatically, but if you're implementing manually, ensure you generate a new attestation for every API call.
🔐 x402 Protocol Specification

For those implementing x402 manually (without x402-fetch), here's the complete protocol spec. x402 uses USDC's EIP-3009 TransferWithAuthorization for gasless payments.

X-PAYMENT Header Format

The header must be a base64-encoded JSON string:

{
  "x402Version": 1,
  "scheme": "exact",
  "network": "base",
  "payload": {
    "signature": "0x...",           // EIP-712 signature (65 bytes hex)
    "authorization": {
      "from": "0x...",              // Your wallet address (payer)
      "to": "0x...",                // payTo address from 402 response
      "value": "1000",              // Amount in USDC units (6 decimals, e.g. "1000" = $0.001)
      "validAfter": "1703700000",   // Unix timestamp (usually now - 600)
      "validBefore": "1703703600",  // Unix timestamp (usually now + maxTimeoutSeconds)
      "nonce": "0x..."              // 32-byte random hex (unique per request)
    }
  }
}

EIP-712 Typed Data (for signing)

USDC on Base uses EIP-3009 TransferWithAuthorization:

// Domain
{
  name: "USD Coin",
  version: "2",
  chainId: 8453,                    // Base mainnet
  verifyingContract: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"  // USDC on Base
}

// 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" }
  ]
}

// Message (same as authorization object above)
{
  from: "0xYourWallet...",
  to: "0xPayToAddress...",
  value: 1000n,                     // BigInt
  validAfter: 1703700000n,
  validBefore: 1703703600n,
  nonce: "0x..." // 32 random bytes
}

Complete Manual Implementation (JavaScript)

import { createWalletClient, http, encodeFunctionData } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { base } from 'viem/chains';
import crypto from 'crypto';

const USDC_ADDRESS = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';

// EIP-712 domain for USDC on Base
const domain = {
  name: 'USD Coin',
  version: '2',
  chainId: 8453,
  verifyingContract: USDC_ADDRESS
};

const 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' }
  ]
};

async function createPaymentHeader(walletClient, paymentInfo) {
  const now = Math.floor(Date.now() / 1000);
  const nonce = '0x' + crypto.randomBytes(32).toString('hex');
  
  const authorization = {
    from: walletClient.account.address,
    to: paymentInfo.payTo,
    value: BigInt(paymentInfo.maxAmountRequired),
    validAfter: BigInt(now - 600),      // 10 min before
    validBefore: BigInt(now + paymentInfo.maxTimeoutSeconds),
    nonce
  };

  // Sign with EIP-712
  const signature = await walletClient.signTypedData({
    domain,
    types,
    primaryType: 'TransferWithAuthorization',
    message: authorization
  });

  // Build x402 payload
  const payload = {
    x402Version: 1,
    scheme: paymentInfo.scheme,
    network: paymentInfo.network,
    payload: {
      signature,
      authorization: {
        from: authorization.from,
        to: authorization.to,
        value: authorization.value.toString(),
        validAfter: authorization.validAfter.toString(),
        validBefore: authorization.validBefore.toString(),
        nonce
      }
    }
  };

  return Buffer.from(JSON.stringify(payload)).toString('base64');
}

// Usage
async function makePayment(url) {
  const account = privateKeyToAccount(process.env.PRIVATE_KEY);
  const walletClient = createWalletClient({ account, chain: base, transport: http() });
  
  // Step 1: Get 402 response
  const res = await fetch(url);
  if (res.status !== 402) return res;
  
  const paymentInfo = (await res.json()).accepts[0];
  
  // Step 2: Create and sign payment
  const xPayment = await createPaymentHeader(walletClient, paymentInfo);
  
  // Step 3: Retry with payment
  return fetch(url, { headers: { 'X-PAYMENT': xPayment } });
}
✅ Pre-flight Checklist

Before using x402:

  1. Have USDC on Base mainnet (not Ethereum mainnet)
  2. Estimate costs using formulas above
  3. Ensure wallet has at least 10% buffer for gas and failed attempts
  4. Test with a single /check request ($0.001) first

Check your USDC balance:

// JavaScript (using viem - same library as x402-fetch)
import { createPublicClient, http, formatUnits } from 'viem';
import { base } from 'viem/chains';

const USDC_BASE = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
const client = createPublicClient({ chain: base, transport: http() });

const balance = await client.readContract({
  address: USDC_BASE,
  abi: [{ name: 'balanceOf', type: 'function', inputs: [{ type: 'address' }], outputs: [{ type: 'uint256' }] }],
  functionName: 'balanceOf',
  args: [YOUR_ADDRESS]
});
console.log('USDC Balance:', formatUnits(balance, 6));
# Python
from web3 import Web3
USDC_BASE = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'
w3 = Web3(Web3.HTTPProvider('https://mainnet.base.org'))
usdc = w3.eth.contract(address=USDC_BASE, abi=[{"constant":True,"inputs":[{"name":"account","type":"address"}],"name":"balanceOf","outputs":[{"name":"","type":"uint256"}],"type":"function"}])
balance = usdc.functions.balanceOf(YOUR_ADDRESS).call()
print(f'USDC Balance: {balance / 1e6}')

Before using API Keys:

  1. Get an API key at /api-keys.html
  2. Purchase credits (minimum $5 for 5,000 credits)
  3. Check your balance: curl -H "X-API-KEY: bp_..." https://bluepages.fyi/api/me
Downloadable Examples

🔑 API Key Examples (Recommended)

💳 x402 Examples

⚠️ Security: Never hardcode private keys or API keys. Use environment variables.
🤖 MCP Server (AI Integration)

Bluepages provides an MCP (Model Context Protocol) server that allows AI assistants like Claude to look up addresses and Twitter handles directly.

What is MCP?
MCP is a protocol that lets AI assistants connect to external tools. With the Bluepages MCP server, Claude can run address lookups during conversations without you writing any code.

Quick Setup for Claude Desktop:

{
  "mcpServers": {
    "bluepages": {
      "command": "node",
      "args": ["/path/to/bluepages/mcp-server.js"],
      "env": {
        "BLUEPAGES_API_KEY": "your-api-key-here"
      }
    }
  }
}

Available MCP Tools:

Tool Description Cost
check_address Check if address exists 1 credit
check_twitter Check if Twitter exists 1 credit
get_data_for_address Get Twitter for address 50 credits*
get_data_for_twitter Get address for Twitter 50 credits*
batch_check Check up to 50 items 40/found + 1/not-found
batch_get_data Get data for up to 50 items 40/found*
check_credits Check remaining credits Free

* Only charged if data is found

Learn more about MCP →  |  Full MCP Documentation →