Skip to main content

What is Base Verify?

Base Verify allows users to prove ownership of verified accounts (X, Coinbase, Instagram, TikTok) without sharing credentials. Your app receives a deterministic token for Sybil resistance. Even if a wallet has few transactions, Base Verify reveals whether 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 onchain activity. Example use cases:
  • Token-gated airdrops or daily rewards
  • Exclusive content access (e.g., creator coins)
  • Identity-based rewards and loyalty programs

Core concepts

Provider

An identity platform that Base Verify integrates with. Currently supports X (Twitter), Coinbase, Instagram, and TikTok.

Verification

Cryptographic proof that a wallet owns an account with a specific provider.

Trait

A specific attribute of the provider account that can be verified. Examples:
  • verified: true — X account has blue checkmark
  • coinbase_one_active: true — active Coinbase One subscription
  • followers: gt:1000 — X account has over 1,000 followers
  • followers_count: gte:5000 — Instagram account with 5,000+ followers
  • video_count: gte:50 — TikTok account with 50+ videos

Action

A developer-defined string that identifies what the user is doing with their verification. Actions let you issue different tokens for different use cases within the same app. Examples:
  • claim_daily_reward — claiming a daily reward
  • join_allowlist — joining an exclusive allowlist
  • unlock_premium_content — accessing gated content
  • participate_in_raffle — entering a raffle

How actions work

Actions are specified in the SIWE message resources:
Title
resources: [
  'urn:verify:provider:x',
  'urn:verify:action:claim_daily_reward'
]
The action is returned in the API response:
Title
{
  "token": "abc123...",
  "action": "claim_daily_reward",
  "wallet": "0x1234..."
}

Why actions matter

Different actions produce different tokens. This enables multiple independent claims from the same verified account:
  • User verifies X account with action claim_airdrop → Token: abc123
  • Same X account with action join_allowlist → Token: def456 (different)
  • Same X account with action claim_airdrop again → Token: abc123 (same as first)
Use cases:
  • Multiple campaigns — run separate airdrops without interference
  • Feature gating — different tokens for different premium features
  • Time-based events — new action per event (e.g., raffle_jan_2025, raffle_feb_2025)

Choosing action names

Use descriptive, lowercase names with underscores:
GoodBad
claim_genesis_airdropairdrop (too generic)
unlock_pro_featuresaction1 (meaningless)
enter_weekly_rafflebase_verify_token (reserved/confusing)
Once you launch with an action name, don’t change it. Changing the action generates different tokens for the same users, breaking your Sybil resistance.

Token — Sybil resistance

A deterministic identifier tied to the provider account, not the wallet. This is the key anti-Sybil mechanism.

How it works

  1. Wallet A verifies an X account → Base Verify returns Token: abc123 → you have never seen it, so grant the airdrop.
  2. The same X account tries again with Wallet B → Base Verify returns Token: abc123 → you have seen it, so block the duplicate claim.
Without Base Verify, users could claim multiple times with different wallets. With Base Verify, one verified account = one token = one claim.

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 (privacy)
  • Action-specific — tokens vary based on the action in your SIWE message
  • Persistent — tokens don’t expire or rotate (unless the user deletes their verification)
  • Trait-independent — tokens stay the same even if traits change (e.g., follower count increases)

How to store tokens

Title
{
  token: "abc123...",
  walletAddress: "0x1234...",
  provider: "x",
  claimedAt: "2024-01-15",
}

Prevent double claims

Title
async function claimAirdrop(verificationToken: string, walletAddress: string) {
  const existingClaim = await db.findClaimByToken(verificationToken);

  if (existingClaim) {
    return { error: "This X account already claimed" };
  }

  await db.createClaim({
    token: verificationToken,
    wallet: walletAddress,
    claimedAt: new Date()
  });

  return { success: true };
}

