Comparing aave with farcaster

Author

@0xinit

Stars

53

Repository

0xinit/cryptoskills

skills/aave/SKILL.md

Aave V3

Aave V3 is the dominant on-chain lending protocol. Users supply assets to earn yield and borrow against collateral. The protocol runs on Ethereum, Arbitrum, Optimism, Base, Polygon, and other EVM chains with identical interfaces. All interaction goes through the IPool contract.

What You Probably Got Wrong

LLMs confuse V2 and V3 constantly. V3 has different interfaces, different addresses, and different behavior. These corrections are non-negotiable.

  • V3 is not V2 — different interfaces everywhere — V2 uses LendingPool with deposit(). V3 uses Pool with supply(). The function signatures, events, and return types differ. If you see ILendingPool or deposit(), you are writing V2 code. Stop.
  • aTokens rebase — balance changes every blockaToken.balanceOf(user) increases each block as interest accrues. This is not a transfer. Do not try to track balances with Transfer events alone. Use balanceOf() at read time or scaledBalanceOf() for the underlying non-rebasing amount.
  • Stable rate borrowing is deprecated on most markets — Aave governance disabled stable rate borrows on Ethereum mainnet and most L2 deployments. Use VARIABLE_RATE = 2 for the interestRateMode parameter. Passing STABLE_RATE = 1 will revert on markets where it is disabled.
  • Health factor is 18-decimal fixed point, not a percentagegetUserAccountData() returns healthFactor as a uint256 with 18 decimals. A health factor of 1e18 means liquidation threshold. Below 1e18 = liquidatable. Do not divide by 100.
  • Flash loan fee is 0.05% on V3, not 0.09% — V3 reduced the default flash loan premium from 0.09% (V2) to 0.05%. The exact fee is configurable per market via governance. Check FLASHLOAN_PREMIUM_TOTAL on the Pool contract.
  • E-Mode changes collateral/borrow parameters — Enabling E-Mode (Efficiency Mode) overrides LTV, liquidation threshold, and liquidation bonus for assets in the same category (e.g., stablecoins). It does NOT change the underlying asset. Forgetting this causes incorrect health factor calculations.
  • Supply caps exist in V3 — V3 introduced per-asset supply and borrow caps. supply() will revert if the cap is reached. Always check getReserveData() for supplyCap before large deposits.
  • V3 addresses are different per chain AND per market — Ethereum has a "Main" market and a "Lido" market with completely different Pool addresses. Always verify you are targeting the correct market.

Quick Start

Installation

npm install @aave/aave-v3-core viem

For TypeScript projects, the @aave/aave-v3-core package provides Solidity interfaces. For frontend/backend interaction, use viem directly with the ABIs below.

Minimal ABI Fragments

const poolAbi = [
  {
    name: "supply",
    type: "function",
    stateMutability: "nonpayable",
    inputs: [
      { name: "asset", type: "address" },
      { name: "amount", type: "uint256" },
      { name: "onBehalfOf", type: "address" },
      { name: "referralCode", type: "uint16" },
    ],
    outputs: [],
  },
  {
    name: "borrow",
    type: "function",
    stateMutability: "nonpayable",
    inputs: [
      { name: "asset", type: "address" },
      { name: "amount", type: "uint256" },
      { name: "interestRateMode", type: "uint256" },
      { name: "referralCode", type: "uint16" },
      { name: "onBehalfOf", type: "address" },
    ],
    outputs: [],
  },
  {
    name: "repay",
    type: "function",
    stateMutability: "nonpayable",
    inputs: [
      { name: "asset", type: "address" },
      { name: "amount", type: "uint256" },
      { name: "interestRateMode", type: "uint256" },
      { name: "onBehalfOf", type: "address" },
    ],
    outputs: [{ name: "", type: "uint256" }],
  },
  {
    name: "withdraw",
    type: "function",
    stateMutability: "nonpayable",
    inputs: [
      { name: "asset", type: "address" },
      { name: "amount", type: "uint256" },
      { name: "to", type: "address" },
    ],
    outputs: [{ name: "", type: "uint256" }],
  },
  {
    name: "getUserAccountData",
    type: "function",
    stateMutability: "view",
    inputs: [{ name: "user", type: "address" }],
    outputs: [
      { name: "totalCollateralBase", type: "uint256" },
      { name: "totalDebtBase", type: "uint256" },
      { name: "availableBorrowsBase", type: "uint256" },
      { name: "currentLiquidationThreshold", type: "uint256" },
      { name: "ltv", type: "uint256" },
      { name: "healthFactor", type: "uint256" },
    ],
  },
  {
    name: "flashLoanSimple",
    type: "function",
    stateMutability: "nonpayable",
    inputs: [
      { name: "receiverAddress", type: "address" },
      { name: "asset", type: "address" },
      { name: "amount", type: "uint256" },
      { name: "params", type: "bytes" },
      { name: "referralCode", type: "uint16" },
    ],
    outputs: [],
  },
  {
    name: "setUserEMode",
    type: "function",
    stateMutability: "nonpayable",
    inputs: [{ name: "categoryId", type: "uint8" }],
    outputs: [],
  },
  {
    name: "getReserveData",
    type: "function",
    stateMutability: "view",
    inputs: [{ name: "asset", type: "address" }],
    outputs: [
      {
        name: "",
        type: "tuple",
        components: [
          { name: "configuration", type: "uint256" },
          { name: "liquidityIndex", type: "uint128" },
          { name: "currentLiquidityRate", type: "uint128" },
          { name: "variableBorrowIndex", type: "uint128" },
          { name: "currentVariableBorrowRate", type: "uint128" },
          { name: "currentStableBorrowRate", type: "uint128" },
          { name: "lastUpdateTimestamp", type: "uint40" },
          { name: "id", type: "uint16" },
          { name: "aTokenAddress", type: "address" },
          { name: "stableDebtTokenAddress", type: "address" },
          { name: "variableDebtTokenAddress", type: "address" },
          { name: "interestRateStrategyAddress", type: "address" },
          { name: "accruedToTreasury", type: "uint128" },
          { name: "unbacked", type: "uint128" },
          { name: "isolationModeTotalDebt", type: "uint128" },
        ],
      },
    ],
  },
] as const;

