Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Getting Started: Build Your Own Adapter (Clean Fork) – Absinthe Docs
Skip to content

Getting Started: Build Your Own Adapter (Clean Fork)

This guide helps you understand the Absinthe adapter architecture so you can safely modify existing adapters or create new ones for your protocol.

Mental Model: How Absinthe Works

The Data Flow

Protocol Events → Adapter → Enriched Events/Positions → CSV/API → Points Issuance
  1. Protocol Events: On-chain activity (swaps, transfers, deposits, etc.)
  2. Adapter: Your code that interprets these events
  3. Enriched Data: Standardized output (positions or actions)
  4. Sinks: Where data goes (CSV files, stdout, Absinthe API)
  5. Points: Final reward calculations based on your data

Key Concept: Trackables

A trackable is the fundamental unit of tracking. It defines:

PropertyDescriptionExample
kindWhat type of dataaction (one-time events) or position (ongoing balances)
quantityTypeHow to measuretoken_based, count, or none
paramsRequired identifierspoolAddress, contractAddress
assetSelectorsOptional asset filtersswapLegAddress (which token to price)
requiredPricerCustom pricing logicuniv2nav, univ3lp

When to Use Each Kind

KindUse WhenExamples
actionOne-time eventsSwaps, claims, votes, bridges
positionOngoing balances that change over timeLP tokens, staked assets, holdings

When to Use Each quantityType

quantityTypeUse WhenExamples
token_basedTracking fungible asset amountsToken holdings, swap volume, LP positions
countCounting occurrencesNumber of votes, number of claims
noneJust recording that something happenedIdentity verification, contract deployment

Repository Structure

absinthe-adapters/
├── adapters/                    # All adapter implementations
│   ├── _shared/                 # Shared utilities for adapters
│   │   └── index.ts            # Re-exports common utilities
│   ├── _template/              # Template for new adapters
│   │   ├── index.ts           # Main adapter file
│   │   ├── metadata.ts        # Display info
│   │   └── tests/config/      # Example configs
│   ├── erc20-holdings/         # Simple ERC20 tracking
│   ├── uniswap-v2/             # DEX with swaps + LP
│   ├── uniswap-v3/             # DEX with NFT positions
│   ├── morphomarkets/          # Lending markets
│   ├── morphov1vaults/         # Vault v1
│   └── morphov2vaults/         # Vault v2
├── src/
│   ├── types/                  # Core type definitions
│   │   ├── manifest.ts        # Manifest types (evmAddress, etc.)
│   │   └── adapter.ts         # Adapter interfaces
│   ├── config/                 # Configuration schemas
│   │   └── schema.ts          # JSON config validation
│   └── feeds/                  # Pricing feeds
└── docs/                       # Documentation

Adapter Anatomy

Every adapter consists of three files:

1. index.ts — Main Adapter Logic

import { defineAdapter } from '../_shared/index.ts';
import { manifest } from './manifest.ts'; // or inline
import { metadata } from './metadata.ts';
 
export default defineAdapter({
  manifest, // Trackable definitions
  metadata, // Display info
  build: ({ config }) => ({
    buildSqdProcessor: (base) => {
      /* What events to listen for */
    },
    onLog: async ({ log, emitFns, redis, sqdRpcCtx }) => {
      /* Process events */
    },
  }),
});

2. manifest.ts — Trackable Definitions

import { Manifest, evmAddress } from '../_shared/index.ts';
 
export const manifest = {
  name: 'my-protocol',
  version: '0.0.1',
  chainArch: 'evm',
  trackables: {
    myTrackable: {
      kind: 'position', // or 'action'
      quantityType: 'token_based', // or 'count', 'none'
      params: {
        contractAddress: evmAddress('The contract to track'),
      },
      // assetSelectors: { ... }  // Optional
      // requiredPricer: myFeed,  // Optional
    },
  },
} as const satisfies Manifest;

3. metadata.ts — Display Information

