Programmatic access to bluepages.fyi
Query crypto address ↔ Twitter mappings via REST API. Two payment options:
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 (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")
/batch/check: $0.04 flat/batch/data: $2.00 (only call if check found results)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% |
💳 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.
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