const erc20Abi = [
  {
    name: "approve",
    type: "function",
    stateMutability: "nonpayable",
    inputs: [
      { name: "spender", type: "address" },
      { name: "amount", type: "uint256" },
    ],
    outputs: [{ name: "", type: "bool" }],
  },
  {
    name: "balanceOf",
    type: "function",
    stateMutability: "view",
    inputs: [{ name: "account", type: "address" }],
    outputs: [{ name: "", type: "uint256" }],
  },
] as const;

Basic Supply (TypeScript)

import { createPublicClient, createWalletClient, http, parseUnits } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { mainnet } from "viem/chains";

const POOL = "0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2" as const;
const USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" as const;

const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);

const publicClient = createPublicClient({
  chain: mainnet,
  transport: http(process.env.RPC_URL),
});

const walletClient = createWalletClient({
  account,
  chain: mainnet,
  transport: http(process.env.RPC_URL),
});

const amount = parseUnits("1000", 6); // 1000 USDC

// Approve Pool to spend USDC
const approveHash = await walletClient.writeContract({
  address: USDC,
  abi: erc20Abi,
  functionName: "approve",
  args: [POOL, amount],
});
await publicClient.waitForTransactionReceipt({ hash: approveHash });

// Supply USDC to Aave
const supplyHash = await walletClient.writeContract({
  address: POOL,
  abi: poolAbi,
  functionName: "supply",
  args: [USDC, amount, account.address, 0],
});
const receipt = await publicClient.waitForTransactionReceipt({ hash: supplyHash });

if (receipt.status !== "success") {
  throw new Error("Supply transaction reverted");
}

Core Operations

Supply (Solidity)

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {IPool} from "@aave/aave-v3-core/contracts/interfaces/IPool.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract AaveSupplier {
    IPool public immutable pool;

    constructor(address _pool) {
        pool = IPool(_pool);
    }

    /// @notice Supply asset to Aave V3 on behalf of msg.sender
    /// @param asset ERC20 token to supply
    /// @param amount Amount in token's native decimals
    function supplyToAave(address asset, uint256 amount) external {
        IERC20(asset).transferFrom(msg.sender, address(this), amount);
        IERC20(asset).approve(address(pool), amount);
        pool.supply(asset, amount, msg.sender, 0);
    }
}

Borrow (TypeScript)

// Variable rate = 2. Stable rate (1) is deprecated on most markets.
const VARIABLE_RATE = 2n;

const borrowHash = await walletClient.writeContract({
  address: POOL,
  abi: poolAbi,
  functionName: "borrow",
  args: [USDC, parseUnits("500", 6), VARIABLE_RATE, 0, account.address],
});
const borrowReceipt = await publicClient.waitForTransactionReceipt({ hash: borrowHash });

if (borrowReceipt.status !== "success") {
  throw new Error("Borrow transaction reverted");
}

Repay (TypeScript)

const repayAmount = parseUnits("500", 6);

// Approve Pool to pull repayment
const repayApproveHash = await walletClient.writeContract({
  address: USDC,
  abi: erc20Abi,
  functionName: "approve",
  args: [POOL, repayAmount],
});
await publicClient.waitForTransactionReceipt({ hash: repayApproveHash });

// type(uint256).max to repay entire debt
const repayHash = await walletClient.writeContract({
  address: POOL,
  abi: poolAbi,
  functionName: "repay",
  args: [USDC, repayAmount, 2n, account.address],
});
const repayReceipt = await publicClient.waitForTransactionReceipt({ hash: repayHash });

if (repayReceipt.status !== "success") {
  throw new Error("Repay transaction reverted");
}

Withdraw (TypeScript)

// type(uint256).max withdraws entire balance
const maxUint256 = 2n ** 256n - 1n;

const withdrawHash = await walletClient.writeContract({
  address: POOL,
  abi: poolAbi,
  functionName: "withdraw",
  args: [USDC, maxUint256, account.address],
});
const withdrawReceipt = await publicClient.waitForTransactionReceipt({
  hash: withdrawHash,
});

if (withdrawReceipt.status !== "success") {
  throw new Error("Withdraw transaction reverted");
}