import { AdapterMetadata } from '../_shared/index.ts';
 
export const metadata = {
  displayName: 'My Protocol',
  description: 'Tracks holdings in My Protocol',
  category: 'defi',
  tags: ['my-protocol', 'evm'],
  author: 'Your Team',
  authorUrl: 'https://yourprotocol.com',
  status: 'beta',
  createdAt: '2025-01-19',
} as const satisfies AdapterMetadata;

Configuration File Structure

Every adapter run requires a .absinthe.json config file:

{
  "chainArch": "evm",
  "flushInterval": "1h",
  "redisUrl": "${env:REDIS_URL}",
 
  "sinkConfig": {
    "sinks": [{ "sinkType": "csv", "path": "output.csv" }, { "sinkType": "stdout" }]
  },
 
  "network": {
    "chainId": 1,
    "gatewayUrl": "https://v2.archive.subsquid.io/network/ethereum-mainnet",
    "rpcUrl": "https://your-rpc-url",
    "finality": 75
  },
 
  "range": {
    "fromBlock": 18000000,
    "toBlock": 18100000
  },
 
  "adapterConfig": {
    "adapterId": "my-adapter-name",
    "config": {
      "myTrackable": [
        {
          "params": {
            "contractAddress": "0x..."
          },
          "pricing": {
            "kind": "pegged",
            "usdPegValue": 1
          }
        }
      ]
    }
  }
}

Config Fields Explained

FieldRequiredDescription
chainArchAlways "evm" for Ethereum-compatible chains
flushIntervalHow often to write data (e.g., "1h", "48h")
redisUrlRedis connection string (use ${env:REDIS_URL} for env var)
sinkConfigWhere to output data (CSV, stdout, absinthe API)
network.chainIdNumeric chain ID (1 for Ethereum, 43111 for Hemi, etc.)
network.gatewayUrlSubsquid archive URL (speeds up historical data)
network.rpcUrlYour RPC endpoint
network.finalityBlock confirmations to wait (default: 75)
range.fromBlockStarting block number
range.toBlockEnding block (omit for continuous indexing)
adapterConfig.adapterIdWhich adapter to use
adapterConfig.configTrackable instances with params and pricing

Pricing Configuration

Pricing tells the adapter how to convert raw token amounts to USD values.

Available Pricing Kinds

KindUse CaseExample
peggedStable value{ "kind": "pegged", "usdPegValue": 1 }
coingeckoMarket price{ "kind": "coingecko", "id": "ethereum" }
codexCodex API{ "kind": "codex", "address": "0x..." }
univ2navUniV2 LP NAVRequires token0 and token1 sub-feeds
univ3lpUniV3 LP NAVRequires token0 and token1 sub-feeds
morphov1vaultsMorpho V1Requires underlyingAsset sub-feed
morphov2vaultsMorpho V2Requires underlyingAsset sub-feed
morphomarketsMorpho MarketsRequires underlyingAsset sub-feed

Pricing Structure

{
  "pricing": {
    "assetType": "erc20",
    "priceFeed": {
      "kind": "pegged",
      "usdPegValue": 100
    }
  }
}

For composite pricing (LP tokens):

{
  "pricing": {
    "assetType": "erc20",
    "priceFeed": {
      "kind": "univ2nav",
      "token0": {
        "assetType": "erc20",
        "priceFeed": { "kind": "coingecko", "id": "ethereum" }
      },
      "token1": {
        "assetType": "erc20",
        "priceFeed": { "kind": "pegged", "usdPegValue": 1 }
      }
    }
  }
}

How to Fork/Modify an Adapter

Step 1: Copy an Existing Adapter
Terminal
cp -r adapters/uniswap-v2 adapters/my-protocol
Step 2: Update Manifest

Edit adapters/my-protocol/index.ts:

