In this guide, you’ll learn how to perform a full EIP-712-based Ethereum address verification for Farcaster using:

  • Privy RPC for secure off-chain signing
  • Viem for EIP-712 hash computation and local signature verification
  • Neynar API for submitting the verification to Farcaster

This tutorial is for advanced users who want to automate or deeply understand the Farcaster address verification process, including smart contract wallets.


Prerequisites

  • Node.js ≥ 18
  • A .env file with:
    PRIVY_VALIDATION_FID=...
    PRIVY_VALIDATION_ADDRESS=0x...
    PRIVY_VALIDATION_CLIENT_ID=...
    PRIVY_ID=...
    PRIVY_SECRET=...
    NEYNAR_API_KEY=...
    ADDRESS_VALIDATION_SIGNER_UUID=...
  • Install dependencies:
    npm install dotenv viem buffer node-fetch

1. Setup & Imports

Start by importing dependencies and loading your environment variables:

import { config } from 'dotenv'
import { Buffer } from 'buffer'
import { Address, createPublicClient, hashTypedData, http } from 'viem'
import { optimism } from 'viem/chains'

config() // load .env

2. Environment Variables & Constants

Define your configuration and constants:

const FID                = Number(process.env.PRIVY_VALIDATION_FID)
const FARCASTER_NETWORK  = 1 // Farcaster mainnet
const VALIDATION_ADDRESS = process.env.PRIVY_VALIDATION_ADDRESS as Address
const PRIVY_WALLET_ID    = process.env.PRIVY_VALIDATION_CLIENT_ID!
const PRIVY_APP_ID       = process.env.PRIVY_ID!
const PRIVY_SECRET       = process.env.PRIVY_SECRET!
const NEYNAR_API_KEY     = process.env.NEYNAR_API_KEY!
const SIGNER_UUID        = process.env.ADDRESS_VALIDATION_SIGNER_UUID!

3. EIP-712 Domain & Types

These are taken from Farcaster’s official EIP-712 spec:

const EIP_712_FARCASTER_VERIFICATION_CLAIM = [
  { name: 'fid',      type: 'uint256' },
  { name: 'address',  type: 'address' },
  { name: 'blockHash',type: 'bytes32' },
  { name: 'network',  type: 'uint8'   },
] as const

const EIP_712_FARCASTER_DOMAIN = {
  name:    'Farcaster Verify Ethereum Address',
  version: '2.0.0',
  salt:    '0xf2d857f4a3edcb9b78b4d503bfe733db1e3f6cdc2b7971ee739626c97e86a558',
} as const

We recommend using their package to import these but they’re providing for clarity.


4. Compose the Typed Data

Build the EIP-712 message to be signed. Make sure to include the correct chainId and protocol fields as these are different for smart account verification:

async function getMessageToSign(address: Address, blockHash: `0x${string}`) {
  return {
    domain:   { ...EIP_712_FARCASTER_DOMAIN, chainId: optimism.id },
    types:    { VerificationClaim: EIP_712_FARCASTER_VERIFICATION_CLAIM },
    primaryType: 'VerificationClaim' as const,
    message:  {
      fid:       BigInt(FID),
      address,
      blockHash,
      network:   FARCASTER_NETWORK,
      protocol:  0, // contract flow uses protocol=0
    },
  }
}

5. Compute the EIP-712 Hash

Use Viem’s hashTypedData to get the digest for signing:

const typedDataHash = hashTypedData({
  domain:      typedData.domain,
  types:       typedData.types,
  primaryType: typedData.primaryType,
  message:     typedData.message,
})

We generate the hashTypedData directly due to an issue with privys typed data signers.


6. Sign via Privy RPC

Request a signature from Privy over the EIP-712 hash:

async function signWithPrivy(hash: `0x${string}`) {
  const resp = await fetch(
    `https://api.privy.io/v1/wallets/${PRIVY_WALLET_ID}/rpc`,
    {
      method: 'POST',
      headers: {
        'Content-Type':   'application/json',
        'privy-app-id':   PRIVY_APP_ID,
        'Authorization':  `Basic ${Buffer.from(
                            `${PRIVY_APP_ID}:${PRIVY_SECRET}`
                          ).toString('base64')}`,
      },
      body: JSON.stringify({ method: 'secp256k1_sign', params: { hash } }),
    }
  )
  const { data } = await resp.json()
  return data.signature as `0x${string}`
}

It is necessary to use secp256k1_sign due to the aforementioned issue with their typed signers.


7. Local Signature Verification

Double-check the signature locally before submitting:

const ok = await client.verifyTypedData({
  address:       VALIDATION_ADDRESS,
  domain:        typedData.domain,
  types:         typedData.types,
  primaryType:   typedData.primaryType,
  message:       typedData.message,
  signature:     rpcSig,
})
console.log('Local verification:', ok)

8. Submit to Neynar

Send the verification to Neynar for on-chain registration:

const neynarResp = await fetch(
  'https://api.neynar.com/v2/farcaster/user/verification',
  {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'x-api-key':    NEYNAR_API_KEY,
    },
    body: JSON.stringify({
      signer_uuid:       SIGNER_UUID,
      address:           VALIDATION_ADDRESS,
      block_hash:        hash,
      eth_signature:     rpcSig,
      verification_type: 1,
      chain_id:          optimism.id,
    }),
  }
)
console.log('Neynar response:', await neynarResp.json())

9. Error Handling

If anything fails, log and exit:

} catch (err) {
  console.error('Error in validation flow:', err)
  process.exit(1)
}

References & Further Reading

If you have questions or want to share what you built, tag @neynar on Farcaster or join the Telegram!