Repay and Withdraw (Solidity)

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {IPool} from "@aave/aave-v3-core/contracts/interfaces/IPool.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract AavePositionManager {
    IPool public immutable pool;

    constructor(address _pool) {
        pool = IPool(_pool);
    }

    /// @notice Repay variable-rate debt on behalf of msg.sender
    function repayDebt(address asset, uint256 amount) external {
        IERC20(asset).transferFrom(msg.sender, address(this), amount);
        IERC20(asset).approve(address(pool), amount);
        // interestRateMode 2 = variable rate
        pool.repay(asset, amount, 2, msg.sender);
    }

    /// @notice Withdraw supplied asset back to msg.sender
    /// @param amount Use type(uint256).max to withdraw entire balance
    function withdrawFromAave(address asset, uint256 amount) external {
        pool.withdraw(asset, amount, msg.sender);
    }
}

Flash Loans

V3 provides flashLoanSimple for single-asset flash loans (simpler interface, lower gas) and flashLoan for multi-asset. The fee is 0.05% by default.

Flash Loan Receiver (Solidity)

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {IPool} from "@aave/aave-v3-core/contracts/interfaces/IPool.sol";
import {IFlashLoanSimpleReceiver} from
    "@aave/aave-v3-core/contracts/flashloan/base/FlashLoanSimpleReceiver.sol";
import {IPoolAddressesProvider} from
    "@aave/aave-v3-core/contracts/interfaces/IPoolAddressesProvider.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract SimpleFlashLoan is IFlashLoanSimpleReceiver {
    IPoolAddressesProvider public immutable override ADDRESSES_PROVIDER;
    IPool public immutable override POOL;

    constructor(address provider) {
        ADDRESSES_PROVIDER = IPoolAddressesProvider(provider);
        POOL = IPool(IPoolAddressesProvider(provider).getPool());
    }

    /// @notice Called by Aave Pool after flash loan funds are transferred
    /// @dev Must approve Pool to pull back (amount + premium) before returning
    function executeOperation(
        address asset,
        uint256 amount,
        uint256 premium,
        address initiator,
        bytes calldata /* params */
    ) external override returns (bool) {
        if (msg.sender != address(POOL)) revert("Caller not Pool");
        if (initiator != address(this)) revert("Initiator not this contract");

        // --- Custom logic here ---
        // You have `amount` of `asset` available in this contract.
        // Do arbitrage, liquidation, collateral swap, etc.

        // Repay flash loan: approve Pool to pull amount + fee
        uint256 amountOwed = amount + premium;
        IERC20(asset).approve(address(POOL), amountOwed);

        return true;
    }

    /// @notice Trigger a flash loan
    /// @param asset Token to borrow
    /// @param amount Amount to flash borrow
    function requestFlashLoan(address asset, uint256 amount) external {
        POOL.flashLoanSimple(address(this), asset, amount, "", 0);
    }
}

Trigger Flash Loan (TypeScript)

const flashLoanContractAddress = "0x...YOUR_DEPLOYED_CONTRACT..." as `0x${string}`;

const flashLoanAbi = [
  {
    name: "requestFlashLoan",
    type: "function",
    stateMutability: "nonpayable",
    inputs: [
      { name: "asset", type: "address" },
      { name: "amount", type: "uint256" },
    ],
    outputs: [],
  },
] as const;

// Flash borrow 1M USDC
const txHash = await walletClient.writeContract({
  address: flashLoanContractAddress,
  abi: flashLoanAbi,
  functionName: "requestFlashLoan",
  args: [USDC, parseUnits("1000000", 6)],
});

const flashReceipt = await publicClient.waitForTransactionReceipt({ hash: txHash });

if (flashReceipt.status !== "success") {
  throw new Error("Flash loan reverted");
}

Reading Protocol State

Get User Account Data

const [
  totalCollateralBase,
  totalDebtBase,
  availableBorrowsBase,
  currentLiquidationThreshold,
  ltv,
  healthFactor,
] = await publicClient.readContract({
  address: POOL,
  abi: poolAbi,
  functionName: "getUserAccountData",
  args: [account.address],
});

// All "Base" values are in USD with 8 decimals (Aave oracle base currency)
const collateralUsd = Number(totalCollateralBase) / 1e8;
const debtUsd = Number(totalDebtBase) / 1e8;

// healthFactor has 18 decimals. Below 1e18 = liquidatable.
const hf = Number(healthFactor) / 1e18;
console.log(`Health Factor: ${hf}`);
console.log(`Collateral: $${collateralUsd}, Debt: $${debtUsd}`);

Get Reserve Data

const reserveData = await publicClient.readContract({
  address: POOL,
  abi: poolAbi,
  functionName: "getReserveData",
  args: [USDC],
});

// Supply APY: currentLiquidityRate is a ray (27 decimals)
const supplyRateRay = reserveData.currentLiquidityRate;
const supplyAPY = Number(supplyRateRay) / 1e27;
console.log(`USDC Supply APY: ${(supplyAPY * 100).toFixed(2)}%`);

// aToken address for this reserve
const aTokenAddress = reserveData.aTokenAddress;

Track aToken Balance

// aToken balance includes accrued interest (rebases every block)
const aUsdcBalance = await publicClient.readContract({
  address: reserveData.aTokenAddress,
  abi: erc20Abi,
  functionName: "balanceOf",
  args: [account.address],
});

