Programmatic access to bluepages.fyi
Query crypto address ↔ Twitter mappings via REST API. Two payment options:
Add your API key to the X-API-KEY header on every request:
# cURL
curl -H "X-API-KEY: bp_your_key_here" \
"https://bluepages.fyi/check?address=0x..."
# JavaScript
fetch('https://bluepages.fyi/check?address=0x...', {
headers: { 'X-API-KEY': 'bp_your_key_here' }
})
# Python
requests.get('https://bluepages.fyi/check',
params={'address': '0x...'},
headers={'X-API-KEY': 'bp_your_key_here'}
)
Response headers show your credit usage:
X-Credits-Used: 1
X-Credits-Remaining: 4999
X-Points-Earned: 1
Don't have an API key? Get one here →
Have a private key + USDC on Base? Here's everything you need.
📋 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
];
// 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 = {};
// ============================================================
// PHASE 1: Check ALL addresses, collect found ones globally
// This is critical for cost efficiency!
// ============================================================
console.log('Phase 1: Checking which addresses exist...');
const allFoundAddresses = []; // Collect ALL found addresses across batches
for (let i = 0; i < addresses.length; i += 50) {
const batch = addresses.slice(i, i + 50);
console.log(` Checking batch ${Math.floor(i/50) + 1}/${Math.ceil(addresses.length/50)}...`);
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();
// Collect found addresses and store initial results
for (const [addr, info] of Object.entries(checkData.results?.addresses || {})) {
if (info.exists) {
allFoundAddresses.push(addr); // Add to global list
results[addr] = { exists: true };
} else {
results[addr] = { exists: false };
}
}
// Rate limit between check batches
if (i + 50 < addresses.length) await sleep(2000);
}
console.log(`Found ${allFoundAddresses.length} addresses with data`);
// ============================================================
// PHASE 2: Fetch data for ALL found addresses in batches of 50
// batch/data costs $2.00 flat - maximize addresses per call!
// ============================================================
if (allFoundAddresses.length === 0) {
console.log('No addresses found, skipping data fetch');
return results;
}
console.log('Phase 2: Fetching full data...');
for (let i = 0; i < allFoundAddresses.length; i += 50) {
const batch = allFoundAddresses.slice(i, i + 50);
console.log(` Fetching data ${Math.floor(i/50) + 1}/${Math.ceil(allFoundAddresses.length/50)} (${batch.length} addresses)...`);
await sleep(2000); // Rate limit
const dataRes = await fetchWithPayment('https://bluepages.fyi/batch/data', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ addresses: batch })
});
const dataJson = await dataRes.json();
// Update results with full data
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
};
}
}
}
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
]
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 payments are single-use)"""
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 = {}
# ============================================================
# PHASE 1: Check ALL addresses, collect found ones globally
# This is critical for cost efficiency!
# ============================================================
print('Phase 1: Checking which addresses exist...')
all_found_addresses = [] # Collect ALL found addresses across batches
for i in range(0, len(addresses), 50):
batch = addresses[i:i+50]
print(f' Checking batch {i//50 + 1}/{-(-len(addresses)//50)}...')
check_resp = make_request('POST', '/batch/check', {'addresses': batch})
check_data = check_resp.json()
for addr, info in check_data.get('results', {}).get('addresses', {}).items():
if info.get('exists'):
all_found_addresses.append(addr) # Add to global list
results[addr] = {'exists': True}
else:
results[addr] = {'exists': False}
# Rate limit between check batches
if i + 50 < len(addresses):
time.sleep(2)
print(f'Found {len(all_found_addresses)} addresses with data')
# ============================================================
# PHASE 2: Fetch data for ALL found addresses in batches of 50
# batch/data costs $2.00 flat - maximize addresses per call!
# ============================================================
if not all_found_addresses:
print('No addresses found, skipping data fetch')
return results
print('Phase 2: Fetching full data...')
for i in range(0, len(all_found_addresses), 50):
batch = all_found_addresses[i:i+50]
print(f' Fetching data {i//50 + 1}/{-(-len(all_found_addresses)//50)} ({len(batch)} addresses)...')
time.sleep(2) # Rate limit
data_resp = make_request('POST', '/batch/data', {'addresses': batch})
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', []))
}
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")
/batch/data costs $2.00 flat whether you fetch 1 or 50 addresses!
| Scenario | Inefficient ❌ | Two-Phase ✅ | Savings |
|---|---|---|---|
| 1,000 addrs, 30 found (3%) | 20×$0.04 + 20×$2 = $40.80 | 20×$0.04 + 1×$2 = $2.80 | 93% |
| 500 addrs, 50 found (10%) | 10×$0.04 + 10×$2 = $20.40 | 10×$0.04 + 1×$2 = $2.40 | 88% |
The lower your hit rate, the more you save. Most real-world use cases have 3-10% hit rates.
Two-phase workflow:
POST /batch/check — Find which addresses have data ($0.04 per batch of 50)
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
]);
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 all identities and cluster information for a single address or identity.
/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"]
}
]
}
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
|
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 foundtwitter - Whether Twitter data specifically existsfarcaster - Whether Farcaster data specifically existsBatch retrieve full data for up to 50 items. API key users only charged for results found.
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 sourcespriority - 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.
Look up your own data by signing a message from your wallet. Rate limited to prevent abuse.
// Request
{
"address": "0x1234567890abcdef1234567890abcdef12345678",
"signature": "0x..."
}
/check first ($0.001) to verify data exists/data ($0.05) for full detailsThis saves money when addresses aren't in the database.
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
// 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);
# 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):
# 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')
🔑 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% |
🤖 Programmatic Credit Purchase
Purchase credits via API using x402 payment. Requires: npm install x402-fetch@^1.1.0 viem
// JavaScript - Authenticate and purchase credits
import { createWalletClient, http } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { base } from 'viem/chains';
import { wrapFetchWithPayment } from 'x402-fetch';
const account = privateKeyToAccount(process.env.PRIVATE_KEY);
const walletClient = createWalletClient({ account, chain: base, transport: http() });
// Step 1: Authenticate to get API key
const message = `Sign this message to access your Bluepages API dashboard.
Address: ${account.address}
Timestamp: ${Date.now()}`;
const signature = await walletClient.signMessage({ message });
const authRes = await fetch('https://bluepages.fyi/api/auth', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ address: account.address, message, signature })
});
const { user } = await authRes.json();
console.log('API Key:', user.apiKey); // Save this!
// Step 2: Purchase credits with x402
// NOTE: wrapFetchWithPayment(fetch, walletClient, maxAmount) - fetch is first arg!
const paymentFetch = wrapFetchWithPayment(fetch, walletClient, BigInt(50_000_000));
const purchaseRes = await paymentFetch(
'https://bluepages.fyi/api/credits/purchase?package=starter', // starter|pro|enterprise
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ address: account.address })
}
);
const result = await purchaseRes.json();
// Response: { success, creditsAdded, newCredits, transactionHash }
console.log('Credits added:', result.creditsAdded);
console.log('New balance:', result.newCredits);
console.log('TX:', result.transactionHash);
# Python - Authenticate and purchase credits
# pip install eth-account requests x402>=1.0.0
from eth_account import Account
from eth_account.messages import encode_defunct
import requests, time
account = Account.from_key(PRIVATE_KEY)
# Step 1: Authenticate to get API key
message = f"""Sign this message to access your Bluepages API dashboard.
Address: {account.address}
Timestamp: {int(time.time() * 1000)}"""
signed = account.sign_message(encode_defunct(text=message))
signature = '0x' + signed.signature.hex()
res = requests.post('https://bluepages.fyi/api/auth', json={
'address': account.address,
'message': message,
'signature': signature
})
api_key = res.json()['user']['apiKey']
print(f'API Key: {api_key}') # Save this!
# Step 2: Purchase credits with x402
# Note: Python x402 library required for payment
from x402 import x402_requests
session = x402_requests(private_key=PRIVATE_KEY, max_amount=50_000_000)
res = session.post(
'https://bluepages.fyi/api/credits/purchase?package=starter',
json={'address': account.address}
)
result = res.json()
# Response: { success, creditsAdded, newCredits, transactionHash }
print(f"Credits: {result['creditsAdded']}, TX: {result['transactionHash']}")
💳 x402 Pay-per-request
| Endpoint | Price |
|---|---|
/check |
$0.001 |
/data |
$0.05 |
/batch/check |
$0.04 |
/batch/data |
$2.00 |
Use these formulas to estimate costs before running batch jobs:
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
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
| 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)
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.
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 } });
}
Before using x402:
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:
curl -H "X-API-KEY: bp_..." https://bluepages.fyi/api/me
🔑 API Key Examples (Recommended)
💳 x402 Examples
Bluepages provides an MCP (Model Context Protocol) server that allows AI assistants like Claude to look up addresses and Twitter handles directly.
/bluepages skill in one step:
/plugin marketplace add bluepagesdoteth/agent-plugins
/plugin install bluepages
Other MCP Clients (Claude Desktop, Cursor, etc.):
1. Add the MCP server to your client config (e.g. claude_desktop_config.json):
{
"mcpServers": {
"bluepages": {
"command": "npx",
"args": ["-y", "github:bluepagesdoteth/bluepages-mcp"],
"env": {
"BLUEPAGES_API_KEY": "your-api-key-here",
"PRIVATE_KEY": "your_eth_private_key_here"
}
}
}
}
2. Optionally, install the /bluepages skill for guided lookups:
npx skills add bluepagesdoteth/agent-plugins
BLUEPAGES_API_KEY — Prepaid credits, 20% cheaper (get one
here)
PRIVATE_KEY — Ethereum private key for x402 pay-per-request (USDC on Base)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 |
set_credit_alert |
Set low-credit warning threshold | Free |
* Only charged if data is found