> ## Documentation Index
> Fetch the complete documentation index at: https://docs.neynar.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Farcaster Address Verification with EIP-712

> A step-by-step guide to verifying an Ethereum address on Farcaster using EIP-712, Privy RPC, Viem, and Neynar.

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

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

***

## Prerequisites

* Node.js ≥ 18
* A `.env` file with:
  ```env theme={"system"}
  PRIVY_VALIDATION_FID=...
  PRIVY_VALIDATION_ADDRESS=0x...
  PRIVY_VALIDATION_CLIENT_ID=...
  PRIVY_ID=...
  PRIVY_SECRET=...
  NEYNAR_API_KEY=...
  ADDRESS_VALIDATION_SIGNER_UUID=...
  ```
* Install dependencies:
  <CodeGroup>
    ```bash Bash theme={"system"}
    npm install dotenv viem buffer node-fetch
    ```
  </CodeGroup>

***

## 1. Setup & Imports

Start by importing dependencies and loading your environment variables:

<CodeGroup>
  ```typescript privy-validation-signer.ts theme={"system"}
  import { config } from 'dotenv'
  import { Buffer } from 'buffer'
  import { Address, createPublicClient, hashTypedData, http } from 'viem'
  import { optimism } from 'viem/chains'

  config() // load .env
  ```
</CodeGroup>

***

## 2. Environment Variables & Constants

Define your configuration and constants:

<CodeGroup>
  ```typescript privy-validation-signer.ts theme={"system"}
  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!
  ```
</CodeGroup>

***

## 3. EIP-712 Domain & Types

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

<CodeGroup>
  ```typescript privy-validation-signer.ts theme={"system"}
  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
  ```
</CodeGroup>

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:

<CodeGroup>
  ```typescript privy-validation-signer.ts theme={"system"}
  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
      },
    }
  }
  ```
</CodeGroup>

***

## 5. Compute the EIP-712 Hash

Use Viem's `hashTypedData` to get the digest for signing:

<CodeGroup>
  ```typescript privy-validation-signer.ts theme={"system"}
  const typedDataHash = hashTypedData({
    domain:      typedData.domain,
    types:       typedData.types,
    primaryType: typedData.primaryType,
    message:     typedData.message,
  })
  ```
</CodeGroup>

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:

<CodeGroup>
  ```typescript privy-validation-signer.ts theme={"system"}
  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}`
  }
  ```
</CodeGroup>

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:

<CodeGroup>
  ```typescript privy-validation-signer.ts theme={"system"}
  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)
  ```
</CodeGroup>

***

## 8. Submit to Neynar

Send the verification to Neynar for on-chain registration:

<CodeGroup>
  ```typescript privy-validation-signer.ts theme={"system"}
  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())
  ```
</CodeGroup>

***

## 9. Error Handling

If anything fails, log and exit:

<CodeGroup>
  ```typescript privy-validation-signer.ts theme={"system"}
  } catch (err) {
    console.error('Error in validation flow:', err)
    process.exit(1)
  }
  ```
</CodeGroup>

***

## References & Further Reading

* [EIP-712 Spec](https://eips.ethereum.org/EIPS/eip-712)
* [Farcaster Hub: validateVerificationAddEthAddressBody](https://github.com/farcasterxyz/hub-monorepo/blob/main/packages/core/src/validations.ts)
* [Farcaster Hub: verifyVerificationEthAddressClaimSignature](https://github.com/farcasterxyz/hub-monorepo/blob/main/packages/core/src/crypto/eip712.ts)
* [Neynar Docs](https://docs.neynar.com/)
* [Privy Docs](https://docs.privy.io/)

<Info>
  If you have questions or want to share what you built, tag <a href="https://warpcast.com/neynar">@neynar</a> on Farcaster or join the <a href="https://neynar.com/slack">Slack</a>!
</Info>

<Accordion title="Full Example Code: privy-validation-signer.ts">
  ```typescript privy-validation-signer.ts theme={"system"}
  /**
   * Tutorial: EIP-712 verification with Privy RPC and Farcaster
   */

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

  config()

  // --- Constants & Configuration ---
  const FID                  = Number(process.env.PRIVY_VALIDATION_FID)               // Farcaster ID
  const FARCASTER_NETWORK    = 1                                                      // 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!

  // --- EIP-712 Schemas (inlined, taken from eip712 in @farcaster/hub-web) ---
  const EIP_712_FARCASTER_VERIFICATION_CLAIM = [
    { name: 'fid',      type: 'uint256' },
    { name: 'address',  type: 'address' },
    { name: 'blockHash',type: 'bytes32' },
    { name: 'network',  type: 'uint8'   },
  ] as const // eip712.EIP_712_FARCASTER_VERIFICATION_CLAIM from @farcaster/hub-web

  const EIP_712_FARCASTER_DOMAIN = {
    name:    'Farcaster Verify Ethereum Address',
    version: '2.0.0',
    salt:    '0xf2d857f4a3edcb9b78b4d503bfe733db1e3f6cdc2b7971ee739626c97e86a558',
  } as const // eip712.EIP_712_FARCASTER_DOMAIN from @farcaster/hub-web

  // --- Helper: compose EIP-712 message ---
  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
      },
    }
  }

  // --- Helper: sign hash via Privy RPC ---
  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}`
  }

  // --- Main Flow ---
  async function main() {
    try {
      // 1) Fetch block hash
      const client    = createPublicClient({ chain: optimism, transport: http() })
      const { hash }  = await client.getBlock()

      // 2) Build EIP-712 message
      const typedData = await getMessageToSign(VALIDATION_ADDRESS, hash)
      console.log('Message to sign:', typedData)

      // 3) Compute EIP-712 hash
      const typedDataHash = hashTypedData({
        domain:      typedData.domain,
        types:       typedData.types,
        primaryType: typedData.primaryType,
        message:     typedData.message,
      })

      // 4) Sign via Privy RPC
      const rpcSig  = await signWithPrivy(typedDataHash)
      console.log('Privy RPC signature:', rpcSig)

      // 5) Verify locally
      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)

      // 6) Submit to Neynar
      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())
    } catch (err) {
      console.error('Error in validation flow:', err)
      process.exit(1)
    }
  }

  main()
  ```
</Accordion>