// Balance in human-readable format (USDC has 6 decimals)
const balanceFormatted = Number(aUsdcBalance) / 1e6;
console.log(`aUSDC balance: ${balanceFormatted}`);

Contract Addresses

Last verified: 2025-05-01

All Aave V3 deployments share the same interface. Addresses sourced from @bgd-labs/aave-address-book and official Aave governance.

Pool (main entry point for all operations)

ChainAddress
Ethereum0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2
Arbitrum0x794a61358D6845594F94dc1DB02A252b5b4814aD
Optimism0x794a61358D6845594F94dc1DB02A252b5b4814aD
Polygon0x794a61358D6845594F94dc1DB02A252b5b4814aD
Base0xA238Dd80C259a72e81d7e4664a9801593F98d1c5

PoolAddressesProvider

ChainAddress
Ethereum0x2f39d218133AFaB8F2B819B1066c7E434Ad94E9e
Arbitrum0xa97684ead0e402dC232d5A977953DF7ECBaB3CDb
Optimism0xa97684ead0e402dC232d5A977953DF7ECBaB3CDb
Polygon0xa97684ead0e402dC232d5A977953DF7ECBaB3CDb
Base0xe20fCBdBfFC4Dd138cE8b2E6FBb6CB49777ad64D

Aave Oracle

ChainAddress
Ethereum0x54586bE62E3c3580375aE3723C145253060Ca0C2
Arbitrum0xb56c2F0B653B2e0b10C9b928C8580Ac5Df02C7C7
Optimism0xD81eb3728a631871a7eBBaD631b5f424909f0c77
Polygon0xb023e699F5a33916Ea823A16485e259257cA8Bd1
Base0x2Cc0Fc26eD4563A5ce5e8bdcfe1A2878676Ae156

Common Token Addresses (Ethereum Mainnet)

TokenAddress
WETH0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
USDC0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
USDT0xdAC17F958D2ee523a2206206994597C13D831ec7
DAI0x6B175474E89094C44Da98b954EedeAC495271d0F
WBTC0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599

Verify any address before mainnet use: cast code <address> --rpc-url $RPC_URL

E-Mode (Efficiency Mode)

E-Mode lets users achieve higher capital efficiency when borrowing and supplying correlated assets (e.g., stablecoins against stablecoins, ETH against stETH).

How It Works

Each E-Mode category defines:

  • Higher LTV (e.g., 97% for stablecoins vs 75% default)
  • Higher liquidation threshold (e.g., 97.5%)
  • Lower liquidation bonus (e.g., 1% vs 5%)
  • Optional oracle override for the category

Common E-Mode Categories

IDLabelTypical LTVUse Case
0None (default)Varies per assetGeneral lending
1Stablecoins97%Borrow USDT against USDC
2ETH correlated93%Borrow WETH against wstETH

Category IDs and parameters vary by chain and market. Query on-chain.

Enable E-Mode (TypeScript)

// Enable stablecoin E-Mode (category 1)
const emodeHash = await walletClient.writeContract({
  address: POOL,
  abi: poolAbi,
  functionName: "setUserEMode",
  args: [1],
});
await publicClient.waitForTransactionReceipt({ hash: emodeHash });

Enable E-Mode (Solidity)

// Enable E-Mode before supplying/borrowing for higher LTV
pool.setUserEMode(1); // 1 = stablecoins category

// To disable, set back to 0
// Reverts if current position would be undercollateralized without E-Mode
pool.setUserEMode(0);

E-Mode Constraint

You can only borrow assets that belong to the active E-Mode category. Supplying is unrestricted. Setting E-Mode to 0 reverts if your position would become unhealthy at default LTV/threshold.

Error Handling

Error CodeNameCauseFix
1CALLER_NOT_POOL_ADMINNon-admin calling admin functionUse correct admin account
26COLLATERAL_CANNOT_COVER_NEW_BORROWInsufficient collateral for borrowSupply more collateral or borrow less
27COLLATERAL_SAME_AS_BORROWING_CURRENCYCannot use same asset as collateral and borrow in isolation modeUse a different collateral
28AMOUNT_BIGGER_THAN_MAX_LOAN_SIZE_STABLEStable rate borrow exceeds limitUse variable rate (interestRateMode = 2)
29NO_DEBT_OF_SELECTED_TYPERepaying debt type that does not existCheck interestRateMode matches your debt
30NO_EXPLICIT_AMOUNT_TO_REPAY_ON_BEHALFRepaying on behalf with type(uint256).maxSpecify exact repay amount when paying for another user
35HEALTH_FACTOR_LOWER_THAN_LIQUIDATION_THRESHOLDAction would make position liquidatableReduce borrow amount or add collateral
36INCONSISTENT_EMODE_CATEGORYBorrowing asset outside active E-Mode categorySwitch E-Mode or borrow a compatible asset
50SUPPLY_CAP_EXCEEDEDAsset supply cap reachedWait for withdrawals or use a different market
51BORROW_CAP_EXCEEDEDAsset borrow cap reachedWait for repayments or use a different market

Full error code list: contracts/protocol/libraries/helpers/Errors.sol in aave-v3-core

Handling Reverts in TypeScript

