Programmatic access to bluepages.fyi
Query crypto address ↔ identity mappings via REST API. Two payment options:
Accept: text/markdown
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
Add your API key to the X-API-KEY header on every request:
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 →
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."
}
Note: Labels (CEX wallets, etc.) contribute to the
exists boolean but are not disclosed on /check. Use
/data to see label details.
Get all identities, labels, sanctions, 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", "priority": 20 },
{ "type": "twitter", "value": "example", "source": "tally", "priority": 20 }
],
"labels": [
{ "type": "cex", "name": "Binance", "detail": "Binance 1", "source": "hildobby", "priority": 71 }
],
"sanctions": [
{
"source": "ofac_sdn",
"entity": "CRYPTEX OTC S.R.O.",
"programs": ["CYBER2"],
"addedAt": "2024-09-26",
"removedAt": null,
"active": true
}
],
"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", "priority": 20 }],
"cluster": { ... }
},
{
"matchType": "twitter",
"matchedValue": "example",
"address": "0xef567890...",
"identities": [...],
"cluster": { ... }
}
]
}
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 the cluster was detected: clusters.xyz, shared_twitter, polymarket (proxy wallet ↔ owner) or shared_ip (legacy IP co-location import)
|
labels |
Array of labels (e.g., CEX wallet). Each has
type, name, detail, source, priority.
On /data and /my-data; not on /check.
|
sanctions |
Array of sanctions entries. Sources: ofac_sdn (US OFAC SDN), uk_ofsi (UK OFSI), il_nbctf (Israel NBCTF), jp_mof (Japan MOF), fr_tresor (France Trésor). Each has
source, entity, programs,
addedAt, removedAt, active.
On /data and /my-data; not on /check.
|
Batch check up to 50 addresses or identities at once.
// Request
{
"addresses": ["0x1234...", "0x5678..."],
"identities": ["@user1", "@user2"]
}
// Response
{
"success": true,
"timestamp": "2025-12-23T12:00:00.000Z",
"totalItems": 4,
"results": {
"addresses": {
"0x1234...": { "exists": true, "types": ["twitter", "farcaster"] },
"0x5678...": { "exists": false, "types": [] }
},
"identities": {
"@user1": { "exists": true, "types": ["twitter"] },
"@user2": { "exists": false, "types": [] }
}
}
}
Response fields:
exists - Whether any identity or label data was foundtypes - Identity types found for this entry (e.g. twitter, farcaster)error - Present instead of the above when the input is invalid
Note: Labels and sanctions contribute to exists but are not
individually disclosed on /batch/check.
Batch retrieve full data for up to 50 items. API key users only charged for results found.
// Request
{
"addresses": ["0x1234...", "0x5678..."],
"identities": ["@user1"] // optional
}
// Response
{
"success": true,
"timestamp": "2025-12-27T12:00:00.000Z",
"totalItems": 3,
"results": {
"addresses": {
"0x1234...": {
"found": true,
"address": "0x1234...",
"identities": [
{ "type": "twitter", "value": "example_user", "source": "neynar", "priority": 25 },
{ "type": "twitter", "value": "example_alt", "source": "layer3", "priority": 100 }
],
"labels": [
{ "type": "cex", "name": "Binance", "detail": "Binance 1", "source": "hildobby", "priority": 71 }
],
"sanctions": [],
"cluster": { ... }
},
"0x5678...": {
"found": false
}
},
"identities": {
"@user1": {
"found": true,
"totalMatches": 1,
"results": [
{
"matchType": "twitter",
"matchedValue": "user1",
"address": "0xabcd...",
"identities": [
{ "type": "twitter", "value": "user1", "source": "uniswap-sybil", "priority": 20 }
],
"labels": [],
"sanctions": [],
"cluster": null
}
]
}
}
}
}
Response fields:
/data (identities[], labels[], sanctions[], cluster) plus foundtotalMatches and results[], each result a /data-shaped record plus matchType/matchedValuepriority - Source trust ranking (lower = better): tally(20) < neynar(25) < layer3(100)error - Present instead of the above when the input is invalid
Look up your own data by signing a SIWE message (EIP-4361) from your wallet. The
message carries a one-time nonce from GET /api/nonce, an expiration
time, and this domain — so each signature is valid exactly once and cannot be
replayed. Rate limited to prevent abuse.
// Sign a SIWE message, then POST it
import { createSiweMessage } from 'viem/siwe';
const { nonce } = await (await fetch('https://bluepages.fyi/api/nonce')).json();
const message = createSiweMessage({
domain: 'bluepages.fyi',
address: account.address,
statement: 'View my Bluepages data.',
uri: 'https://bluepages.fyi',
version: '1',
chainId: 8453,
nonce,
issuedAt: new Date(),
expirationTime: new Date(Date.now() + 5 * 60 * 1000), // nonce valid 5 min
});
const signature = await walletClient.signMessage({ message });
// Request body
const res = await fetch('https://bluepages.fyi/my-data', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message, signature }) // the nonce is consumed on use
});
Response includes labels and sanctions arrays.
Labels and sanctions persist even for opted-out addresses.
Same flow for POST /opt-out — send
{ message, signature } (plus an optional twitter handle)
with the statement Opt out of Bluepages data.
The address is the one recovered from the signature — both endpoints take no
separate address field, and a body without message is
rejected with 400 Missing required fields: message, signature.
API-key users: GET /my-data with your X-API-KEY
header returns the data for the address linked to your key — no signature needed
(same 1/min limit).
GET /opt-out/status?address=0x… — whether an address has opted outGET /api/packages — credit packages and current pricingGET /api/me — your credit balance (send X-API-KEY)GET /openapi.json — machine-readable spec for every endpointx402 users: Use /check first ($0.001) to verify data exists before calling /data ($0.05). This saves money when addresses aren't in the database.
API key users: Call /data directly — you're only charged if data is found. For batch, call /batch/data directly — skip /batch/check.
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/found + 1/not-found | Variable | 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 v2 payment. Requires: npm install viem @x402/fetch@^2.24.0 @x402/evm@^2.24.0
Your API key is shown only once — on first registration, or when you call /api/regenerate-key. /api/auth does not return it again on later sign-ins; save it somewhere safe when you see it. Lost it? Regenerate — there is no way to retrieve the old one. If you purchased credits via x402 without ever registering (an auto-created account), call /api/regenerate-key to obtain your first key. /api/auth also returns a sessionToken — a short-lived (7-day) credential the web dashboard uses to spend credits; programmatic clients should ignore it and use the API key.
// JavaScript - Authenticate and purchase credits
import { privateKeyToAccount } from 'viem/accounts';
import { createSiweMessage } from 'viem/siwe';
import { wrapFetchWithPaymentFromConfig } from '@x402/fetch';
import { ExactEvmScheme } from '@x402/evm';
// privateKeyToAccount() returns a local account that can sign messages
// directly (SIWE below) — no wallet client / RPC transport needed for that.
const account = privateKeyToAccount(process.env.EVM_PRIVATE_KEY);
// Step 1: Authenticate with SIWE (EIP-4361) to get API key
const { nonce } = await (await fetch('https://bluepages.fyi/api/nonce')).json();
const message = createSiweMessage({
domain: 'bluepages.fyi',
address: account.address,
statement: 'Sign in to your Bluepages API dashboard.',
uri: 'https://bluepages.fyi',
version: '1',
chainId: 8453,
nonce,
issuedAt: new Date(),
expirationTime: new Date(Date.now() + 5 * 60 * 1000), // nonce valid 5 min
});
const signature = await account.signMessage({ message });
const authRes = await fetch('https://bluepages.fyi/api/auth', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message, signature })
});
const { user } = await authRes.json();
console.log('API Key:', user.apiKey); // Shown once (first registration / regenerate-key only) — save it!
// Step 2: Purchase credits with x402 v2
const paymentFetch = wrapFetchWithPaymentFromConfig(fetch, {
schemes: [{ network: 'eip155:8453', client: new ExactEvmScheme(account) }]
});
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
from datetime import datetime, timedelta, timezone
from eth_account import Account
from eth_account.messages import encode_defunct, encode_typed_data
import base64, json, os, secrets, time
import requests
EVM_PRIVATE_KEY = os.environ['EVM_PRIVATE_KEY'] # never hardcode
account = Account.from_key(EVM_PRIVATE_KEY)
# Step 1: Authenticate with SIWE (EIP-4361) to get API key
nonce = requests.get('https://bluepages.fyi/api/nonce').json()['nonce']
iso = lambda d: d.isoformat(timespec='milliseconds').replace('+00:00', 'Z')
now = datetime.now(timezone.utc)
message = f"""bluepages.fyi wants you to sign in with your Ethereum account:
{account.address}
Sign in to your Bluepages API dashboard.
URI: https://bluepages.fyi
Version: 1
Chain ID: 8453
Nonce: {nonce}
Issued At: {iso(now)}
Expiration Time: {iso(now + timedelta(minutes=5))}"""
signed = account.sign_message(encode_defunct(text=message))
signature = '0x' + signed.signature.hex()
res = requests.post('https://bluepages.fyi/api/auth', json={
'message': message,
'signature': signature
})
api_key = res.json()['user']['apiKey']
print(f'API Key: {api_key}') # Shown once (first registration / regenerate-key only) — save it!
# Step 2: Purchase credits with x402 v2
# Note: the x402 PyPI package's confirmed v2 API doesn't document a full
# "decode 402 -> sign -> retry" HTTP wrapper, so this builds the
# PAYMENT-SIGNATURE header manually (full helper: /examples/api-key-purchase.py)
def build_payment_header(payment_required):
accepted = payment_required['accepts'][0]
now = int(time.time())
nonce = '0x' + secrets.token_hex(32)
authorization = {
'from': account.address, 'to': accepted['payTo'], 'value': accepted['amount'],
'validAfter': str(now - 600), 'validBefore': str(now + accepted['maxTimeoutSeconds']),
'nonce': nonce,
}
extra = accepted.get('extra') or {}
typed_data = {
'types': {
'EIP712Domain': [
{'name': 'name', 'type': 'string'}, {'name': 'version', 'type': 'string'},
{'name': 'chainId', 'type': 'uint256'}, {'name': 'verifyingContract', 'type': 'address'},
],
'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'},
],
},
'domain': {
'name': extra.get('name', 'USD Coin'), 'version': extra.get('version', '2'),
'chainId': int(accepted['network'].split(':')[1]), 'verifyingContract': accepted['asset'],
},
'primaryType': 'TransferWithAuthorization',
'message': {
'from': authorization['from'], 'to': authorization['to'],
'value': int(authorization['value']), 'validAfter': int(authorization['validAfter']),
'validBefore': int(authorization['validBefore']), 'nonce': nonce,
},
}
signed = Account.sign_message(encode_typed_data(full_message=typed_data), EVM_PRIVATE_KEY)
signature = signed.signature.hex()
if not signature.startswith('0x'):
signature = '0x' + signature
payload = {
'x402Version': 2, 'resource': payment_required['resource'], 'accepted': accepted,
'payload': {'signature': signature, 'authorization': authorization},
}
return base64.b64encode(json.dumps(payload).encode()).decode()
purchase_url = 'https://bluepages.fyi/api/credits/purchase?package=starter'
purchase_body = {'address': account.address}
res = requests.post(purchase_url, json=purchase_body)
if res.status_code == 402:
h = res.headers.get('PAYMENT-REQUIRED') # authoritative (base64 JSON); the body mirrors it
payment_required = json.loads(base64.b64decode(h)) if h else res.json()
headers = {'PAYMENT-SIGNATURE': build_payment_header(payment_required)}
res = requests.post(purchase_url, json=purchase_body, headers=headers)
result = res.json()
if not res.ok: # a 402 here means the payment was rejected — see the server's reason
raise SystemExit(f"Purchase failed ({res.status_code}): {result.get('message') or result.get('error')}")
# 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 |
API Key Users: Skip the check phase
Call /batch/data directly — you're only charged for items that return data (40 credits each). No need to check first.
// API Key: Direct batch lookup (no check phase needed)
const API_KEY = 'bp_your_key_here';
const BASE_URL = 'https://bluepages.fyi';
async function lookupMany(addresses) {
// Go straight to /batch/data — only charged for found items
const dataRes = await fetch(`${BASE_URL}/batch/data`, {
method: 'POST',
headers: {
'X-API-KEY': API_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify({ addresses })
});
return await dataRes.json();
}
// Usage — up to 50 addresses per batch
const results = await lookupMany([
'0x1234...',
'0x5678...',
]);
x402 Users: Two-phase workflow
/batch/data costs a flat $2.00 regardless of results, so check first to avoid paying for empty batches.
POST /batch/check — Find which addresses have data ($0.04 per batch of 50)
POST /batch/data — Get full data for found addresses only ($2.00 per batch)Use these formulas to estimate costs before running batch jobs:
Call /batch/data directly — only charged for found items:
cost = found_items × 40 credits × $0.001/credit = found × $0.04
Example: 400 addresses, 80 have data (20% hit rate)
Direct: 80 × $0.04 = $3.20
Compare with two-phase (unnecessary for API key users):
Phase 1: /batch/check charges 40 credits per found + 1 per not-found
80 × 40 credits = $3.20 + 320 × 1 credit = $0.32 → $3.52
Phase 2: 80 × $0.04 = $3.20
Total: $6.72 (vs $3.20 direct — two-phase costs more than double)
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
/batch/check
entirely — /batch/data only charges for found items (40 credits each).
x402 users should use /batch/check first to avoid paying $2.00 for
mostly-empty batches.
| Use Case | Recommendation | Why |
|---|---|---|
| Batch processing (50+ addresses) | API Key | Pay only for found data, skip check phase, 2x rate limits |
| Single lookups / testing | Either | Similar cost, x402 needs no account |
| 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 skip creating an account?
YES → Use x402 (pay from a funded wallet, no sign-up)
NO → Use API Key (no per-request signing)
Have a private key + USDC on Base? Here's everything you need.
Complete JavaScript Script (copy-paste ready):
// batch-lookup.js - Look up identities for a list of addresses
// Install: npm install viem @x402/fetch@^2.24.0 @x402/evm@^2.24.0
import { privateKeyToAccount } from 'viem/accounts';
import { wrapFetchWithPaymentFromConfig } from '@x402/fetch';
import { ExactEvmScheme } from '@x402/evm';
// CONFIG - Set these!
const EVM_PRIVATE_KEY = process.env.EVM_PRIVATE_KEY; // Your private key (with 0x prefix)
const ADDRESSES = [
'0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', // vitalik.eth
'0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B',
// Add more addresses here
];
// Setup x402 v2 payment for Base mainnet (eip155:8453)
const account = privateKeyToAccount(EVM_PRIVATE_KEY);
const fetchWithPayment = wrapFetchWithPaymentFromConfig(fetch, {
schemes: [{ network: 'eip155:8453', client: new ExactEvmScheme(account) }]
});
// 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) {
// Identities are sorted by priority (lower = more trusted)
results[addr] = {
exists: true,
identities: info.identities.map(id => ({
type: id.type, value: id.value, source: id.source
}))
};
}
}
}
return results;
}
// Run it
lookupAddresses(ADDRESSES).then(results => {
console.log('\n=== Results ===');
for (const [addr, data] of Object.entries(results)) {
if (data.identities?.length) {
const summary = data.identities.map(id => `${id.type}:${id.value} (${id.source})`).join(', ');
console.log(`${addr.slice(0,10)}... → ${summary}`);
} else if (data.exists) {
console.log(`${addr.slice(0,10)}... → found but no identities`);
} else {
console.log(`${addr.slice(0,10)}... → not found`);
}
}
});
Complete Python Script:
# batch_lookup.py - Look up identities for a list of addresses
# Install: pip install eth-account requests
#
# Note: the x402 PyPI package's confirmed v2 API builds a payload from an
# already-decoded PaymentRequired object, but doesn't document a full
# "decode 402 -> sign -> retry" HTTP wrapper for requests. So make_request()
# below builds and sends the PAYMENT-SIGNATURE header manually.
import base64
import json
import os
import secrets
import time
import requests
from eth_account import Account
from eth_account.messages import encode_typed_data
# CONFIG - Set these!
EVM_PRIVATE_KEY = os.environ['EVM_PRIVATE_KEY'] # Your private key
ADDRESSES = [
'0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', # vitalik.eth
'0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B',
# Add more addresses here
]
BASE_URL = 'https://bluepages.fyi'
account = Account.from_key(EVM_PRIVATE_KEY)
def _select_requirements(payment_required):
for accepted in payment_required['accepts']:
if accepted.get('network') == 'eip155:8453':
return accepted
return payment_required['accepts'][0]
def _build_payment_header(payment_required):
"""Sign an EIP-3009 TransferWithAuthorization and base64-encode the v2 payload."""
accepted = _select_requirements(payment_required)
now = int(time.time())
nonce = '0x' + secrets.token_hex(32)
authorization = {
'from': account.address, 'to': accepted['payTo'], 'value': accepted['amount'],
'validAfter': str(now - 600), 'validBefore': str(now + accepted['maxTimeoutSeconds']),
'nonce': nonce,
}
extra = accepted.get('extra') or {}
typed_data = {
'types': {
'EIP712Domain': [
{'name': 'name', 'type': 'string'}, {'name': 'version', 'type': 'string'},
{'name': 'chainId', 'type': 'uint256'}, {'name': 'verifyingContract', 'type': 'address'},
],
'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'},
],
},
'domain': {
'name': extra.get('name', 'USD Coin'), 'version': extra.get('version', '2'),
'chainId': int(accepted['network'].split(':')[1]), 'verifyingContract': accepted['asset'],
},
'primaryType': 'TransferWithAuthorization',
'message': {
'from': authorization['from'], 'to': authorization['to'],
'value': int(authorization['value']), 'validAfter': int(authorization['validAfter']),
'validBefore': int(authorization['validBefore']), 'nonce': nonce,
},
}
signed = Account.sign_message(encode_typed_data(full_message=typed_data), EVM_PRIVATE_KEY)
signature = signed.signature.hex()
if not signature.startswith('0x'):
signature = '0x' + signature
payload = {
'x402Version': 2, 'resource': payment_required['resource'], 'accepted': accepted,
'payload': {'signature': signature, 'authorization': authorization},
}
return base64.b64encode(json.dumps(payload).encode()).decode()
def _decode_payment_required(resp):
"""Read the v2 402: the PAYMENT-REQUIRED header is authoritative (base64 JSON);
the JSON body is a same-shape mirror kept as a fallback."""
header = resp.headers.get('PAYMENT-REQUIRED')
if header:
return json.loads(base64.b64decode(header))
body = resp.json()
if body.get('x402Version') == 2 and body.get('accepts'):
return body
raise RuntimeError(f'402 without payment requirements: {body}')
def make_request(method, endpoint, data=None):
"""Try unpaid, pay on 402, retry once with PAYMENT-SIGNATURE."""
url = f'{BASE_URL}{endpoint}'
resp = requests.post(url, json=data) if method == 'POST' else requests.get(url)
if resp.status_code != 402:
return resp
headers = {'PAYMENT-SIGNATURE': _build_payment_header(_decode_payment_required(resp))}
if method == 'POST':
return requests.post(url, json=data, headers=headers)
return requests.get(url, headers=headers)
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'):
# Identities are sorted by priority (lower = more trusted)
results[addr] = {
'exists': True,
'identities': [
{'type': i['type'], 'value': i['value'], 'source': i['source']}
for i in info.get('identities', [])
]
}
return results
# Run it
if __name__ == '__main__':
results = lookup_addresses(ADDRESSES)
print('\n=== Results ===')
for addr, data in results.items():
if data.get('identities'):
summary = ', '.join(f"{i['type']}:{i['value']} ({i['source']})" for i in data['identities'])
print(f"{addr[:10]}... → {summary}")
elif data.get('exists'):
print(f"{addr[:10]}... → found but no identities")
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.
// With API Key (recommended) — no need to /check first
const API_KEY = process.env.BLUEPAGES_API_KEY;
async function getData(address) {
const res = await fetch(
`https://bluepages.fyi/data?address=${address}`,
{ headers: { 'X-API-KEY': API_KEY } }
);
// Only charged if data is found!
return res.json();
}
async function batchData(addresses) {
const res = await fetch('https://bluepages.fyi/batch/data', {
method: 'POST',
headers: {
'X-API-KEY': API_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify({ addresses })
});
// Only charged for found items (40 credits each)
return res.json();
}
// Usage — skip /check, go straight to /data
const result = await getData('0x1234567890abcdef1234567890abcdef12345678');
console.log(result);
// Batch — skip /batch/check, go straight to /batch/data
const batch = await batchData(['0x1234...', '0x5678...']);
console.log(batch);
With x402 (pay-per-request):
// Install: npm install viem @x402/fetch@^2.24.0 @x402/evm@^2.24.0
// Requires: USDC on Base mainnet in your wallet
import { privateKeyToAccount } from 'viem/accounts';
import { wrapFetchWithPaymentFromConfig } from '@x402/fetch';
import { ExactEvmScheme } from '@x402/evm';
// Create an x402 v2 client for Base mainnet (eip155:8453)
const account = privateKeyToAccount(process.env.EVM_PRIVATE_KEY);
const x402Fetch = wrapFetchWithPaymentFromConfig(fetch, {
schemes: [{ network: 'eip155:8453', client: new ExactEvmScheme(account) }]
});
// 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 look up
addresses = ['0x1234567890abcdef1234567890abcdef12345678', ...]
# Skip /batch/check — go straight to /batch/data (only charged for found items)
all_data = []
for i in range(0, len(addresses), 50):
batch = addresses[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'):
labels = [l['name'] for l in info.get('labels', [])]
# One row per identity; sorted by priority (lower = more trusted)
for identity in info.get('identities', []):
all_data.append({
'address': addr,
'type': identity['type'],
'value': identity['value'],
'source': identity['source'],
'labels': labels
})
# 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'Saved {len(all_data)} records')
With x402 (pay-per-request):
requests.
This example builds and sends the PAYMENT-SIGNATURE header manually.
# Install: pip install eth-account requests
# Requires: Python 3.10+, USDC on Base mainnet
import base64
import json
import os
import secrets
import time
import requests
from eth_account import Account
from eth_account.messages import encode_typed_data
# Your private key (use env vars, never hardcode!)
EVM_PRIVATE_KEY = os.environ['EVM_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(EVM_PRIVATE_KEY)
def _select_requirements(payment_required):
for accepted in payment_required['accepts']:
if accepted.get('network') == 'eip155:8453':
return accepted
return payment_required['accepts'][0]
def _build_payment_header(payment_required):
"""Sign an EIP-3009 TransferWithAuthorization and base64-encode the v2 payload."""
accepted = _select_requirements(payment_required)
now = int(time.time())
nonce = '0x' + secrets.token_hex(32)
authorization = {
'from': account.address, 'to': accepted['payTo'], 'value': accepted['amount'],
'validAfter': str(now - 600), 'validBefore': str(now + accepted['maxTimeoutSeconds']),
'nonce': nonce,
}
extra = accepted.get('extra') or {}
typed_data = {
'types': {
'EIP712Domain': [
{'name': 'name', 'type': 'string'}, {'name': 'version', 'type': 'string'},
{'name': 'chainId', 'type': 'uint256'}, {'name': 'verifyingContract', 'type': 'address'},
],
'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'},
],
},
'domain': {
'name': extra.get('name', 'USD Coin'), 'version': extra.get('version', '2'),
'chainId': int(accepted['network'].split(':')[1]), 'verifyingContract': accepted['asset'],
},
'primaryType': 'TransferWithAuthorization',
'message': {
'from': authorization['from'], 'to': authorization['to'],
'value': int(authorization['value']), 'validAfter': int(authorization['validAfter']),
'validBefore': int(authorization['validBefore']), 'nonce': nonce,
},
}
signed = Account.sign_message(encode_typed_data(full_message=typed_data), EVM_PRIVATE_KEY)
signature = signed.signature.hex()
if not signature.startswith('0x'):
signature = '0x' + signature
payload = {
'x402Version': 2, 'resource': payment_required['resource'], 'accepted': accepted,
'payload': {'signature': signature, 'authorization': authorization},
}
return base64.b64encode(json.dumps(payload).encode()).decode()
def _decode_payment_required(resp):
"""Read the v2 402: the PAYMENT-REQUIRED header is authoritative (base64 JSON);
the JSON body is a same-shape mirror kept as a fallback."""
header = resp.headers.get('PAYMENT-REQUIRED')
if header:
return json.loads(base64.b64decode(header))
body = resp.json()
if body.get('x402Version') == 2 and body.get('accepts'):
return body
raise RuntimeError(f'402 without payment requirements: {body}')
def _raise_for_api_error(resp):
"""Surface the server's error instead of letting callers index an error body.
A 404 from /data means "not in the database" (never charged): a result, not an error."""
if resp.ok or resp.status_code == 404:
return resp
try:
body = resp.json()
detail = body.get('message') or body.get('error') or resp.text
except ValueError:
detail = resp.text
raise RuntimeError(f'{resp.request.method} {resp.url} -> {resp.status_code}: {detail}')
def make_request(method, endpoint, data=None):
"""Make an x402 v2 request: try unpaid, pay on 402, retry once with PAYMENT-SIGNATURE."""
url = f'{BASE_URL}{endpoint}'
resp = requests.get(url) if method == 'GET' else requests.post(url, json=data)
if resp.status_code != 402:
return _raise_for_api_error(resp)
# A 402 on the paid retry means the payment itself was rejected (verify or
# settle failed); _raise_for_api_error surfaces the server's reason.
headers = {'PAYMENT-SIGNATURE': _build_payment_header(_decode_payment_required(resp))}
if method == 'GET':
return _raise_for_api_error(requests.get(url, headers=headers))
return _raise_for_api_error(requests.post(url, json=data, headers=headers))
# 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('types'):
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, 'identities': info['identities'], 'labels': info['labels']})
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')
When using x402 payments, you may encounter these errors:
Payment Required (402)
{
"x402Version": 2,
"error": "PAYMENT-SIGNATURE header is required",
"resource": {
"url": "https://bluepages.fyi/check",
"description": "Check if an address or identity exists in database...",
"mimeType": "application/json"
},
"accepts": [{
"scheme": "exact",
"network": "eip155:8453",
"amount": "1000",
"asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"payTo": "0xc21bDbba05d3662f0F63AC6d209C6cb17E61aAD3",
"maxTimeoutSeconds": 60,
"extra": { "name": "USD Coin", "version": "2", "assetTransferMethod": "eip3009" }
}]
}
Solution: Include the PAYMENT-SIGNATURE header with a
valid v2 payment payload. Use @x402/fetch (JavaScript) or build the header
manually per the protocol spec below.
x402 v1 Clients (Legacy)
{
"x402Version": 2,
"error": "x402 v1 (X-PAYMENT) is no longer supported. Send an x402 v2 PAYMENT-SIGNATURE header.",
"resource": { "url": "...", "description": "...", "mimeType": "application/json" },
"accepts": [{ "...": "same 7-field object as above" }]
}
Solution: This API no longer accepts the v1 X-PAYMENT
header — upgrade to an x402 v2 client (@x402/fetch 2.24.0+).
Unsupported Version
{
"x402Version": 2,
"error": "Unsupported x402 version: this server requires x402Version 2",
"resource": { "...": "..." },
"accepts": [{ "...": "..." }]
}
Solution: Your PAYMENT-SIGNATURE payload's x402Version field must be 2.
Payment Verification Failed
{
"x402Version": 2,
"error": "<facilitator invalidReason, e.g. insufficient_funds>",
"payer": "0x...",
"resource": { "...": "..." },
"accepts": [{ "...": "..." }]
}
Solution: Add more USDC to your wallet on Base mainnet, or check that your EIP-712 signature and authorization fields match the accepted requirements exactly.
Payment Settlement Failed
{
"x402Version": 2,
"error": "<facilitator errorReason, or 'Payment settlement failed'>",
"payer": "0x...",
"resource": { "...": "..." },
"accepts": [{ "...": "..." }]
}
Solution: Create a fresh payment authorization (new nonce). Don't reuse a signed authorization across requests — nonces are single-use on-chain.
@x402/fetch handles this automatically; if you're implementing manually,
generate a new random 32-byte nonce for every API call.
For those implementing x402 manually (without @x402/fetch), here's the
complete v2 protocol spec. x402 uses USDC's EIP-3009
TransferWithAuthorization for gasless payments.
PAYMENT-SIGNATURE Header Format
The header must be a base64-encoded JSON string. The accepted field is the exact entry chosen from the 402's accepts[] array, echoed back verbatim:
{
"x402Version": 2,
"resource": { "url": "...", "description": "...", "mimeType": "application/json" },
"accepted": {
"scheme": "exact",
"network": "eip155:8453",
"amount": "1000",
"asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"payTo": "0xc21bDbba05d3662f0F63AC6d209C6cb17E61aAD3",
"maxTimeoutSeconds": 60,
"extra": { "name": "USD Coin", "version": "2", "assetTransferMethod": "eip3009" }
},
"payload": {
"signature": "0x...", // EIP-712 signature (65 bytes hex)
"authorization": {
"from": "0x...", // Your wallet address (payer)
"to": "0x...", // accepted.payTo
"value": "1000", // accepted.amount (USDC atomic units, 6 decimals)
"validAfter": "1703700000", // Unix timestamp (usually now - 600)
"validBefore": "1703703600", // Unix timestamp (usually now + accepted.maxTimeoutSeconds)
"nonce": "0x..." // 32-byte random hex (unique per request)
}
}
}
On success the response carries a PAYMENT-RESPONSE header (base64 JSON
settlement result) — the v1 X-PAYMENT-RESPONSE name is no longer used.
EIP-712 Typed Data (for signing)
USDC on Base uses EIP-3009 TransferWithAuthorization. Domain name/version come from accepted.extra; chainId is the numeric part of accepted.network ("eip155:8453".split(":")[1]); verifyingContract is accepted.asset:
// Domain
{
name: "USD Coin", // accepted.extra.name
version: "2", // accepted.extra.version
chainId: 8453, // Number(accepted.network.split(":")[1])
verifyingContract: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" // accepted.asset
}
// 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 { privateKeyToAccount } from 'viem/accounts';
import crypto from 'crypto';
const USDC_ADDRESS = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
// Picks the Base USDC entry from accepts[] (falls back to first)
function selectRequirements(paymentRequired) {
return paymentRequired.accepts.find(
r => r.network === 'eip155:8453' && r.asset.toLowerCase() === USDC_ADDRESS.toLowerCase()
) || paymentRequired.accepts[0];
}
async function createPaymentHeader(account, paymentRequired) {
const accepted = selectRequirements(paymentRequired);
const now = Math.floor(Date.now() / 1000);
const nonce = '0x' + crypto.randomBytes(32).toString('hex');
const authorization = {
from: account.address,
to: accepted.payTo,
value: BigInt(accepted.amount),
validAfter: BigInt(now - 600), // 10 min before
validBefore: BigInt(now + accepted.maxTimeoutSeconds),
nonce
};
// Sign with EIP-712 — domain from accepted.extra + accepted.network + accepted.asset
const signature = await account.signTypedData({
domain: {
name: accepted.extra?.name || 'USD Coin',
version: accepted.extra?.version || '2',
chainId: Number(accepted.network.split(':')[1]),
verifyingContract: accepted.asset
},
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' }
]
},
primaryType: 'TransferWithAuthorization',
message: authorization
});
// Build the x402 v2 payload: {x402Version, resource, accepted, payload}
const payload = {
x402Version: 2,
resource: paymentRequired.resource,
accepted,
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');
}
// Decodes the v2 402: the PAYMENT-REQUIRED header is authoritative (base64
// JSON); the JSON body is a same-shape mirror kept as a fallback.
function parsePaymentRequired(response, body) {
const h = response.headers.get('PAYMENT-REQUIRED');
if (h) return JSON.parse(Buffer.from(h, 'base64').toString('utf-8'));
if (body?.x402Version === 2 && body.accepts) return body;
throw new Error('No payment requirements');
}
// Usage
async function makePayment(url) {
const account = privateKeyToAccount(process.env.EVM_PRIVATE_KEY);
// Step 1: Get 402 response
const res = await fetch(url);
if (res.status !== 402) return res;
const body = await res.json().catch(() => null);
const paymentRequired = parsePaymentRequired(res, body);
// Step 2: Create and sign payment
const paymentSignature = await createPaymentHeader(account, paymentRequired);
// Step 3: Retry with payment
return fetch(url, { headers: { 'PAYMENT-SIGNATURE': paymentSignature } });
}
Before using x402:
Check your USDC balance:
// JavaScript (using viem - same library as @x402/fetch / @x402/evm)
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
Bluepages provides an MCP (Model Context Protocol) server that allows AI assistants like Claude to look up addresses and identities 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, up to 40% cheaper than x402 requests (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_identity |
Check if identity (Twitter, email, Farcaster…) exists | 1 credit |
get_data_for_address |
Get identities for address | 50 credits* |
get_data_for_identity |
Get addresses for identity | 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* |
batch_check_streaming |
Same as batch_check, for large lists (100+) with progress |
40/found + 1/not-found |
batch_get_data_streaming |
Same as batch_get_data, for large lists (100+) with progress |
40/found* |
check_credits |
Check remaining credits | Free |
get_api_key |
Get or create your API key by signing a message (needs PRIVATE_KEY) |
Free |
purchase_credits |
Buy a credit package via x402 (needs PRIVATE_KEY) |
$5–$600 USDC |
set_credit_alert |
Set low-credit warning threshold | Free |
* Only charged if data is found
API Key Examples (Recommended)
x402 Examples