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.
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
);