import { BaseError, ContractFunctionRevertedError } from "viem";

try {
  await publicClient.simulateContract({
    address: POOL,
    abi: poolAbi,
    functionName: "borrow",
    args: [USDC, parseUnits("500", 6), 2n, 0, account.address],
    account: account.address,
  });
} catch (err) {
  if (err instanceof BaseError) {
    const revertError = err.walk(
      (e) => e instanceof ContractFunctionRevertedError
    );
    if (revertError instanceof ContractFunctionRevertedError) {
      const errorName = revertError.data?.errorName;
      console.error(`Aave revert: ${errorName}`);
    }
  }
}

Security

Health Factor Monitoring

A health factor below 1.0 means the position is liquidatable. Third-party liquidators actively monitor the mempool. Always maintain a buffer.

async function checkHealthFactor(
  userAddress: `0x${string}`
): Promise<{ safe: boolean; healthFactor: number }> {
  const [, , , , , healthFactor] = await publicClient.readContract({
    address: POOL,
    abi: poolAbi,
    functionName: "getUserAccountData",
    args: [userAddress],
  });

  const hf = Number(healthFactor) / 1e18;

  // 1.5 is a conservative safety buffer
  return { safe: hf > 1.5, healthFactor: hf };
}

Liquidation Risk Factors

  • Oracle price movement — If collateral price drops or debt price increases, health factor drops. Aave uses Chainlink oracles; check feed freshness.
  • Accruing interest — Variable borrow rates compound. A 50% APY borrow accumulates debt faster than most users expect.
  • E-Mode exit — Disabling E-Mode instantly applies lower LTV/thresholds. A safe E-Mode position may become liquidatable at default parameters.
  • Supply cap filling — If you need to add emergency collateral and the supply cap is full, you cannot. Diversify collateral types.

Best Practices

  1. Simulate before executing — Always call simulateContract before writeContract to catch reverts without spending gas.
  2. Check receipt.status — A confirmed transaction can still revert. Always verify receipt.status === "success".
  3. Monitor health factor off-chain — Set up alerts when HF drops below 2.0. Automate repayment or collateral addition below 1.5.
  4. Never hardcode gas limits for Aave calls — Pool operations have variable gas costs depending on reserves touched, E-Mode state, and isolation mode. Let the node estimate.
  5. Approve exact amounts — Avoid type(uint256).max approvals in production. Approve only what is needed per transaction.

References

farcaster

View full →

Author

@0xinit

Stars

53

Repository

0xinit/cryptoskills

skills/farcaster/SKILL.md

Farcaster

Farcaster is a sufficiently decentralized social protocol. Users register onchain identities (FIDs) on OP Mainnet and publish social data (casts, reactions, links) as offchain messages to Snapchain, a purpose-built message ordering layer. Neynar provides the primary API infrastructure and, since January 2026, owns the Farcaster protocol itself. Frames v2 (Mini Apps) enable full-screen interactive web applications embedded inside Farcaster clients like Warpcast.

What You Probably Got Wrong

  • Manifest accountAssociation domain MUST exactly match the FQDN where /.well-known/farcaster.json is hosted. A mismatch causes silent failure -- the Mini App will not load, no error is surfaced to the developer, and Warpcast simply shows nothing. The domain in the signature payload must be byte-identical to the hosting domain (no trailing slash, no protocol prefix, no port unless non-standard).

  • Neynar webhooks MUST be verified via HMAC-SHA512 at write time. Check the X-Neynar-Signature header against the raw request body. Never parse JSON before verification -- you must verify the raw bytes.

import crypto from "node:crypto";
import type { IncomingHttpHeaders } from "node:http";

function verifyNeynarWebhook(
  rawBody: Buffer,
  headers: IncomingHttpHeaders,
  webhookSecret: string
): boolean {
  const signature = headers["x-neynar-signature"];
  if (typeof signature !== "string") return false;

  const hmac = crypto.createHmac("sha512", webhookSecret);
  hmac.update(rawBody);
  const computedSignature = hmac.digest("hex");

  return crypto.timingSafeEqual(
    Buffer.from(signature, "hex"),
    Buffer.from(computedSignature, "hex")
  );
}
  • Farcaster is NOT a blockchain. It is a social protocol with an onchain registry (OP Mainnet) for identity and key management, plus an offchain message layer (Snapchain) for social data. Casts, reactions, and follows are never posted to any blockchain.

  • FIDs are onchain but casts are NOT. Farcaster IDs (FIDs) live in the IdRegistry contract on OP Mainnet. Casts, reactions, and link messages are stored on Snapchain and are not onchain data.

  • Frames v2 is NOT Frames v1 -- completely different spec. Frames v1 used static OG images with action buttons and server-side rendering. Frames v2 (Mini Apps) are full-screen interactive web applications loaded in an iframe with SDK access to wallet, user context, and notifications. Do not mix the two APIs.

  • Neynar is NOT just an API provider. Neynar acquired Farcaster from Merkle Manufactory in January 2026. Neynar now owns and operates the protocol, the Snapchain infrastructure, and the primary API layer.

  • Frame images must be static. Frame preview images (OG images shown in feed) cannot contain JavaScript. They are rendered as static images by the client. Interactive behavior only works inside the launched Mini App.

  • @farcaster/frame-sdk and @farcaster/miniapp-sdk are converging. Both packages exist but frame-sdk is the current stable package for Frames v2. Check import paths -- functionality overlaps but the packages are not yet unified.

  • Farcaster timestamps use a custom epoch. Timestamps are seconds since January 1, 2021 00:00:00 UTC (Farcaster epoch), not Unix epoch. To convert: unixTimestamp = farcasterTimestamp + 1609459200.

  • Cast text has a 1024 BYTE limit, not characters. UTF-8 multibyte characters (emoji, CJK, accented characters) consume 2-4 bytes each. A 1024-character cast with emoji will exceed the limit.

  • Warpcast aggressively caches OG/frame images. Changing content at the same URL will not update the preview in Warpcast feeds. Use cache-busting query parameters or new URLs when updating frame images.

