Seshat

Documentation

Fee Scanner

Fee Intelligence

The fee scanner is Seshat's core data pipeline. It discovers every Bankr/Doppler token launched on Base, reads unclaimed creator fee balances every 10 minutes via the Bankr API, and enqueues scoring whenever meaningful fees accumulate.

Cadence: every 10 minPrimary: Bankr APIFallback: chain V1/V2 ABI

Discovery strategy

Token discovery runs on every 10-minute cron cycle. The Bankr API is the source of truth for all Doppler (Uniswap V4) tokens. Clanker API is a secondary fallback, used only when Bankr returns nothing.

1. Bankr API

GET /token-launches?limit=100 — returns the most recent Bankr launches with deployer wallet, feeRecipient wallet, pool ID, Twitter handles, and chain. All status=deployed, chain=base tokens are tracked.

2. Clanker API

GET /tokens (Clanker) — used as fallback when Bankr returns nothing. Covers Clanker-deployed tokens not yet visible in the Bankr feed.

3. Chain / Basescan

Last resort: reads token name/symbol via ERC-20 on-chain calls, and resolves the deployer wallet from Basescan's contract creator endpoint.

// providers/bankr.ts
// Primary discovery — Bankr API
const launches = await bankr.fetchRecentLaunches(100);

for (const launch of launches) {
  if (launch.status !== 'deployed' || launch.chain !== 'base') continue;

  // Each launch has three distinct identities:
  const deployer         = launch.deployer.walletAddress;       // who signed the tx
  const feeRecipient     = launch.feeRecipient.walletAddress;   // where fees flow
  const projectTwitter   = launch.feeRecipient.xUsername        // company/project Twitter
                        ?? launch.deployer.xUsername;
  const poolId           = launch.poolId;  // 32-byte Uniswap V4 pool ID
}

Developer identity — 3-layer model

Every Bankr token has three distinct identities. Understanding which is which is critical for correct developer scoring and Twitter-based intelligence.

Layer 1 — Deployer wallet

The 0x address that signed the deploy transaction. Often a proxy or ops wallet — not necessarily the real builder. Stored as deployer_wallet on the token record.

Layer 2 — feeRecipient (company)

Where creator fees flow. This is the company or project's wallet and Twitter (@bondoncredit, for example). tokens.twitter_handle stores their X username. This is the primary scoring target.

Layer 3 — Actual developer

The human who built it — found by exploring the company's social graph (bio, following, mentions). E.g. @vandynathan for the BOND token. Their personal FrontrunPro smart followers are the core signal.

Fee reading — Bankr API primary

All Bankr/Doppler tokens run on Uniswap V4 pools. The Bankr API provides a fee endpoint that returns claimable and claimed amounts per token, handling pool address resolution internally.

Critical: Doppler pool pairs have variable token ordering — WETH may be token0 or token1 depending on the order addresses were sorted at pool creation. Always use the token0Label /token1Label fields to identify which side is WETH. Fee amounts are returned as strings — always parseFloat().

// providers/bankr.ts
// GET /token-launches/:tokenAddress/fees?days=30
// Response shape:
{
  address: string,   // feeRecipient wallet
  tokens: [{
    tokenAddress: string,
    token0Label:  string,   // e.g. "WETH" or "BOND"
    token1Label:  string,   // e.g. "BOND" or "WETH" — ordering varies
    claimable: { token0: string, token1: string },  // amounts as strings
    claimed:   { token0: string, token1: string, count: number },
  }],
  totals: {
    claimableWeth: number,  // always WETH-denominated (reliable fallback)
    claimedWeth:   number,
    claimCount:    number,
  }
}

// Label-aware extraction — never assume token0 = WETH
extractUnclaimedEth(fees: BankrFeeData, tokenAddress: string): number {
  const entry = fees.tokens.find(t => t.tokenAddress?.toLowerCase() === addr);
  if (entry) {
    if (entry.token0Label === 'WETH') return parseFloat(entry.claimable.token0);
    if (entry.token1Label === 'WETH') return parseFloat(entry.claimable.token1);
  }
  // totals.claimableWeth is always correct regardless of pool ordering
  return parseFloat(String(fees.totals?.claimableWeth ?? 0));
}

WETH → USD conversion

Creator fees are denominated in WETH, not the project token. USD conversion uses dex.getEthPrice() which fetches the live WETH price on Base (WETH address: 0x4200...0006). Never multiply by the project token's own price.

const ethPriceUsd  = await dex.getEthPrice();        // fetched once per scan cycle
const unclaimedUsd = unclaimedEth * ethPriceUsd;   // ✓ correct
// const wrong = unclaimedEth * tokenPriceUsd;      // ✗ wrong — token price ≠ WETH price

