Seshat

Documentation

Fee Scanner

Fee Intelligence

The fee scanner is Seshat's core data pipeline. It tracks every Clanker-created token on Base, reads unclaimed creator fee balances every 10 minutes, and detects the moment a first-ever claim is made.

Cadence: every 10 minReal-time via Alchemy Webhooks

Factory contracts

Clanker has deployed five factory versions on Base. Each version has a slightly different ABI for reading fee state.

// Clanker factory addresses (Base mainnet)
const FACTORIES = {
  V0: "0x250c9FB2",   // earliest, creatorReward() not exposed
  V1: "0x9B84fcE5",   // unclaimedFees / totalFeesClaimed / feeRecipient
  V2: "0x732560fa",   // creatorReward / claimedFees / creator
  V3: "0x375C15db",   // same ABI as V2
  V4: "0xE85A59c6",   // FeeLocker pattern
} as const;

// V4 fee locker — emits ClaimTokens / ClaimTokensPermissioned
const FEE_LOCKER_V4 = "0xF3622742";

ABI fallback strategy

Rather than tracking which factory minted each token, the scanner tries the V2 ABI first (most common), then falls back to V1 if the call reverts. This covers all historical tokens in a single pass.

// baseChain.ts
async function getCreatorReward(lpAddress: string): Promise<bigint> {
  try {
    // V2 / V3: creatorReward(address) → uint256
    return await publicClient.readContract({
      address: lpAddress as `0x${string}`,
      abi: CLANKER_V2_ABI,
      functionName: "creatorReward",
    });
  } catch {
    // V1: unclaimedFees() → uint256
    return await publicClient.readContract({
      address: lpAddress as `0x${string}`,
      abi: CLANKER_V1_ABI,
      functionName: "unclaimedFees",
    });
  }
}

Multicall batching

Reading 2,800+ contracts individually would require 2,800+ RPC calls per scan cycle. Instead, the scanner uses Uniswap V3's Multicall3 contract to batch 20 reads per RPC call — reducing 2,800 calls to 140.

// 20 tokens per multicall batch
const BATCH_SIZE = 20;

async function batchGetCreatorRewards(
  lpAddresses: string[]
): Promise<Map<string, bigint>> {
  const results = new Map<string, bigint>();

  for (let i = 0; i < lpAddresses.length; i += BATCH_SIZE) {
    const batch = lpAddresses.slice(i, i + BATCH_SIZE);
    const calls = batch.map(addr => ({
      address: addr as `0x${string}`,
      abi: CLANKER_V2_ABI,
      functionName: "creatorReward",
    }));

    const raw = await publicClient.multicall({ contracts: calls, allowFailure: true });

    for (let j = 0; j < batch.length; j++) {
      const res = raw[j];
      if (res.status === "success") {
        results.set(batch[j], res.result as bigint);
      } else {
        // V2 failed — try V1 individually
        const v1 = await getCreatorRewardV1(batch[j]);
        results.set(batch[j], v1);
      }
    }
  }
  return results;
}

Real-time notifications (Alchemy Webhooks)

The 10-minute polling cadence is the fallback. For first-ever fee claims, Seshat uses Alchemy's Notify API to deliver real-time webhook callbacks — typically within 1 second of the transaction being confirmed on Base.

This works without any polling and uses zero Claude tokens. The webhook handler is a plain Cloudflare Worker endpoint that verifies the Alchemy HMAC signature, parses the event, and dispatches the alert.