Critical Context

Neynar acquired Farcaster from Merkle Manufactory in January 2026. This means:

  • Neynar operates the protocol, Snapchain validators, and the Hub network
  • The Neynar API is the canonical way to interact with Farcaster
  • Warpcast remains the primary client, now under Neynar's umbrella
  • The open-source protocol spec and hub software remain MIT-licensed
  • Third-party hubs can still run, but Neynar controls the reference implementation

Protocol Architecture

Snapchain

Snapchain replaced the Hub network in April 2025 as Farcaster's offchain message ordering layer.

PropertyDetail
ConsensusMalachite BFT (Tendermint-derived)
Throughput10,000+ messages per second
ShardingAccount-level -- each FID's messages are ordered independently
FinalitySub-second for message acceptance
Data modelAppend-only log of signed messages per FID
Validator setOperated by Neynar (post-acquisition)

Messages on Snapchain are CRDTs (Conflict-free Replicated Data Types). Each message type has merge rules that ensure consistency across nodes without coordination:

  • CastAdd conflicts with a later CastRemove for the same hash -- remove wins
  • ReactionAdd conflicts with ReactionRemove for the same target -- last-write-wins by timestamp
  • LinkAdd conflicts with LinkRemove -- last-write-wins by timestamp

Message Structure

Every Farcaster message is an Ed25519-signed protobuf:

MessageData {
  type: MessageType     // CAST_ADD, REACTION_ADD, LINK_ADD, etc.
  fid: uint64           // Farcaster ID of the author
  timestamp: uint32     // Farcaster epoch seconds
  network: Network      // MAINNET = 1
  body: MessageBody     // Type-specific payload
}

Message {
  data: MessageData
  hash: bytes           // Blake3 hash of serialized MessageData
  hash_scheme: BLAKE3
  signature: bytes      // Ed25519 signature over hash
  signature_scheme: ED25519
  signer: bytes         // Public key of the signer (app key)
}

Onchain Registry (OP Mainnet)

Farcaster's onchain contracts manage identity, keys, and storage on OP Mainnet.

Last verified: March 2026

ContractAddressPurpose
IdRegistry0x00000000Fc6c5F01Fc30151999387Bb99A9f489bMaps FIDs to custody addresses
KeyRegistry0x00000000Fc1237824fb747aBDE0FF18990E59b7eMaps FIDs to Ed25519 app keys (signers)
StorageRegistry0x00000000FcCe7f938e7aE6D3c335bD6a1a7c593DManages storage units per FID
IdGateway0x00000000Fc25870C6eD6b6c7E41Fb078b7656f69Permissioned FID registration entry point
KeyGateway0x00000000fC56947c7E7183f8Ca4B62398CaaDF0BPermissioned key addition entry point
Bundler0x00000000FC04c910A0b5feA33b03E0447ad0B0aABatches register + addKey + rent in one tx
# Verify IdRegistry is deployed on OP Mainnet
cast code 0x00000000Fc6c5F01Fc30151999387Bb99A9f489b --rpc-url https://mainnet.optimism.io

# Look up custody address for an FID
cast call 0x00000000Fc6c5F01Fc30151999387Bb99A9f489b \
  "custodyOf(uint256)(address)" 3 \
  --rpc-url https://mainnet.optimism.io

Registration Flow

1. User calls IdGateway.register() or Bundler.register()
   -> IdRegistry assigns next sequential FID to custody address
       |
2. User (or Bundler) calls KeyGateway.add()
   -> KeyRegistry maps FID to an Ed25519 public key (app key / signer)
       |
3. User (or Bundler) calls StorageRegistry.rent()
   -> Allocates storage units (each unit = 5,000 casts, 2,500 reactions, 2,500 links)
       |
4. App key can now sign Farcaster messages on behalf of the FID

Farcaster IDs (FIDs)

Every Farcaster user has an FID -- a sequentially assigned uint256 stored in IdRegistry on OP Mainnet.

ConceptDescription
FIDThe user's numeric identity, immutable once assigned
Custody addressThe Ethereum address that owns the FID -- can transfer ownership
App key (signer)Ed25519 key pair registered in KeyRegistry -- signs messages
Recovery addressCan initiate FID recovery if custody address is compromised

An FID can have multiple app keys. Each app (Warpcast, third-party client) registers its own app key via KeyGateway. The custody address can revoke any app key by calling KeyRegistry.remove().

Neynar API v2