Architecture and flow

                    ┌─────────────┐
                    │             │  1. User connects wallet
                    │   Your      │
                    │   Mini App  │
                    │  (Frontend) │
                    └──────┬──────┘

                           │ 2. App generates SIWE message (frontend)
                           │    • Includes wallet address
                           │    • Includes provider (x, coinbase, instagram, tiktok)
                           │    • Includes traits (verified:true, followers:gt:1000)
                           │    • Includes action (e.g. claim_airdrop)

                           │ 3. User signs SIWE message with wallet

                           │ 4. Send signature + message to YOUR backend


                    ┌──────────────┐
                    │  Mini App    │  • Validates trait requirements
                    │  Backend     │  • Verifies signature with Base Verify API
                    │  (Your API)  │
                    └──────┬───────┘



   200 OK ←───────┌──────────────────┐───────→ 400
   Verified!      │                  │         User has account
   (DONE)         │  Base Verify API │         but 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
                        └─────────────┘

Your app’s responsibilities

  • Generate SIWE messages with trait requirements
  • Handle user wallet connection
  • Redirect to the Base Verify Mini App when verification is not found
  • Store the returned verification token to prevent reuse
  • Keep your secret key secure on the backend

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

Response codes

CodeMeaningAction
200 OKWallet has verified the provider account AND meets all trait requirements. Returns a unique token.Grant access, store the token.
404 Not FoundWallet has never verified this provider.Redirect user to the Base Verify Mini App.
400 Bad Request (verification_traits_not_satisfied)Wallet has verified the provider, but doesn’t meet the trait requirements.Show user they don’t meet requirements. Do not redirect.

Getting started

Prerequisites

  1. API key — fill out the interest form to get access
  2. Wallet integration — users must be able to connect and sign messages
  3. Backend server — to securely call the Base Verify API

Register your app

Provide the Base Verify team:
  1. Your Mini App domain
  2. Your redirect URI — where users return after verification (e.g., https://yourapp.com)
Your secret key must never be exposed in frontend code. All Base Verify API calls must go through your backend.

Implementation

1

Configure your app

Create a configuration file for your Base Verify integration:
lib/config.ts
export const config = {
  appUrl: 'https://your-app.com',
  baseVerifySecretKey: process.env.BASE_VERIFY_SECRET_KEY,
  baseVerifyApiUrl: 'https://verify.base.dev/v1',
  baseVerifyMiniAppUrl: 'https://verify.base.dev',
}
Add your secret key to .env.local:
.env.local
BASE_VERIFY_SECRET_KEY=your_secret_key_here
2

Generate the SIWE signature (frontend)

Build a SIWE message that includes the provider, trait requirements, and action:
lib/signature-generator.ts
import { SiweMessage, generateNonce } from 'siwe'
import { config } from './config'

export async function generateSignature(
  signMessageFunction: (message: string) => Promise<string>,
  address: string
) {
  const resources = [
    'urn:verify:provider:x',
    'urn:verify:provider:x:verified:eq:true',
    'urn:verify:provider:x:followers:gte:100',
    'urn:verify:action:claim_airdrop'
  ]

  const siweMessage = new SiweMessage({
    domain: new URL(config.appUrl).hostname,
    address,
    statement: 'Verify your X account',
    uri: config.appUrl,
    version: '1',
    chainId: 8453,
    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 }
}
3

Check verification (frontend → backend)

The frontend generates the signature and sends it to your backend, which calls the Base Verify API.Frontend:
Title
async function checkVerification(address: string) {
  const signature = await generateSignature(
    async (msg) => {
      return new Promise((resolve, reject) => {
        signMessage(
          { message: msg },
          { onSuccess: resolve, onError: reject }
        )
      })
    },
    address
  )

  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;
}
Backend (your API endpoint):
pages/api/check-verification.ts
import { validateTraits } from '../../lib/trait-validator';

export default async function handler(req, res) {
  const { signature, message, address } = req.body;

  const expectedTraits = {
    'verified': 'true',
    'followers': 'gte:100'
  };

  const validation = validateTraits(message, 'x', expectedTraits);

  if (!validation.valid) {
    return res.status(400).json({
      error: 'Invalid trait requirements in message',
      details: validation.error
    });
  }

  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}`,
    },
    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' });
}
Your backend must validate that the trait requirements in the SIWE message match what your backend expects. This prevents users from modifying trait requirements on the frontend to bypass your access controls. See security best practices for details.
4

Redirect to Base Verify (frontend)

If you receive a 404 response, redirect the user to the Base Verify Mini App to complete OAuth:
Title
function redirectToVerifyMiniApp(provider: string) {
  const params = new URLSearchParams({
    redirect_uri: config.appUrl,
    providers: provider,
  })

  const miniAppUrl = `${config.baseVerifyMiniAppUrl}?${params.toString()}`

  const deepLink = `cbwallet://miniapp?url=${encodeURIComponent(miniAppUrl)}`
  window.open(deepLink, '_blank')
}
After verification, the user returns to your redirect_uri with ?success=true. Run the check again (step 3) — it now returns 200 with a token.

Error handling

ResponseWhat to do
404User hasn’t verified. Redirect to the Base Verify Mini App.
400 (verification_traits_not_satisfied)User has account but doesn’t meet requirements. Show a message — don’t redirect and don’t retry.
200Store the token and grant access.
Do not retry 404 responses — the user simply hasn’t verified yet. Do not retry 400 responses with verification_traits_not_satisfied — retrying won’t help unless the user’s account metrics change (e.g., they gain more followers).

API reference

Authentication

All API requests require your secret key in the Authorization header:
Title
Authorization: Bearer YOUR_SECRET_KEY

POST /v1/base_verify_token

Check if a wallet has a specific verification and retrieve the verification token.

Request

Title
{
  signature: string,   // SIWE signature from wallet
  message: string      // SIWE message (includes provider/traits in resources)
}

Example request

Title
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..."
  }'