export const manifest = {
  name: 'my-protocol', // Change this
  version: '0.0.1',
  chainArch: 'evm',
  trackables: {
    // Modify trackables as needed
  },
} as const satisfies Manifest;
Step 3: Update Metadata

Edit adapters/my-protocol/metadata.ts:

export const metadata = {
  displayName: 'My Protocol',
  description: 'Your description here',
  // ...
};
Step 4: Modify Event Handling

If your protocol uses different events:

  1. Add new ABIs to adapters/my-protocol/abi/
  2. Generate TypeScript types with squid-evm-typegen
  3. Update buildSqdProcessor to listen for your events
  4. Update onLog to handle your events
Step 5: Create Test Config

Create adapters/my-protocol/tests/config/my-protocol.absinthe.json:

{
  "chainArch": "evm",
  "flushInterval": "1h",
  "redisUrl": "${env:REDIS_URL}",
  "sinkConfig": { "sinks": [{ "sinkType": "csv", "path": "test.csv" }] },
  "network": { "chainId": 1, "rpcUrl": "https://your-rpc" },
  "range": { "fromBlock": 1000000, "toBlock": 1001000 },
  "adapterConfig": {
    "adapterId": "my-protocol",
    "config": { }
  }
}
Step 6: Test Your Adapter
Terminal
# Run the adapter
pnpm run dev -- --config adapters/my-protocol/tests/config/my-protocol.absinthe.json
 
# Check the output CSV
cat test.csv

Common Patterns

Pattern 1: Caching Immutable Data

Use Redis to cache data that never changes (token addresses, decimals):

const cacheKey = `myprotocol:${contractAddr}:token0`;
let token0 = await redis.get(cacheKey);
 
if (!token0) {
  const contract = new MyAbi.Contract(sqdRpcCtx, contractAddr);
  token0 = await contract.token0();
  await redis.set(cacheKey, token0);
}

Pattern 2: Routing by Event Type

onLog: async ({ log, emitFns }) => {
  if (log.topic0 === TransferTopic) {
    await handleTransfer(log, emitFns);
  }
  if (log.topic0 === SwapTopic) {
    await handleSwap(log, emitFns);
  }
};

Pattern 3: Fan-Out to Multiple Instances

When one event should update multiple trackable instances:

const instances = config.myTrackable.filter((i) => i.params.contractAddress === log.address);
 
for (const instance of instances) {
  await handleEvent(log, emitFns, instance);
}

Pattern 4: Balance Delta for Positions

await emitFns.position.balanceDelta({
  user: userAddress,
  asset: { type: 'erc20', address: tokenAddress },
  amount: deltaAmount, // positive for add, negative for remove
  activity: 'hold', // or 'lp', 'stake'
  trackableInstance: instance,
});

Pattern 5: Action Emit for Events

await emitFns.action.swap({
  key: md5Hash(`${log.txRef}${log.index}`), // Unique key for deduplication
  user: userAddress,
  asset: { type: 'erc20', address: tokenAddress },
  amount: swapAmount,
  activity: 'swap',
  trackableInstance: instance,
});

Quick Reference

Emit Functions

FunctionKindUse For
emitFns.position.balanceDelta()positionBalance changes
emitFns.position.reprice()positionTrigger repricing
emitFns.action.swap()actionSwap events
emitFns.action.action()actionGeneric actions

Asset Types

// ERC20
{ type: 'erc20', address: '0x...' }
 
// ERC721 (NFT)
{ type: 'erc721', address: '0x...', tokenId: '123' }
 
// Custom
{ type: 'custom', prefix: 'morpho', key: 'market-id-supply' }

Activity Types

  • hold — Passive holding
  • lp — Liquidity provision
  • stake — Staking
  • swap — Trading
  • claim — Claiming rewards

Next Steps

  1. Learn from Existing Adapters — Deep dive into existing adapters
  2. Testing Guide — Learn how to validate your output
  3. Create Config & Deploy — Generate config and deploy manually
  4. Ask Questions — Join our Absinthe Community Slack