Neynar provides the primary API for reading and writing Farcaster data. Current SDK version: @neynar/nodejs-sdk v3.131.0.

Setup

npm install @neynar/nodejs-sdk
import { NeynarAPIClient, Configuration } from "@neynar/nodejs-sdk";

const config = new Configuration({
  apiKey: process.env.NEYNAR_API_KEY,
});

const neynar = new NeynarAPIClient(config);

Fetch User by FID

const { users } = await neynar.fetchBulkUsers({ fids: [3] });
const user = users[0];
console.log(user.username, user.display_name, user.follower_count);

Publish a Cast

const response = await neynar.publishCast({
  signerUuid: process.env.SIGNER_UUID,
  text: "Hello from Neynar SDK",
});
console.log(response.cast.hash);

Fetch Feed

const feed = await neynar.fetchFeed({
  feedType: "following",
  fid: 3,
  limit: 25,
});

for (const cast of feed.casts) {
  console.log(`@${cast.author.username}: ${cast.text}`);
}

Search Users

const result = await neynar.searchUser({ q: "vitalik", limit: 5 });
for (const user of result.result.users) {
  console.log(`FID ${user.fid}: @${user.username}`);
}

Fetch Cast by Hash

const { cast } = await neynar.lookupCastByHashOrWarpcastUrl({
  identifier: "0xfe90f9de682273e05b201629ad2338bdcd89b6be",
  type: "hash",
});
console.log(cast.text, cast.reactions.likes_count);

Webhook Configuration

Create webhooks in the Neynar dashboard or via API. Webhooks fire on cast creation, reaction events, follow events, and more.

import express from "express";
import crypto from "node:crypto";

const app = express();

// Raw body is required for signature verification
app.use("/webhook", express.raw({ type: "application/json" }));

app.post("/webhook", (req, res) => {
  const rawBody = req.body as Buffer;
  const signature = req.headers["x-neynar-signature"] as string;

  if (!signature) {
    res.status(401).json({ error: "Missing signature" });
    return;
  }

  const hmac = crypto.createHmac("sha512", process.env.NEYNAR_WEBHOOK_SECRET!);
  hmac.update(rawBody);
  const computed = hmac.digest("hex");

  const isValid = crypto.timingSafeEqual(
    Buffer.from(signature, "hex"),
    Buffer.from(computed, "hex")
  );

  if (!isValid) {
    res.status(401).json({ error: "Invalid signature" });
    return;
  }

  const event = JSON.parse(rawBody.toString("utf-8"));
  console.log("Verified webhook event:", event.type);

  res.status(200).json({ status: "ok" });
});

app.listen(3001, () => console.log("Webhook listener on :3001"));

Frames v2 / Mini Apps

Frames v2 are full-screen interactive web applications embedded inside Farcaster clients. They replaced the static image + button model of Frames v1 with a rich SDK-powered experience.

Manifest (/.well-known/farcaster.json)

Every Mini App must serve a manifest at /.well-known/farcaster.json on its domain:

{
  "accountAssociation": {
    "header": "eyJmaWQiOjM...",
    "payload": "eyJkb21haW4iOiJleGFtcGxlLmNvbSJ9",
    "signature": "abc123..."
  },
  "frame": {
    "version": "1",
    "name": "My Mini App",
    "iconUrl": "https://example.com/icon.png",
    "homeUrl": "https://example.com/app",
    "splashImageUrl": "https://example.com/splash.png",
    "splashBackgroundColor": "#1a1a2e",
    "webhookUrl": "https://example.com/api/webhook"
  }
}

The accountAssociation proves that the FID owner controls the domain. The payload decoded is {"domain":"example.com"} -- this domain MUST match the FQDN hosting the manifest file.

Meta Tags

Add these to your app's HTML <head> for Farcaster clients to discover the Mini App:

<meta name="fc:frame" content='{"version":"next","imageUrl":"https://example.com/og.png","button":{"title":"Launch App","action":{"type":"launch_frame","name":"My App","url":"https://example.com/app","splashImageUrl":"https://example.com/splash.png","splashBackgroundColor":"#1a1a2e"}}}' />

Frame SDK Setup

npm install @farcaster/frame-sdk
import sdk from "@farcaster/frame-sdk";

async function initMiniApp() {
  const context = await sdk.context;

  // context.user contains the viewing user's FID, username, pfpUrl
  console.log(`User FID: ${context.user.fid}`);
  console.log(`Username: ${context.user.username}`);

  // Signal to the client that the app is ready to render
  sdk.actions.ready();
}

initMiniApp();

SDK Actions

// Open an external URL in the client's browser
sdk.actions.openUrl("https://example.com");

// Close the Mini App
sdk.actions.close();

// Compose a cast with prefilled text
sdk.actions.composeCast({
  text: "Check out this Mini App!",
  embeds: ["https://example.com/app"],
});

// Add a Mini App to the user's favorites (prompts confirmation)
sdk.actions.addFrame();

Transaction Frames

Mini Apps can trigger onchain transactions through the embedded wallet provider. The SDK exposes an EIP-1193 provider that connects to the user's wallet in the Farcaster client.

Wallet Provider Setup

import sdk from "@farcaster/frame-sdk";
import { createWalletClient, custom, parseEther, type Address } from "viem";
import { base } from "viem/chains";

