Bulk pregeneration allows you to create Para wallets for multiple Twitter users ahead of time. Users can later claim these wallets by authenticating with their Twitter accounts, creating a seamless onboarding experience for airdrops, token distributions, or whitelist rewards.

How Bulk Pregeneration Works

When you bulk pregenerate wallets:

  1. Generate wallets for Twitter usernames using Para’s server SDK
  2. Store user shares securely on your backend
  3. Fund wallets with tokens or NFTs for airdrops (optional)
  4. Users claim wallets later by signing in with Twitter
  5. Para matches the Twitter username to the pregenerated wallet

Prerequisites

You need a Para API key and basic knowledge of Node.js/TypeScript development.

Getting Twitter Usernames

You can obtain Twitter usernames for bulk pregeneration from various sources:

  • Database of pre-registered users - Users who joined your whitelist or waitlist
  • Twitter API - Programmatically fetch followers, mentions, or community members
  • CSV files - Export from existing user databases or CRM systems
  • Contest participants - Users who engaged with your Twitter campaigns

For this tutorial, we’ll use a local CSV file as an example, but the core logic applies regardless of your data source.

Server-Side Implementation

Setup Para Server Client

First, create a Para server client to handle wallet generation:

lib/para-server.ts
import { Environment, Para } from "@getpara/server-sdk";

export function getParaServerClient() {
  const apiKey = process.env.PARA_API_KEY;
  if (!apiKey) {
    throw new Error("PARA_API_KEY is required");
  }
  
  return new Para(Environment.BETA, apiKey);
}

Create Bulk Generation API

Create an API endpoint to generate wallets for Twitter usernames:

api/wallet/generate/route.ts
import { getParaServerClient } from "@/lib/para-server";
import { NextResponse } from "next/server";

interface GenerateWalletRequest {
  handle: string;
  type: "TWITTER";
}

export async function POST(request: Request) {
  try {
    const { handle, type }: GenerateWalletRequest = await request.json();
    
    if (!handle || type !== "TWITTER") {
      return NextResponse.json({ error: "Invalid handle or type" }, { status: 400 });
    }
    
    const para = getParaServerClient();
    
    const wallet = await para.createPregenWallet({
      type: "EVM",
      pregenId: { xUsername: handle.trim() }
    });
    
    const userShare = await para.getUserShare();
    
    if (!wallet || !userShare) {
      throw new Error("Failed to generate wallet");
    }
    
    await storeWalletData(handle.trim(), wallet, userShare);
    
    return NextResponse.json({
      success: true,
      handle: handle.trim(),
      address: wallet.address
    });
  } catch (error) {
    console.error("Wallet generation error:", error);
    return NextResponse.json({ error: "Generation failed" }, { status: 500 });
  }
}

async function storeWalletData(handle: string, wallet: any, userShare: any) {
  // Store wallet and user share in your database
  // This is critical for users to claim their wallets later
}

Batch Processing Implementation

For processing multiple handles efficiently:

hooks/use-batch-processor.ts
import { useState } from "react";

interface BatchResult {
  handle: string;
  success: boolean;
  address?: string;
  error?: string;
}

export function useBatchProcessor() {
  const [processing, setProcessing] = useState(false);
  const [results, setResults] = useState<BatchResult[]>([]);
  const [progress, setProgress] = useState(0);
  
  const processBatch = async (handles: string[]) => {
    setProcessing(true);
    setResults([]);
    setProgress(0);
    
    const batchSize = 10;
    const batches = [];
    
    for (let i = 0; i < handles.length; i += batchSize) {
      batches.push(handles.slice(i, i + batchSize));
    }
    
    const allResults: BatchResult[] = [];
    
    for (let i = 0; i < batches.length; i++) {
      const batch = batches[i];
      const batchResults = await Promise.all(
        batch.map(async (handle) => {
          try {
            const response = await fetch("/api/wallet/generate", {
              method: "POST",
              headers: { "Content-Type": "application/json" },
              body: JSON.stringify({ handle, type: "TWITTER" })
            });
            
            const data = await response.json();
            
            return {
              handle,
              success: data.success,
              address: data.address,
              error: data.error
            };
          } catch (error) {
            return {
              handle,
              success: false,
              error: "Network error"
            };
          }
        })
      );
      
      allResults.push(...batchResults);
      setResults([...allResults]);
      setProgress(((i + 1) / batches.length) * 100);
      
      // Rate limiting delay
      if (i < batches.length - 1) {
        await new Promise(resolve => setTimeout(resolve, 1000));
      }
    }
    
    setProcessing(false);
  };
  
  return {
    processing,
    results,
    progress,
    processBatch
  };
}

Alternative Data Sources

From Database

You can fetch usernames directly from your database:

// Example: Fetch whitelist users from database
async function getWhitelistUsers() {
  const users = await db.users.findMany({
    where: { whitelisted: true },
    select: { twitterUsername: true }
  });
  
  return users.map(user => user.twitterUsername).filter(Boolean);
}

From Twitter API

Programmatically fetch Twitter usernames:

// Example: Get followers using Twitter API
async function getFollowers(userId: string) {
  const response = await fetch(`https://api.twitter.com/2/users/${userId}/followers`, {
    headers: {
      'Authorization': `Bearer ${process.env.TWITTER_BEARER_TOKEN}`
    }
  });
  
  const data = await response.json();
  return data.data?.map((user: any) => user.username) || [];
}

Example: CSV Processing

For this tutorial, we’ll demonstrate with a CSV file containing Twitter handles:

handle,type
@username1,twitter
@username2,twitter
username3,twitter

The @ symbol is optional and will be automatically handled. Headers are also optional.

Frontend Considerations

While the UI implementation depends on your application’s design system, consider these patterns:

  • Progress tracking - Show real-time batch processing status
  • Error handling - Display failed generations with retry options
  • Results export - Allow downloading generation results
  • Batch size control - Let users adjust processing batch sizes

The core logic remains the same regardless of your UI framework choice.

Important Considerations

Rate Limiting

Para’s API has rate limits. Process handles in batches with delays:

// Process 10 handles at a time with 1-second delays
const batchSize = 10;
const delay = 1000; // 1 second between batches

Error Handling

Always implement retry logic for failed generations:

const retryFailed = async (failedResults: BatchResult[]) => {
  const failedHandles = failedResults
    .filter(result => !result.success)
    .map(result => result.handle);
  
  // Retry processing
  await processBatch(failedHandles);
};

Data Storage

Store wallet data securely in your database:

  • Wallet addresses for reference
  • User shares for wallet claiming
  • Handle mappings for Twitter username lookup
  • Generation timestamps for tracking

Testing Your Implementation

  1. Start with small batches (5-10 handles)
  2. Use test Twitter handles that you control
  3. Verify wallet generation in Para Developer Portal
  4. Test claiming flow with actual Twitter authentication

Next Steps