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:

The remainder of this document focuses on listening to the onchain events, as it’s the most complex.

TL;DR

  1. Subscribe to Airlock.Create (v3 & v4) on Base.
  2. Verify it’s a Paragraph coin: Airlock.getAssetData(asset).integrator === PARAGRAPH_INTEGRATOR[chainId].
  3. Persist asset (ERC-20), numeraire, initializer, and poolOrHook.
  4. If v3: poolOrHook is a Uniswap v3 pool → track pool events
  5. If v4: poolOrHook is the Doppler Hook → track Swap, Rebalance, EarlyExit, InsufficientProceeds.
  6. (Optional) Track Airlock.Collect for 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):

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)

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

Quickstart (indexer flow)

  1. Subscribe to Airlock.Create (v3 & v4)
  2. For each chain, subscribe to Airlock.Create on both the v3 and v4 Airlock addresses (from the SDK maps).
  3. Verify Paragraph coin
  4. Read getAssetData(asset).integrator and compare to the Paragraph integrator address for the chain.
  5. Persist core pointers
  6. Store asset (ERC-20), numeraire, initializer, and poolOrHook. These are your canonical references.
  7. Route by version
  8. If initializer === DOPPLER_V3_ADDRESSES[chainId].lockableV3Initializer → v3. Otherwise treat as v4.
  9. Track discovery surface
  10. v3: follow Uniswap v3 pool events
  11. v4: subscribe to the Doppler Hook’s Swap, Rebalance, EarlyExit, InsufficientProceeds.
  12. (Optional) Fees
  13. Listen for Airlock.Collect if 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)


Practical tips

  1. getAssetData(asset).integrator == PARAGRAPH_INTEGRATOR[chainId]
  2. DERC20.tokenURI() starts with https://api.paragraph.com/coins/metadata/…

What changes with v4?