Base Verify is for web app builders to allow their users to prove they have verified accounts (X Blue, Coinbase One) without sharing credentials.
How it works:
- Your app checks if user has verification → backend returns yes/no
- If no verification → redirect user to Base Verify web app
- User completes OAuth in mini app → returns to your app
- Check again → user now verified
Why This Matters:
Even if a wallet has few transactions, Base Verify reveals if the user is high-value through their verified social accounts (X Blue, Instagram, TikTok) or Coinbase One subscription. This lets you identify quality users regardless of on-chain activity.
Example Use Cases:
- Token-gated airdrops or daily rewards
- Exclusive content access (e.g. creator coins)
- Identity-based rewards and loyalty programs
┌─────────────┐
│ │ 1. User connects wallet
│ Your │
│ Mini App │
│ │
└──────┬──────┘
│
│ 2. App generates SIWE message (frontend)
│ • Includes wallet address
│ • Includes provider (x, coinbase, instagram, tiktok)
│ • Includes traits (verified:true, followers:gt:1000)
│ • Includes action (base_verify_token)
│
│ 3. User signs SIWE message with wallet
│
│ 4. Send signed message to Base Verify API
│ (from frontend or via backend)
│
▼
200 OK ←───────┌──────────────────┐───────→ 400
Verified! │ │ User has account
(DONE) │ Base Verify API │ However, traits not met
│ verify.base.dev │ (DONE)
└────────┬─────────┘
│
│ 404 Not Found
▼
5. Redirect to Base Verify Mini App
│
▼
┌──────────────────────┐
│ Base Verify │ 6. User completes OAuth
│ Mini App │ (X, Coinbase, Instagram, TikTok)
│ verify.base.dev │ 7. Base Verify stores verification
└──────────┬───────────┘
│
│ 8. Redirects back to your app
▼
┌─────────────┐
│ Your │ 9. Check again (step 4)
│ Mini App │ → Now returns 200 ✅ or 400
└─────────────┘ SIWE (Sign-In with Ethereum) is a standard way for users to prove they control a wallet address by signing a message.
Why Base Verify requires SIWE:
-
Privacy Protection: We don't want to leak information about which wallets have verifications. By requiring a signature, only the wallet owner can check their own verification status.
-
Security: The signature proves the request is coming from the actual wallet owner, not someone else looking up verification data.
-
Trait Requirements: The SIWE message includes the specific traits you're checking (e.g., "X account with >1000 followers"). Base Verify validates the signature and checks if those traits match.
What goes in the SIWE message:
{
domain: "your-app.com",
address: "0x1234...", // User's wallet
chainId: 8453, // Base
resources: [
"urn:verify:provider:x", // Which provider
"urn:verify:provider:x:verified:eq:true", // Trait requirements
"urn:verify:action:base_verify_token" // What action
]
}The user signs this message with their wallet, proving they own the address and agree to check these specific traits.
Your App's Responsibilities:
- Generate SIWE messages with trait requirements
- Handle user wallet connection
- Redirect to Base Verify Mini App when verification not found
- Store the returned verification token to prevent reuse
Base Verify's Responsibilities:
- Validate SIWE signatures
- Store provider verifications (X, Coinbase, Instagram, TikTok)
- Check if verification meets trait requirements
- Facilitate OAuth flow with providers
- Return deterministic tokens for Sybil resistance
- 200 OK: Wallet has verified the provider account AND meets all trait requirements. Returns a unique token.
- 404 Not Found: Wallet has never verified this provider. Redirect user to Base Verify Mini App.
- 400 Bad Request (with message
"verification_traits_not_satisfied"): Wallet has verified the provider, but doesn't meet the trait requirements (e.g., has X account but not enough followers).
An identity platform that Base Verify integrates with. Currently supports X (Twitter), Coinbase, Instagram, and TikTok.
Cryptographic proof that a wallet owns an account with a specific Provider.
A specific attribute of the Provider account that can be verified.
Examples:
verified: true- X account has blue checkmarkcoinbase_one_active: true- Active Coinbase One subscriptionfollowers: gt:1000- X account has over 1000 followersfollowers_count: gte:5000- Instagram account with 5000+ followersvideo_count: gte:50- TikTok account with 50+ videos
A deterministic identifier tied to the Provider account, not the wallet. This is the key anti-sybil mechanism.
How it works:
A user verifies their X account with Wallet A:
- Base Verify returns
Token: abc123 - You've never seen this token → Grant airdrop
The same user tries to claim again with Wallet B:
- Base Verify returns
Token: abc123(same token!) - You've seen this token → Reject duplicate claim
Why this matters: Without Base Verify, users could claim multiple times with different wallets. With Base Verify, one verified account = one token = one claim, regardless of how many wallets they use.
Token Properties:
- Deterministic: The same provider account always produces the same token
- Unique per provider: A user's X token is different from their Instagram token
- Unique per app: Your app receives different tokens than other apps (for privacy)
- Action-specific: Tokens can vary based on the action in your SIWE message
- Persistent: Tokens don't expire or rotate (unless the user deletes their verification)
- Trait-independent: Token stays the same even if traits change (e.g., follower count increases)
How to Store Tokens:
// In your database
{
token: "abc123...", // The verification token from Base Verify
walletAddress: "0x1234...", // The wallet that claimed (for your records)
provider: "x", // Which provider was verified
claimedAt: "2024-01-15", // When they claimed
// Store whatever else you need for your use case
}Example: Preventing Double Claims
async function claimAirdrop(verificationToken: string, walletAddress: string) {
// Check if this token was already used
const existingClaim = await db.findClaimByToken(verificationToken);
if (existingClaim) {
return { error: "This X account already claimed" };
}
// Store the token
await db.createClaim({
token: verificationToken,
wallet: walletAddress,
claimedAt: new Date()
});
return { success: true };
}Important: The token is the anti-sybil primitive. Even if a user connects with 100 different wallets, they'll get the same token each time because they verified with the same X/Instagram/TikTok/Coinbase account.
Before diving into the full integration, here's the absolute minimal example showing the core flow. This example checks if a wallet has verified an X account (no trait requirements).
// Your backend endpoint
async function checkVerification(walletAddress: string, signature: string, message: string) {
const response = await fetch('https://verify.base.dev/v1/base_verify_token', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${YOUR_SECRET_KEY}`, // Get from Base Verify team
},
body: JSON.stringify({
signature: signature,
message: message // SIWE message signed by user
})
});
if (response.status === 200) {
const { token } = await response.json();
return { verified: true, token };
} else if (response.status === 404) {
return { verified: false, needsVerification: true };
} else if (response.status === 400) {
const data = await response.json();
if (data.message === 'verification_traits_not_satisfied') {
return { verified: false, traitsNotMet: true };
}
}
}import { SiweMessage, generateNonce } from 'siwe';
// User clicks "Check Verification"
async function handleCheck() {
// Build SIWE message
const siweMessage = new SiweMessage({
domain: window.location.hostname,
address: userWalletAddress,
statement: 'Check X verification',
uri: window.location.origin,
version: '1',
chainId: 8453,
nonce: generateNonce(),
resources: [
'urn:verify:provider:x', // Checking for X provider
'urn:verify:action:base_verify_token'
],
});
// User signs with wallet
const message = siweMessage.prepareMessage();
const signature = await walletSignMessage(message);
// Send to your backend
const result = await fetch('/api/check-verification', {
method: 'POST',
body: JSON.stringify({ address: userWalletAddress, signature, message })
});
return result.json();
}// If check returns 404 (not verified)
function redirectToBaseVerify() {
const params = new URLSearchParams({
redirect_uri: window.location.origin, // Where to return after verification
providers: 'x' // Which provider to verify
});
const webAppUrl = `https://verify.base.dev?${params}`;
window.location.href = webAppUrl;
}// User completes verification and returns to your app
// URL will have ?success=true
if (window.location.search.includes('success=true')) {
// Check verification again - should now return 200
const result = await handleCheck();
if (result.verified) {
console.log('Verification successful!', result.token);
// Store token to prevent reuse
await saveToken(result.token);
}
}This is the complete minimal flow:
- Check → 404
- Redirect to Base Verify
- User verifies with provider
- Return to your app
- Check again → 200
Now let's explore the full integration with configuration, traits, and error handling.
Before integrating Base Verify, you need:
- API Keys - Fill out the interest form to get your keys
- A mini app - Your app should run in Coinbase Wallet or Base ecosystem
- Wallet integration - Users must be able to connect and sign messages
You'll receive a Secret Key from the Base Verify team.
Secret Key:
- NEVER expose to clients or frontend code
- Must only be used on your backend server
- Used for all API calls to Base Verify
- Format:
sec_...
Security Warning: If you accidentally expose your secret key in frontend code, browser dev tools, or version control, contact the Base Verify team immediately to rotate it.
When you receive your key, you'll also need to register:
- Redirect URIs - Where users return after verification (e.g.,
https://yourapp.com) - App Name - How your app appears in the Base Verify flow
Contact the Base Verify team to register these settings.
Security Warning: Your Base Verify secret key must NEVER be exposed in frontend code, browser console, or version control. All Base Verify API calls must go through your backend server. If your key is compromised, contact the Base Verify team immediately.
The integration involves several components working together:
- SIWE Message Generation (Frontend) - Create signed messages proving wallet ownership
- Verification Check (Backend) - Call Base Verify API to check status
- Redirect Handling (Frontend) - Send users to Base Verify when needed
- Token Storage (Backend) - Store verification tokens to prevent reuse
Let's walk through each component:
This configuration is shared between your backend and frontend.
Create lib/config.ts:
export const config = {
appUrl: 'https://your-app.com',
baseVerifySecretKey: process.env.BASE_VERIFY_SECRET_KEY, // Backend only!
baseVerifyApiUrl: 'https://verify.base.dev/v1',
baseVerifyWebAppUrl: 'https://verify.base.dev',
}Create .env.local (backend):
BASE_VERIFY_SECRET_KEY=your_secret_key_hereImportant: Never include your secret key in frontend code or environment variables that are exposed to the browser (like NEXT_PUBLIC_* vars).
Fill out the interest form to get your secret key.
Purpose: Generate SIWE messages that users sign with their wallet. This proves wallet ownership and includes the traits you want to verify.
Runs on: Frontend (client-side)
Create lib/signature-generator.ts:
import { SiweMessage, generateNonce } from 'siwe'
import { config } from './config'
export async function generateSignature(
signMessageFunction: (message: string) => Promise<string>,
address: string
) {
// Build resources array for SIWE
const resources = [
'urn:verify:provider:x',
'urn:verify:provider:x:verified:eq:true',
'urn:verify:provider:x:followers:gte:100',
'urn:verify:action:base_verify_token'
]
// Create SIWE message
const siweMessage = new SiweMessage({
domain: new URL(config.appUrl).hostname,
address,
statement: 'Verify your X account',
uri: config.appUrl,
version: '1',
chainId: 8453, // Base
nonce: generateNonce(),
issuedAt: new Date().toISOString(),
expirationTime: new Date(Date.now() + 6 * 60 * 60 * 1000).toISOString(),
resources,
})
const message = siweMessage.prepareMessage()
const signature = await signMessageFunction(message)
return { message, signature, address }
}Purpose: Check if the user has the required verification by calling Base Verify API.
Runs on: Backend only (secret key must not be exposed to frontend)
Flow:
- Frontend generates SIWE message and gets user signature
- Frontend sends signature to YOUR backend
- Your backend calls Base Verify API with secret key
- Your backend returns result to frontend
Frontend code:
async function checkVerification(address: string) {
// Generate SIWE signature
const signature = await generateSignature(
async (msg) => {
return new Promise((resolve, reject) => {
signMessage(
{ message: msg },
{ onSuccess: resolve, onError: reject }
)
})
},
address
)
// Send to YOUR backend (not directly to Base Verify)
const response = await fetch('/api/check-verification', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
signature: signature.signature,
message: signature.message,
address: address
})
})
const data = await response.json();
return data; // Your backend returns the result
}Backend code (YOUR API endpoint):
// pages/api/check-verification.ts
export default async function handler(req, res) {
const { signature, message, address } = req.body;
// Call Base Verify with YOUR secret key
const response = await fetch('https://verify.base.dev/v1/base_verify_token', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.BASE_VERIFY_SECRET_KEY}`, // Secret key stays on backend
},
body: JSON.stringify({
signature: signature,
message: message,
})
});
if (response.ok) {
const data = await response.json();
return res.status(200).json({ verified: true, token: data.token });
} else if (response.status === 404) {
return res.status(404).json({ verified: false, needsVerification: true });
} else if (response.status === 400) {
const data = await response.json();
if (data.message === 'verification_traits_not_satisfied') {
return res.status(400).json({ verified: false, traitsNotMet: true });
}
}
return res.status(500).json({ error: 'Verification check failed' });
}Purpose: If verification not found (404), redirect user to Base Verify web app to complete OAuth.
Runs on: Frontend
function redirectToVerifyWebApp(provider: string) {
const params = new URLSearchParams({
redirect_uri: config.appUrl,
providers: provider,
})
const webAppUrl = `${config.baseVerifyWebAppUrl}?${params.toString()}`
window.location.href = webAppUrl
}After verification, user returns to your redirect_uri with ?success=true. Check verification again to get the token.
Check if a wallet has a specific verification and retrieve the verification token.
Authentication: Requires Authorization: Bearer {SECRET_KEY}
Important: This endpoint must only be called from your backend. Never expose your secret key in frontend code.
Request:
{
signature: string, // SIWE signature from wallet
message: string // SIWE message (includes provider/traits in resources)
}Example Request:
curl -X POST https://verify.base.dev/v1/base_verify_token \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_SECRET_KEY" \
-d '{
"signature": "0x1234...",
"message": "verify.base.dev wants you to sign in..."
}'Response (200 OK):
{
token: string, // Deterministic verification token (for Sybil resistance)
action: string, // Action from SIWE message
wallet: string // User's wallet address
}Response (404 Not Found):
User doesn't have this verification. Redirect to mini app.
{
error: "verification_not_found"
}Response (400 Bad Request):
User has provider account but doesn't meet trait requirements.
{
code: 9,
message: "verification_traits_not_satisfied",
details: []
}Response (401 Unauthorized):
Invalid or missing API key.
{
error: "unauthorized"
}When redirecting to Base Verify Mini App:
https://verify.base.dev?redirect_uri={your_app_url}&providers={provider}| Parameter | Required | Description |
|---|---|---|
redirect_uri |
Yes | Where to send user after verification |
providers |
Yes | Provider to verify: coinbase, x, instagram, or tiktok |
The user completes verification in the mini app, then returns to your redirect_uri.
Traits are specific attributes of a provider account that you can verify. Before looking at provider-specific traits, understand the global trait system rules.
Traits are specified in SIWE message resources using this format:
urn:verify:provider:{provider}:{trait_name}:{operation}:{value}
Example:
urn:verify:provider:x:followers:gte:1000
This checks if an X account has greater than or equal to 1000 followers.
| Operation | Symbol | Applies To | Description | Example |
|---|---|---|---|---|
| Equals | eq |
All types | Exact match | verified:eq:true |
| Greater Than | gt |
Integers | Strictly greater | followers:gt:1000 |
| Greater/Equal | gte |
Integers | Greater or equal | followers:gte:1000 |
| Less Than | lt |
Integers | Strictly less | followers:lt:5000 |
| Less/Equal | lte |
Integers | Less or equal | followers:lte:5000 |
| In (list) | in |
Strings | Value in comma-separated list | verified_type:in:blue,government |
Boolean Traits
- Values:
"true"or"false"(as strings) - Only supports
eqoperation - Example:
verified:eq:true
Integer Traits
- Values: Numbers as strings
- Supports:
eq,gt,gte,lt,lte - Example:
followers:gte:1000
String Traits
- Values: Text strings
- Supports:
eq,in - Example:
verified_type:eq:blueorverified_type:in:blue,government
Within One Provider (AND logic):
When you specify multiple traits for the same provider, ALL must be satisfied:
resources: [
'urn:verify:provider:x',
'urn:verify:provider:x:verified:eq:true',
'urn:verify:provider:x:followers:gte:10000'
]
// User must have verified X account AND 10k+ followersMultiple Providers:
Currently, you can only check one provider per request. To check multiple providers, make separate API calls.
Using traits in signature generation:
// Simple boolean check
const signature = await generateSignature({
provider: 'x',
traits: { 'verified': 'true' },
action: 'base_verify_token'
});
// Integer comparison
const signature = await generateSignature({
provider: 'instagram',
traits: { 'followers_count': 'gte:5000' },
action: 'base_verify_token'
});
// Multiple traits (AND logic)
const signature = await generateSignature({
provider: 'tiktok',
traits: {
'follower_count': 'gte:1000',
'likes_count': 'gte:10000',
'video_count': 'gte:50'
},
action: 'base_verify_token'
});
// String with IN operation
const signature = await generateSignature({
provider: 'coinbase',
traits: {
'verified_type': 'in:blue,government'
},
action: 'base_verify_token'
});Tiered Access:
// Bronze tier: any verified account
traits: { 'verified': 'true' }
// Silver tier: 1k+ followers
traits: { 'followers': 'gte:1000' }
// Gold tier: 10k+ followers
traits: { 'followers': 'gte:10000' }Content Creator Verification:
// Active TikTok creator
traits: {
'follower_count': 'gte:5000',
'video_count': 'gte:100',
'likes_count': 'gte:50000'
}Each provider section below includes:
- Trait Table: All available traits and their types
- Code Examples: How to use traits in your integration
- Try It Live: Interactive examples you can test with your connected wallet
The interactive examples let you test real API calls to Base Verify:
What happens when you click "Try It":
- Your wallet signs a SIWE message with the specified traits
- The request is sent to
https://verify.base.dev/v1/base_verify_token - You see the full request (headers, body) and response (status, data)
Response Codes:
- 200 OK: You have verified this provider and meet the trait requirements
- 404 Not Found: You haven't verified this provider yet (need to visit Base Verify Mini App)
- 400 Bad Request (message:
verification_traits_not_satisfied): You have the provider account but don't meet traits (e.g., not enough followers)
Why test this:
- See exactly how the API works
- Understand request/response formats
- Test your own verification status
- Debug trait requirements before implementing
Note: These examples call Base Verify API directly from your browser for demo purposes. In production, you should proxy all Base Verify API calls through your backend to keep your secret key secure.
Provider: coinbase
Available Traits:
| Trait | Type | Operations | Description | Example Values |
|---|---|---|---|---|
coinbase_one_active |
Boolean | eq |
Active Coinbase One subscription | "true", "false" |
coinbase_one_billed |
Boolean | eq |
User has been billed for Coinbase One | "true", "false" |
Examples:
// Check for Coinbase One subscribers
{
provider: 'coinbase',
traits: { 'coinbase_one_active': 'true' }
}
// Check for billed Coinbase One subscribers
{
provider: 'coinbase',
traits: { 'coinbase_one_billed': 'true' }
}Provider: x
Available Traits:
| Trait | Type | Operations | Description | Example Values |
|---|---|---|---|---|
verified |
Boolean | eq |
Has any type of verification | "true", "false" |
verified_type |
String | eq |
Type of verification | "blue", "government", "business", "none" |
followers |
Integer | eq, gt, gte, lt, lte |
Number of followers | "1000", "50000" |
Examples:
// Check for any verified account
{
provider: 'x',
traits: { 'verified': 'true' }
}
// Check for specific verification type
{
provider: 'x',
traits: { 'verified_type': 'blue' }
}
// Check for follower count (greater than or equal to)
{
provider: 'x',
traits: { 'followers': 'gte:1000' }
}
// Check for follower count (exact)
{
provider: 'x',
traits: { 'followers': 'eq:50000' }
}
// Combine multiple traits
{
provider: 'x',
traits: {
'verified': 'true',
'followers': 'gte:10000'
}
}Provider: instagram
Available Traits:
| Trait | Type | Operations | Description | Example Values |
|---|---|---|---|---|
username |
String | eq |
Instagram username | "john_doe" |
followers_count |
Integer | eq, gt, gte, lt, lte |
Number of followers | "1000", "50000" |
instagram_id |
String | eq |
Unique Instagram user ID | "1234567890" |
Examples:
// Check for specific username
{
provider: 'instagram',
traits: { 'username': 'john_doe' }
}
// Check for follower count (greater than)
{
provider: 'instagram',
traits: { 'followers_count': 'gt:1000' }
}
// Check for follower count (greater than or equal to)
{
provider: 'instagram',
traits: { 'followers_count': 'gte:5000' }
}
// Combine multiple traits
{
provider: 'instagram',
traits: {
'username': 'john_doe',
'followers_count': 'gte:10000'
}
}Provider: tiktok
Available Traits:
| Trait | Type | Operations | Description | Example Values |
|---|---|---|---|---|
open_id |
String | eq |
TikTok Open ID (unique per app) | "abc123..." |
union_id |
String | eq |
TikTok Union ID (unique across apps) | "def456..." |
display_name |
String | eq |
TikTok display name | "John Doe" |
follower_count |
Integer | eq, gt, gte, lt, lte |
Number of followers | "1000", "50000" |
following_count |
Integer | eq, gt, gte, lt, lte |
Number of accounts following | "500", "2000" |
likes_count |
Integer | eq, gt, gte, lt, lte |
Total likes received | "10000", "100000" |
video_count |
Integer | eq, gt, gte, lt, lte |
Number of videos posted | "50", "200" |
Examples:
// Check for follower count
{
provider: 'tiktok',
traits: { 'follower_count': 'gt:1000' }
}
// Check for likes count
{
provider: 'tiktok',
traits: { 'likes_count': 'gte:10000' }
}
// Check for video count
{
provider: 'tiktok',
traits: { 'video_count': 'gte:50' }
}
// Combine multiple traits (e.g., active creator)
{
provider: 'tiktok',
traits: {
'follower_count': 'gte:5000',
'likes_count': 'gte:100000',
'video_count': 'gte:100'
}
}
// Check for specific display name
{
provider: 'tiktok',
traits: { 'display_name': 'John Doe' }
}What Base Verify Stores:
- Wallet addresses associated with verified provider accounts
- Provider account metadata (username, follower counts, verification status)
- OAuth tokens (encrypted, never shared with apps)
- Verification timestamps
What Base Verify Does NOT Store:
- Your users' private keys
- Provider account passwords
- User activity or browsing history
- Any data beyond what's needed for verification
When you call /v1/base_verify_token, you receive:
Standard Response (200 OK):
{
"token": "abc123...", // Deterministic verification token
"action": "base_verify_token",
"wallet": "0x1234..." // User's wallet address
}No PII is returned unless you explicitly request disclosures (requires special permission):
{
"token": "abc123...",
"disclosures": {
"x_username": "johndoe", // Only if can_request_disclosures = true
"x_followers": 5000,
"x_verified_type": "blue"
}
}1. SIWE Signature Requirement
Every API call requires a valid SIWE signature from the wallet owner. This prevents:
- Arbitrary lookup of verification status
- Third parties checking if a wallet is verified
- Enumeration attacks
2. OAuth Token Security
- OAuth access tokens are encrypted at rest
- Never exposed to your application
- Used only by Base Verify to refresh provider data
- Can be revoked by user at any time
3. User Control
Users can delete their verifications at any time:
- Removes all stored provider data
- Invalidates future token generation
- Your app's stored tokens become meaningless (user can't re-verify with same account)
How Base Verify validates provider accounts:
- User initiates OAuth in Base Verify Mini App
- Provider (X, Instagram, etc.) authenticates the user
- Provider returns OAuth token to Base Verify
- Base Verify fetches account data using OAuth token
- Base Verify stores verification linked to user's wallet
- OAuth token is encrypted and stored securely
Your app never handles OAuth tokens or redirects. This is all handled within the Base Verify web app.
Try the Base Verify web app: https://verify.base.dev
Try an example demo here: https://baseverifydemo.com
We'd love your feedback as you integrate! You'll be one of the first external users of Base Verify.
Want to integrate? Fill out the interest form and we'll reach out with API access.