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- Protocol Events: On-chain activity (swaps, transfers, deposits, etc.)
- Adapter: Your code that interprets these events
- Enriched Data: Standardized output (positions or actions)
- Sinks: Where data goes (CSV files, stdout, Absinthe API)
- Points: Final reward calculations based on your data
Key Concept: Trackables
A trackable is the fundamental unit of tracking. It defines:
| Property | Description | Example |
|---|---|---|
| kind | What type of data | action (one-time events) or position (ongoing balances) |
| quantityType | How to measure | token_based, count, or none |
| params | Required identifiers | poolAddress, contractAddress |
| assetSelectors | Optional asset filters | swapLegAddress (which token to price) |
| requiredPricer | Custom pricing logic | univ2nav, univ3lp |
When to Use Each Kind
| Kind | Use When | Examples |
|---|---|---|
| action | One-time events | Swaps, claims, votes, bridges |
| position | Ongoing balances that change over time | LP tokens, staked assets, holdings |
When to Use Each quantityType
| quantityType | Use When | Examples |
|---|---|---|
| token_based | Tracking fungible asset amounts | Token holdings, swap volume, LP positions |
| count | Counting occurrences | Number of votes, number of claims |
| none | Just recording that something happened | Identity 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/ # DocumentationAdapter 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
| Field | Required | Description |
|---|---|---|
chainArch | ✅ | Always "evm" for Ethereum-compatible chains |
flushInterval | ✅ | How often to write data (e.g., "1h", "48h") |
redisUrl | ✅ | Redis connection string (use ${env:REDIS_URL} for env var) |
sinkConfig | ✅ | Where to output data (CSV, stdout, absinthe API) |
network.chainId | ✅ | Numeric chain ID (1 for Ethereum, 43111 for Hemi, etc.) |
network.gatewayUrl | ❌ | Subsquid archive URL (speeds up historical data) |
network.rpcUrl | ✅ | Your RPC endpoint |
network.finality | ❌ | Block confirmations to wait (default: 75) |
range.fromBlock | ✅ | Starting block number |
range.toBlock | ❌ | Ending block (omit for continuous indexing) |
adapterConfig.adapterId | ✅ | Which adapter to use |
adapterConfig.config | ✅ | Trackable instances with params and pricing |
Pricing Configuration
Pricing tells the adapter how to convert raw token amounts to USD values.
Available Pricing Kinds
| Kind | Use Case | Example |
|---|---|---|
pegged | Stable value | { "kind": "pegged", "usdPegValue": 1 } |
coingecko | Market price | { "kind": "coingecko", "id": "ethereum" } |
codex | Codex API | { "kind": "codex", "address": "0x..." } |
univ2nav | UniV2 LP NAV | Requires token0 and token1 sub-feeds |
univ3lp | UniV3 LP NAV | Requires token0 and token1 sub-feeds |
morphov1vaults | Morpho V1 | Requires underlyingAsset sub-feed |
morphov2vaults | Morpho V2 | Requires underlyingAsset sub-feed |
morphomarkets | Morpho Markets | Requires 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-protocolStep 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:
- Add new ABIs to
adapters/my-protocol/abi/ - Generate TypeScript types with
squid-evm-typegen - Update
buildSqdProcessorto listen for your events - Update
onLogto 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.csvCommon 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
| Function | Kind | Use For |
|---|---|---|
emitFns.position.balanceDelta() | position | Balance changes |
emitFns.position.reprice() | position | Trigger repricing |
emitFns.action.swap() | action | Swap events |
emitFns.action.action() | action | Generic 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 holdinglp— Liquidity provisionstake— Stakingswap— Tradingclaim— Claiming rewards
Next Steps
- Learn from Existing Adapters — Deep dive into existing adapters
- Testing Guide — Learn how to validate your output
- Create Config & Deploy — Generate config and deploy manually
- Ask Questions — Join our Absinthe Community Slack