Listening to onchain events - Paragraph
Documentation Index
Fetch the complete documentation index at: /docs/llms.txt
Use this file to discover all available pages before exploring further.
This page is for integrators who want to listen to on-chain events to discover and track Paragraph coins. Today, Paragraph coins are created via Doppler. Paragraph currently deploys on Doppler v3 and is migrating to Doppler v4. You’ll primarily listen to Airlock (factory) events, then follow the price-discovery surface (Uniswap v3/v2 for v3, the Doppler Hook for v4).
Note: We are building this functionality into our SDK to make coin interactions easier.
Overview
Paragraph coins are deployed via Doppler. To integrate with these coins, you have several options:
- Listen to the onchain events
- Query the Doppler indexer directly via GraphQL
- [coming soon] Use the SDK to watch the onchain events
- [coming soon] Listen to our own contracts
The remainder of this document focuses on listening to the onchain events, as it’s the most complex.
TL;DR
Subscribe to Airlock.Create(v3 & v4) on Base.- Verify it’s a Paragraph coin:
Airlock.getAssetData(asset).integrator === PARAGRAPH_INTEGRATOR[chainId]. - Persist
asset(ERC-20),numeraire,initializer, andpoolOrHook. - If v3:
poolOrHookis a Uniswap v3 pool → track pool events - If v4:
poolOrHookis the Doppler Hook → trackSwap,Rebalance,EarlyExit,InsufficientProceeds. - (Optional) Track
Airlock.Collectfor protocol/integrator fee withdrawals.
Supported networks & addresses
Use the Doppler SDK address maps to resolve canonical contracts:
// v3 (current)
import { DOPPLER_V3_ADDRESSES } from 'doppler-v3-sdk'
// v4 (migration target)
import { DOPPLER_V4_ADDRESSES } from 'doppler-v4-sdk'
Paragraph integrator (for verification via getAssetData(asset).integrator):
- Base Mainnet (8453):
0x805F3d7A8F8E50d8A5a444e8231BBfb5F97d5196 - Base Sepolia (84532):
0x5902f7E444EAc22BE910B999DDD14cafc9d9D079(Historic test you may see in old mainnet data: 0x797B89c60E561B5ff345D92E75344A106De4e531)
WETH on Base (common numeraire):0x4200000000000000000000000000000000000006
What to listen to
Airlock (common to v3 & v4)
event Create(
address asset, // ERC-20 address of the coin
address indexed numeraire, // paired token (e.g., WETH)
address initializer, // v3 or v4 initializer contract
address poolOrHook // v3: Uniswap v3 pool; v4: Doppler Hook
);
event Collect(address indexed to, address indexed token, uint256 amount); // fee withdrawal
// Public getter used after Create to verify Paragraph integrator, etc.
function getAssetData(address asset) view
returns (
address numeraire,
address timelock,
address governance,
address liquidityMigrator,
address poolInitializer,
address pool,
address migrationPool,
uint256 numTokensToSell,
uint256 totalSupply,
address integrator // ← check this equals PARAGRAPH_INTEGRATOR
);
Identify Paragraph coins Primary: getAssetData(asset).integrator === PARAGRAPH_INTEGRATOR[chainId] Secondary: DERC20(asset).tokenURI() points to https://api.paragraph.com/coins/metadata/<id>.
Price-discovery surface (differs by version)
v3 (Uniswap v3) - From
Create,poolOrHookis the Uniswap v3 pool.- Track pool events:
Swap,Mint,Burn. - Note: Paragraph uses a no-op migrator (
0x6ddfed58d238ca3195e49d8ac3d4cea6386e5c33), so no migration occurs.
- Track pool events:
v4 (Uniswap v4 + Doppler Hook)
From
Create,poolOrHookis the Doppler Hook. Track these hook events:
event Swap(int24 currentTick, uint256 totalProceeds, uint256 totalTokensSold);
event Rebalance(int24 currentTick, int24 tickLower, int24 tickUpper, uint256 epoch);
event EarlyExit(uint256 epoch); // cap reached; sale ends early
event InsufficientProceeds(); // refund/buyback phase at avg clearing price
- Progress / price: normalize decimals and approximate
avgPrice (numeraire per asset) ≈ totalProceeds / totalTokensSold
Quickstart (indexer flow)
- Subscribe to
Airlock.Create(v3 & v4) - For each chain, subscribe to
Airlock.Createon both the v3 and v4 Airlock addresses (from the SDK maps). - Verify Paragraph coin
- Read
getAssetData(asset).integratorand compare to the Paragraph integrator address for the chain. - Persist core pointers
- Store
asset(ERC-20),numeraire,initializer, andpoolOrHook. These are your canonical references. - Route by version
- If
initializer === DOPPLER_V3_ADDRESSES[chainId].lockableV3Initializer→ v3. Otherwise treat as v4. - Track discovery surface
- v3: follow Uniswap v3 pool events
- v4: subscribe to the Doppler Hook’s
Swap,Rebalance,EarlyExit,InsufficientProceeds. - (Optional) Fees
- Listen for
Airlock.Collectif you want to surface protocol/integrator fee withdrawals.
Minimal watcher (TypeScript + viem)
import { createPublicClient, http, parseAbi, decodeEventLog } from 'viem'
import { base } from 'viem/chains'
import { DOPPLER_V3_ADDRESSES } from 'doppler-v3-sdk'
import { DOPPLER_V4_ADDRESSES } from 'doppler-v4-sdk'
const PARAGRAPH_INTEGRATOR: Record<number, `0x${string}`> = {
8453: '0x805F3d7A8F8E50d8A5a444e8231BBfb5F97d5196', // Base mainnet
84532: '0x5902f7E444EAc22BE910B999DDD14cafc9d9D079', // Base Sepolia
}
const airlockAbi = parseAbi([
'event Create(address asset, address indexed numeraire, address initializer, address poolOrHook)',
'event Migrate(address indexed asset, address indexed pool)',
'function getAssetData(address asset) view returns (address,address,address,address,address,address,address,uint256,uint256,address)',
])
const erc20Abi = parseAbi([
'function name() view returns (string)',
'function symbol() view returns (string)',
'function decimals() view returns (uint8)',
'function tokenURI() view returns (string)',
])
const dopplerHookAbi = parseAbi([
'event Swap(int24 currentTick, uint256 totalProceeds, uint256 totalTokensSold)',
'event Rebalance(int24 currentTick, int24 tickLower, int24 tickUpper, uint256 epoch)',
'event EarlyExit(uint256 epoch)',
'event InsufficientProceeds()',
])
const client = createPublicClient({ chain: base, transport: http() })
// Index both v3 + v4 Airlocks for Base; add other chains similarly
const AIRLOCKS = [
DOPPLER_V3_ADDRESSES[base.id].airlock,
DOPPLER_V4_ADDRESSES[base.id].airlock,
] as const
async function onCreate(log: any) {
const { args } = decodeEventLog({
abi: airlockAbi,
data: log.data,
topics: log.topics
}) as any
const asset = args.asset as `0x${string}`
const numeraire = args.numeraire as `0x${string}`
const initializer = args.initializer as `0x${string}`
const poolOrHook = args.poolOrHook as `0x${string}`
// Verify Paragraph coin
const assetData = await client.readContract({
address: log.address as `0x${string}`,
abi: airlockAbi,
functionName: 'getAssetData',
args: [asset],
})
const integrator = assetData[9] as `0x${string}`
if (integrator.toLowerCase() !== PARAGRAPH_INTEGRATOR[base.id].toLowerCase()) return
// Optional: fetch token metadata
const [name, symbol, decimals, tokenURI] = await Promise.all([
client.readContract({ address: asset, abi: erc20Abi, functionName: 'name' }),
client.readContract({ address: asset, abi: erc20Abi, functionName: 'symbol' }),
client.readContract({ address: asset, abi: erc20Abi, functionName: 'decimals' }),
client.readContract({ address: asset, abi: erc20Abi, functionName: 'tokenURI' }).catch(() => ''),
])
// Persist pointers
console.log({
asset, numeraire, initializer, poolOrHook,
name, symbol, decimals, tokenURI
})
// Route by version
const v3Initializer = DOPPLER_V3_ADDRESSES[base.id].lockableV3Initializer
const isV3 = initializer.toLowerCase() === v3Initializer.toLowerCase()
if (isV3) {
// V3: poolOrHook is Uniswap V3 pool — track Uniswap events here
// e.g., subscribe to the pool's Swap/Mint/Burn with its ABI
return
}
// V4: poolOrHook is the Doppler Hook — subscribe to sale lifecycle
client.watchEvent({
address: poolOrHook,
abi: dopplerHookAbi,
eventName: 'Swap',
onLogs: (logs) => {
for (const l of logs) {
const { currentTick, totalProceeds, totalTokensSold } = l.args as any
console.log('Hook Swap', { currentTick, totalProceeds, totalTokensSold })
// avgPrice ≈ totalProceeds / totalTokensSold (normalize decimals)
}
}
})
}
export async function start() {
for (const airlock of AIRLOCKS) {
client.watchEvent({
address: airlock,
abi: airlockAbi,
eventName: 'Create',
onLogs: (logs) => logs.forEach(onCreate),
})
}
}
start()
Reorg safety: range-scan historical logs and delay finalization by ~10–20 blocks before marking records as confirmed.
Event glossary (quick reference)
Airlock.Create→ canonical birth of a coin. Source of truth for:
asset(ERC-20),numeraire,initializer,poolOrHook(v3 pool or v4 hook).Doppler Hook (v4):
Swap→ updates cumulativetotalProceeds&totalTokensSold.Rebalance→ epoch step; new slug placement/range.EarlyExit→ cap reached; sale ends early.InsufficientProceeds→ refund/buyback phase at average clearing price.
Airlock.Collect → fee withdrawals by protocol/integrator.
Practical tips
- Paragraph detection (recommended):
getAssetData(asset).integrator == PARAGRAPH_INTEGRATOR[chainId]DERC20.tokenURI()starts withhttps://api.paragraph.com/coins/metadata/…
- Version detection: compare
initializerfromCreateagainst known v3/v4 initializer addresses from the SDK maps. - Decimals: normalize both the asset and the numeraire for any price math.
- State reads: you can always query Doppler contracts directly (e.g., v4 hook public state) to reconstruct state between events.
What changes with v4?
Airlock.Createremains your discovery source, butpoolOrHooknow points to a hook (not a V3 pool).