// POST /api/webhook/alchemy
export async function handleAlchemyWebhook(
  request: Request,
  env: Env
): Promise<Response> {
  // 1. Verify HMAC-SHA256 signature
  const sig     = request.headers.get("x-alchemy-signature") ?? "";
  const body    = await request.text();
  const key     = await crypto.subtle.importKey(
    "raw", new TextEncoder().encode(env.ALCHEMY_WEBHOOK_SIGNING_KEY),
    { name: "HMAC", hash: "SHA-256" }, false, ["verify"]
  );
  const valid = await crypto.subtle.verify(
    "HMAC", key,
    hexToBytes(sig),
    new TextEncoder().encode(body)
  );
  if (!valid) return new Response("Unauthorized", { status: 401 });

  // 2. Parse the activity logs
  const payload = JSON.parse(body);
  const logs    = payload?.event?.data?.block?.logs ?? [];

  for (const log of logs) {
    const topic0 = log.topics[0];

    // ClaimTokens (FeeLocker V4)
    if (topic0 === CLAIM_TOKENS_TOPIC) {
      await handleFirstEverClaim(log, env);
    }
    // Legacy: Uniswap V3 Collect event
    if (topic0 === COLLECT_TOPIC) {
      await handleLegacyClaim(log, env);
    }
  }
  return new Response("ok");
}

Setting up the Alchemy webhook

In your Alchemy dashboard, create a Custom Webhook with the following GraphQL filter. This fires for every ClaimTokens event on the FeeLocker contract with zero polling overhead.

{
  block {
    logs(
      filter: {
        addresses: ["0xF3622742..."],
        topics: ["0x...ClaimTokens_topic_hash"]
      }
    ) {
      account { address }
      topics
      data
      transaction { hash }
    }
  }
}

First-ever claim detection

A first-ever claim is particularly significant: it means a creator just discovered (or was alerted to) their unclaimed fees for the first time. Seshat detects this by checking thefee_claim_eventstable — if no prior claim exists for an LP contract, the current claim is flagged as first-ever and a priority alert is sent to all subscribers tracking that address.

async function isFirstEverClaim(lpAddress: string, db: D1Database): Promise<boolean> {
  const existing = await db
    .prepare("SELECT id FROM fee_claim_events WHERE lp_address = ? LIMIT 1")
    .bind(lpAddress)
    .first();
  return existing === null;
}

Database schema

-- tokens: one row per Clanker token
CREATE TABLE tokens (
  id              TEXT PRIMARY KEY,  -- token address
  symbol          TEXT NOT NULL,
  name            TEXT,
  lp_address      TEXT NOT NULL,     -- Uniswap V3 LP contract
  factory_version TEXT NOT NULL,     -- V0 | V1 | V2 | V3 | V4
  creator_address TEXT,
  created_at      INTEGER NOT NULL,
  last_scanned_at INTEGER
);

-- fee_snapshots: current unclaimed fee balance
CREATE TABLE fee_snapshots (
  token_id           TEXT NOT NULL,
  unclaimed_eth      TEXT NOT NULL,   -- stored as string (bigint)
  claimed_eth        TEXT NOT NULL,
  fee_usd            REAL,
  scanned_at         INTEGER NOT NULL,
  PRIMARY KEY (token_id, scanned_at)
);

-- fee_claim_events: on-chain claim log
CREATE TABLE fee_claim_events (
  id                TEXT PRIMARY KEY,
  lp_address        TEXT NOT NULL,
  token_address     TEXT,
  claimer           TEXT NOT NULL,
  amount_token0     TEXT NOT NULL,
  amount_token1     TEXT,
  tx_hash           TEXT NOT NULL,
  block_number      INTEGER NOT NULL,
  is_first_ever_claim INTEGER DEFAULT 0,
  claimed_at        INTEGER NOT NULL
);

Bitquery token discovery

Bitquery GraphQL is used as an independent token discovery mechanism — catching tokens created by all five factory versions even before they appear in Clanker's own API. The sync cursor is stored inbitquery_syncso each run processes only new blocks.

query DiscoverNewTokens($sinceBlock: Int!, $limit: Int!) {
  ethereum(network: base) {
    smartContractEvents(
      smartContractAddress: {
        in: [
          "0x250c9FB2", "0x9B84fcE5",
          "0x732560fa", "0x375C15db", "0xE85A59c6"
        ]
      }
      smartContractEvent: { is: "TokenCreated" }
      height: { gt: $sinceBlock }
      limit: { count: $limit }
      orderBy: { ascending: false, field: "block.height" }
    ) {
      block { height timestamp { unixtime } }
      transaction { hash }
      arguments {
        argument
        value
      }
    }
  }
}