Fee reading — chain ABI fallback

For Clanker-deployed tokens (which have LP contracts instead of V4 pools), the scanner falls back to direct ABI calls. V2 is tried first; if it reverts, V1 is used.

// providers/baseChain.ts
async function getCreatorReward(lpAddress: string): Promise<number> {
  try {
    // V2 / V3: creatorReward(address) → uint256
    return await client.readContract({
      address: lpAddress,
      abi: CLANKER_V2_ABI,
      functionName: "creatorReward",
    });
  } catch {
    // V1 fallback: unclaimedFees() → uint256
    return await client.readContract({
      address: lpAddress,
      abi: CLANKER_V1_ABI,
      functionName: "unclaimedFees",
    });
  }
}

Scoring pipeline

After each fee scan, every token with meaningful unclaimed fees is enqueued on the seshat-fee-queue. The queue worker computes the composite score and, if it clears the alert threshold (≥65), enqueues an alert.

// Queue flow after fee scan:
// [fee scanner] → seshat-fee-queue → [composite scorer]
//                                           ↓
//                               if composite ≥ 65 AND research exists:
//                                    → seshat-alert-queue → Telegram / Discord
//
// Research is triggered separately every 6h for top tokens by volume.
// Threshold for research queue: market_cap_usd > $1,000

const ALERT_THRESHOLD    = 65;   // composite score — Telegram/Discord alert fires
const MIN_FEES_USD       = 10;   // minimum unclaimed USD to record a fee_claims row

Contract safety checks

The 2-hour cron also runs a contract safety batch — checking whether each token's contract is source-verified on Basescan and whether ownership has been renounced. Results are stored and factor into the composite score.

// Checks per token (once per 24h):
// 1. Basescan source verification  → is_contract_verified (0/1)
// 2. owner() == address(0)         → ownership_renounced  (0/1)
//    (reverts treated as renounced — no owner() = safe)

await db.prepare(`
  UPDATE tokens SET
    is_contract_verified = ?,
    ownership_renounced  = ?,
    contract_safety_at   = datetime('now')
  WHERE address = ?
`).bind(isVerified ? 1 : 0, renounced ? 1 : 0, tokenAddress).run();

Database schema

-- tokens: one row per tracked token (migrations 0001 + 0003)
CREATE TABLE tokens (
  id                    TEXT PRIMARY KEY,
  address               TEXT UNIQUE NOT NULL,
  symbol                TEXT NOT NULL,
  name                  TEXT NOT NULL,
  deployer_wallet       TEXT,               -- Layer 1: who signed the deploy tx
  pair_address          TEXT,               -- V4 pool ID (32-byte) or V3 LP address
  clanker_id            TEXT,               -- Clanker internal ID (if applicable)
  fee_model             TEXT,               -- 'post_migration' (V4) | 'pre_migration' (V3)
  creator_fee_pct       REAL,               -- creator share (0.5 = 50bp)
  total_fees_accrued    REAL DEFAULT 0,     -- WETH accrued (claimed + unclaimed)
  total_fees_claimed    REAL DEFAULT 0,     -- WETH already claimed
  last_fee_scan         TEXT,               -- ISO datetime of last scan
  price_usd             REAL,
  market_cap_usd        REAL,
  volume_24h_usd        REAL,
  liquidity_usd         REAL,
  twitter_handle        TEXT,               -- Layer 2: feeRecipient Twitter (company/project)
  website_url           TEXT,
  github_url            TEXT,
  is_contract_verified  INTEGER,            -- 1 = Basescan verified
  ownership_renounced   INTEGER,            -- 1 = owner() == address(0)
  fee_accrual_rate_7d   REAL,               -- avg WETH/day over 7 days
  fee_velocity_score    INTEGER,            -- 0-100
  created_at            TEXT DEFAULT (datetime('now')),
  updated_at            TEXT DEFAULT (datetime('now'))
);

-- fee_claims: snapshot per scan cycle where fees ≥ MIN threshold
CREATE TABLE fee_claims (
  id                   TEXT PRIMARY KEY,
  token_address        TEXT NOT NULL,
  developer_id         TEXT,               -- FK → developers.id
  unclaimed_amount_eth REAL NOT NULL,
  unclaimed_amount_usd REAL NOT NULL,
  is_claimed           INTEGER DEFAULT 0,
  claimed_at           TEXT,
  claim_tx_hash        TEXT,
  scanned_at           TEXT DEFAULT (datetime('now')),
  block_number         INTEGER
);