async function sendTransaction() {
  const context = await sdk.context;

  const provider = sdk.wallet.ethProvider;

  const walletClient = createWalletClient({
    chain: base,
    transport: custom(provider),
  });

  const [address] = await walletClient.requestAddresses();

  const hash = await walletClient.sendTransaction({
    account: address,
    to: "0xRecipient..." as Address,
    value: parseEther("0.001"),
  });

  return hash;
}

With Wagmi Connector

For apps using wagmi, wrap the SDK's provider as a connector:

import sdk from "@farcaster/frame-sdk";
import { createConfig, http, useConnect, useSendTransaction } from "wagmi";
import { base } from "wagmi/chains";
import { farcasterFrame } from "@farcaster/frame-wagmi-connector";

const config = createConfig({
  chains: [base],
  transports: {
    [base.id]: http(),
  },
  connectors: [farcasterFrame()],
});

// In your React component:
function MintButton() {
  const { connect, connectors } = useConnect();
  const { sendTransaction } = useSendTransaction();

  async function handleMint() {
    connect({ connector: connectors[0] });
    sendTransaction({
      to: "0xNFTContract..." as `0x${string}`,
      data: "0x...", // mint function calldata
      value: parseEther("0.01"),
    });
  }

  return <button onClick={handleMint}>Mint</button>;
}

Warpcast Deep Links and Cast Intents

Cast Intent URL

Open Warpcast's compose screen with prefilled content:

https://warpcast.com/~/compose?text=Hello%20Farcaster&embeds[]=https://example.com
ParameterDescription
textURL-encoded cast text
embeds[]Up to 2 embed URLs
channelKeyChannel to post in (e.g., farcaster)

Deep Links

# Open a user's profile
https://warpcast.com/<username>

# Open a specific cast
https://warpcast.com/<username>/<cast-hash>

# Open a channel
https://warpcast.com/~/channel/<channel-id>

# Open direct cast composer
https://warpcast.com/~/inbox/create/<fid>

Channels

Channels are topic-based feeds identified by a parent_url. A cast is posted to a channel by setting its parent_url to the channel's URL.

// Post a cast to the "ethereum" channel
const response = await neynar.publishCast({
  signerUuid: process.env.SIGNER_UUID,
  text: "Pectra upgrade is live!",
  channelId: "ethereum",
});

Channel Lookup

const channel = await neynar.lookupChannel({ id: "farcaster" });
console.log(channel.channel.name, channel.channel.follower_count);

Channel Feed

const feed = await neynar.fetchFeed({
  feedType: "filter",
  filterType: "channel_id",
  channelId: "ethereum",
  limit: 25,
});

Neynar API Pricing

Current as of March 2026

PlanMonthly CreditsPriceWebhooksRate Limit
Free100K$015 req/s
Starter1M$49/mo520 req/s
Growth10M$249/mo2550 req/s
Scale60M$899/mo100200 req/s
EnterpriseCustomCustomUnlimitedCustom

Credit costs vary by endpoint. Read operations (user lookup, feed) cost 1-5 credits. Write operations (publish cast, react) cost 10-50 credits. Webhook deliveries are free but count against webhook limits.

Hub / Snapchain Endpoints

Direct hub access for reading raw Farcaster data without the Neynar API abstraction.

ProviderEndpointAuth
Neynar Hub APIhub-api.neynar.comAPI key in x-api-key header
Self-hosted Hublocalhost:2283None (local)

Hub HTTP API Examples

# Get casts by FID
curl -H "x-api-key: $NEYNAR_API_KEY" \
  "https://hub-api.neynar.com/v1/castsByFid?fid=3&pageSize=10"

# Get user data (display name, bio, pfp)
curl -H "x-api-key: $NEYNAR_API_KEY" \
  "https://hub-api.neynar.com/v1/userDataByFid?fid=3"

# Get reactions by FID
curl -H "x-api-key: $NEYNAR_API_KEY" \
  "https://hub-api.neynar.com/v1/reactionsByFid?fid=3&reactionType=1"

Hub gRPC API

# Install hubble CLI
npm install -g @farcaster/hubble

# Query via gRPC
hubble --insecure -r hub-api.neynar.com:2283 getCastsByFid --fid 3

Farcaster Epoch Conversion

// Farcaster epoch: January 1, 2021 00:00:00 UTC
const FARCASTER_EPOCH = 1609459200;

function farcasterTimestampToUnix(farcasterTs: number): number {
  return farcasterTs + FARCASTER_EPOCH;
}

function unixToFarcasterTimestamp(unixTs: number): number {
  return unixTs - FARCASTER_EPOCH;
}

function farcasterTimestampToDate(farcasterTs: number): Date {
  return new Date((farcasterTs + FARCASTER_EPOCH) * 1000);
}

Related Skills

  • viem -- Used for onchain interactions with Farcaster registry contracts on OP Mainnet and for building transaction frames with the wallet provider
  • wagmi -- React hooks for wallet connection in Mini Apps via the @farcaster/frame-wagmi-connector
  • x402 -- Payment protocol that can be integrated with Farcaster Mini Apps for paywalled content

References

AI Skill Finder

Ask me what skills you need

What are you building?

Tell me what you're working on and I'll find the best agent skills for you.