Responses

200 OK — verified:
Title
{
  "token": "abc123...",
  "action": "claim_airdrop",
  "wallet": "0x1234..."
}
FieldTypeDescription
tokenstringDeterministic verification token for Sybil resistance. Same provider account + same action = same token.
actionstringThe custom action specified in the SIWE message. Different actions produce different tokens.
walletstringUser’s wallet address.
404 Not Found — verification not found:
Title
{
  "error": "verification_not_found"
}
Redirect the user to the Base Verify Mini App to complete verification. 400 Bad Request — traits not satisfied:
Title
{
  "code": 9,
  "message": "verification_traits_not_satisfied",
  "details": []
}
The user has the provider account but doesn’t meet trait requirements. Do not redirect. 401 Unauthorized — invalid key:
Title
{
  "error": "unauthorized"
}
Check that your secret key is correct and included in the Authorization header.

Mini App redirect

To redirect users to Base Verify for verification:
Title
https://verify.base.dev?redirect_uri={your_app_url}&providers={provider}
ParameterRequiredDescriptionExample
redirect_uriYesWhere to send the user after verificationhttps://yourapp.com
providersYesProvider to verifyx, coinbase, instagram, tiktok

Trait catalog

Traits are specific attributes of a provider account that you can verify. They are specified in SIWE message resources using this format:
Title
urn:verify:provider:{provider}:{trait_name}:{operation}:{value}

Operations

OperationSymbolApplies toDescriptionExample
EqualseqAll typesExact matchverified:eq:true
Greater thangtIntegersStrictly greaterfollowers:gt:1000
Greater/equalgteIntegersGreater or equalfollowers:gte:1000
Less thanltIntegersStrictly lessfollowers:lt:5000
Less/equallteIntegersLess or equalfollowers:lte:5000
In (list)inStringsValue in comma-separated listverified_type:in:blue,government

Type system

Boolean traits:
  • Values: "true" or "false" (as strings)
  • Only supports eq operation
  • 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:blue or verified_type:in:blue,government

Combining traits

When you specify multiple traits for the same provider, all must be satisfied (AND logic):
Title
resources: [
  'urn:verify:provider:x',
  'urn:verify:provider:x:verified:eq:true',
  'urn:verify:provider:x:followers:gte:10000'
]
You can only check one provider per request. To check multiple providers, make separate API calls.

Common patterns

Tiered access:
Title
// Bronze tier: any verified account
traits: { 'verified': 'true' }

// Silver tier: 1k+ followers
traits: { 'followers': 'gte:1000' }

// Gold tier: 10k+ followers
traits: { 'followers': 'gte:10000' }

Coinbase

Provider: coinbase
TraitTypeOperationsDescriptionExample values
coinbase_one_activeBooleaneqActive Coinbase One subscription"true", "false"
coinbase_one_billedBooleaneqUser has been billed for Coinbase One"true", "false"
Title
// 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' }
}

X (Twitter)

Provider: x
TraitTypeOperationsDescriptionExample values
verifiedBooleaneqHas any type of verification"true", "false"
verified_typeStringeqType of verification"blue", "government", "business", "none"
followersIntegereq, gt, gte, lt, lteNumber of followers"1000", "50000"
Title
// 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' }
}

// Combine multiple traits
{
  provider: 'x',
  traits: {
    'verified': 'true',
    'followers': 'gte:10000'
  }
}

Instagram

Provider: instagram
TraitTypeOperationsDescriptionExample values
usernameStringeqInstagram username"john_doe"
followers_countIntegereq, gt, gte, lt, lteNumber of followers"1000", "50000"
instagram_idStringeqUnique Instagram user ID"1234567890"
Title
// 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'
  }
}

TikTok

Provider: tiktok
TraitTypeOperationsDescriptionExample values
open_idStringeqTikTok Open ID (unique per app)"abc123..."
union_idStringeqTikTok Union ID (unique across apps)"def456..."
display_nameStringeqTikTok display name"John Doe"
follower_countIntegereq, gt, gte, lt, lteNumber of followers"1000", "50000"
following_countIntegereq, gt, gte, lt, lteNumber of accounts following"500", "2000"
likes_countIntegereq, gt, gte, lt, lteTotal likes received"10000", "100000"
video_countIntegereq, gt, gte, lt, lteNumber of videos posted"50", "200"
Title
// Check for follower count
{
  provider: 'tiktok',
  traits: { 'follower_count': 'gt:1000' }
}

// Check for likes count
{
  provider: 'tiktok',
  traits: { 'likes_count': 'gte:10000' }
}

// Combine multiple traits (e.g., active creator)
{
  provider: 'tiktok',
  traits: {
    'follower_count': 'gte:5000',
    'likes_count': 'gte:100000',
    'video_count': 'gte:100'
  }
}

Security and privacy

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
The user signs a structured message proving they control the wallet and agree to check specific traits:
Title
{
  domain: "your-app.com",
  address: "0x1234...",
  chainId: 8453,
  resources: [
    "urn:verify:provider:x",
    "urn:verify:provider:x:verified:eq:true",
    "urn:verify:action:claim_airdrop"
  ]
}

Validate trait requirements

When your backend receives a SIWE message from the frontend, you must validate that the trait requirements in the message match what your backend expects. This prevents users from modifying trait requirements on the frontend to bypass your access controls.
Example attack without validation:
  1. Your app requires users to have 100 followers
  2. User modifies the frontend to request only 10 followers
  3. User signs the modified message
  4. Without validation, your backend forwards the request to Base Verify
  5. User gains access with fewer than 100 followers
Implementation:
Title
import { validateTraits } from './lib/trait-validator';

const expectedTraits = {
  'followers': 'gte:100'
};

const validation = validateTraits(message, 'x', expectedTraits);

if (!validation.valid) {
  return res.status(400).json({
    error: 'Invalid trait requirements in message',
    details: validation.error
  });
}

Protect your secret key

Never:
  • Include the secret key in frontend code
  • Use NEXT_PUBLIC_* or similar environment variables that expose to the browser
  • Commit secret keys to version control
  • Share secret keys in chat, email, or documentation
Always:
  • Store the secret key in backend environment variables only
  • Use .env files that are gitignored
  • Rotate keys immediately if accidentally exposed
  • Call the Base Verify API only from your backend

OAuth security model

Base Verify validates provider accounts through OAuth:
  1. User initiates OAuth in the Base Verify Mini App
  2. Provider (X, Instagram, etc.) authenticates the user
  3. Provider returns an OAuth token to Base Verify
  4. Base Verify fetches account data using the OAuth token
  5. Base Verify stores the verification linked to the user’s wallet
  6. OAuth token is encrypted and stored securely
Your app never handles OAuth tokens or redirects — this is all handled within the Base Verify Mini App.

Data storage

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:
  • Users’ private keys
  • Provider account passwords
  • User activity or browsing history
  • Any data beyond what’s needed for verification
What your app receives: When you call /v1/base_verify_token, you receive only token, action, and wallet. No PII is returned.

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 the same account)

Caching

Cache verification results to reduce API calls:
  • Cache for the user’s session (not permanently)
  • Clear cache when the user disconnects their wallet
  • Don’t check verification on every page load

Support

Want to integrate Base Verify? Fill out the interest form and the team will reach out with API access.