I built a sell planner to dodge the pros. They were under 4% of buys
DEV Community

I built a sell planner to dodge the pros. They were under 4% of buys

Somebody hands you a token you never asked for, and now you have to turn it into dollars. Maybe a nonprofit got a $40K memecoin donation, or a freelancer got paid in a project's token. They have never used a DEX, and they don't want to be the seller who knocks the price down, or the one selling into a day when the funds are dumping too. I built Glidepath for that person. You paste token, chain and amount held. It gives you a dated selling calendar: one tranche per day, each sized to the token's organic demand, with a .ics file so the plan lands in your calendar. It plans. It never trades. - Live: https://glidepath.edycu.dev - Code: https://github.com/edycutjong/glidepath (MIT) The whole design rested on one assumption. The data knocked it down in the first afternoon. This post covers what I assumed, what Nansen's labels showed, and which part of the product turned out to do the real work. The assumption My premise was that a big chunk of a memecoin's DEX buying comes from pros: Smart Money wallets, funds, whales, exchanges, sniper-bot users. If you size your daily sell against headline volume, you are sizing it against money that can leave the same afternoon. So take the pros out, size against what's left, and call that organic demand. Nansen can do that split, because its wallets carry labels. The list of labels that make a buyer "not organic" lives in one constant: /** * Labels that make a buyer "not organic": Nansen's Smart Money tiers, funds, whales, exchanges and the Telegram * sniper-bot user tags. From the OpenAPI LabelType enum; Public Figure / LP / miner labels are deliberately kept - * those are people, not the cohort a forced seller must avoid competing with. / export const EXCLUDED_LABELS = [ "Smart Trader", "30D Smart Trader", "90D Smart Trader", "180D Smart Trader", "Fund", "Whale", "Exchange", "Maestro Bot User", "Top Maestro Bot User", "BananaGun Bot User", "Top BananaGun Bot User", ] as const; Surprise 1: you can't see the label you filtered on My first plan was to pull every buyer from tgm/who-bought-sold and classify the rows myself. That doesn't work. The address_label on a row is a display name, not the label the filter matched. On PEPE, the single wallet that exclude_smart_money_labels removed shows up as nftsindubai.eth . Elsewhere, excluded rows read High Balance or Token Deployer . So the organic/pro split only exists on Nansen's side, as a filter. On the 8 tokens I probed, include_smart_money_labels and exclude_smart_money_labels with the same list partitioned the buyer set exactly. That became the pager: // packages/core/src/nansen.ts - abridged: signature and field list trimmed const filters: Record = {}; if (filter.exclude) filters.exclude_smart_money_labels = [...filter.exclude]; if (filter.include) filters.include_smart_money_labels = [...filter.include]; const rows: WhoBoughtSoldRow[] = []; let pages = 0; for (let page = 1; page ("tgm/who-bought-sold", { chain, token_address: token, buy_or_sell: "BUY", date: { from: iso(from), to: iso(to) }, pagination: { page, per_page: 1000 }, filters, order_by: [{ field: "bought_volume_usd", direction: "DESC" }], } / , fields, opts /); pages++; rows.push(...(res.data ?? [])); if (res.pagination?.is_last_page !== false || (res.data ?? []).length === 0) return { rows, pages, truncated: false }; } return { rows, pages, truncated: true }; Surprise 2: you can't page the organic side either The obvious next move was to page the organic buyers and add them up. Then I ran it on 5-to-7-day-old Solana launches. They had more than 20,000 buying addresses in 7 days: 20 pages of 1,000, 110 seconds, 20 credits, and the list was still truncated. So the math is flipped. Glidepath never adds up the organic side. It takes total DEX buys from tgm/token-information and subtracts the pro rows. Across every token I looked at, the pros were at most 67 wallets, so that list is one page, three at most: // packages/core/src/plan.ts - computePlan, the organic-demand block (abridged: surrounding code omitted) if (totalBuy7dUsd != null) { if (proBuy7dUsd == null) { organicBuy7dUsd = totalBuy7dUsd; organicShare = null; warnings.push("pro-buyer split unavailable (who-bought-sold failed) - organic = all DEX buys"); } else { organicBuy7dUsd = Math.max(0, totalBuy7dUsd - proBuy7dUsd); organicShare = totalBuy7dUsd > 0 ? organicBuy7dUsd / totalBuy7dUsd : null; } } // from the pager's own flag: a list that ends exactly on page 3 is complete, not truncated if (facts.proTruncated) warnings.push(pro-buyer list truncated at ${facts.proPages} pages - organic volume is an upper bound); const organicDailyUsd = organicBuy7dUsd == null ? null : organicBuy7dUsd / 7; That last comment is a bug I fixed later. The warning used to fire on proPages >= 3 , so a pro list that happened to end on exactly page 3 was reported as truncated when it was complete. Surprise 3: the premise was wrong Once the subtraction worked, I printed the split for real tokens. Here is PEPE, recorded live on 2026-09-16: organic buys $458,738/day = ($3,236,056 DEX buys 7d − $24,893 by Smart Money/Fund/Whale/Exchange/bot users [1 wallets]) / 7 · organic share 99.2% · 528 organic buyers One labelled wallet. 99.2% organic. And PEPE wasn't unusual. On all 11 tokens I tried, pros were 0-3.6% of DEX buy volume. Majors came out 98.5-100% organic. Fresh Solana launches came out 96.4-96.7%. The four sniper-bot labels matched zero rows, even on pump.fun launches with 20,000+ buyers. I kept them in the exclusion list, but I don't claim they do anything. So the label filter I built the product around moves the "organic" number by a few percent. That's worth knowing, but it isn't the headline. I had a choice: make the split look dramatic on screen, or show it at its real size. The app shows it at its real size, and the README lists it as a limitation. What actually changes the calendar The part of Nansen's data that visibly changes a plan is cohort flows, not buyer labels. Glidepath marks a day red if Smart Money is net-selling, or if exchanges are receiving more tokens than organic buyers absorb in a day: /* Red-day test: Smart Money net-selling past θ_sm, or net deposits to exchanges past θ_ex. */ export function redDay(smNetUsd: number | null, exNetUsd: number | null, th: Theta): { red: boolean; reason: string | null } { const fmt = (v: number) => ${v th.exUsd) return { red: true, reason: Exchange net deposits ${fmt(exNetUsd)}` }; return { red: false, reason: null }; } export function theta(organicDailyUsd: number | null): Theta { const d = organicDailyUsd ?? 0; return { smUsd: Math.max(THETA_FLOOR_USD, THETA_SM_SHARE * d), exUsd: Math.max(THETA_FLOOR_USD, THETA_EX_SHARE * d) }; } "Today" comes from tgm/flow-intelligence . The 13 complete days before it come from tgm/flows daily cohort history. A red today halves the first tranche, and the red-day rate over the last 13 days stretches the expected finish date. On PEPE's 16 September run, 4 of the 13 days had been red, all from exchange deposits between +$499K and +$1.92M. That turned a 2-tranche plan into "expect ~3 days". When I recorded the demo on the 19th, today itself was red (+$736K net exchange deposits) and the first tranche was halved on camera. The exchange threshold is a full day of organic buying, and that's on purpose. For CEX-listed tokens, ±$1M of exchange flow is routine shuffling. The threshold is the point where you would really be competing with the pros. Tranche size is min(k × organic/day, 1% of liquidity_usd) , where k slides from 10% down to 3% as Nansen's peer-percentile risk indicators rise. For PEPE that came to 2 tranches, $30,598 then $9,372. Under a constant-product model the estimated cost is $147.90, against $230.36 for dumping the whole bag today. Testing the planner computePlan is a pure function of (facts, input, now) , so it's cheap to test hard. There are 6 fast-check properties at 10,000 runs each, 60,000 generated cases. They check that tranches plus remainder equal the bag, that every tranche stays under both caps, that a red today halves tranche 1 and only tranche 1, and that the dates are consecutive. On its first run it found a real bug. At a dust price, the liquidity cap underflowed to a tranche of zero tokens, and the planner printed 90 empty rows. The fix is one guard: // packages/core/src/plan.ts - sizeTranches (abridged: loop omitted) // a tranche of zero tokens (liquidity cap underflowing at a dust price) would otherwise emit MAX_DAYS empty rows: // nothing can be sold at this pace, so the calendar is empty and the whole bag is the unsold remainder if (!(trancheTokens > 0)) return { tranches, days: 0, truncated: true, remainderTokens: amount, remainderPct: 1 }; The suite has 258 vitest tests in total. 13 recorded live plans replay offline byte-for-byte with zero network calls. Cold plans run p50 3.5 s / p95 6.0 s against live Nansen, averaging 13 credits each (12 on EVM chains, 15 on solana/base, where three trade/quote calls replace the cost model with a real route). Limitations - The cost model is an approximation. It treats liquidity_usd as one pool with the token on one side. That's optimistic for tokens whose depth sits in a single thin pool, and it ignores MEV and gas. Only solana and base get a real routed quote. - Future days can't be known red or green. Each calendar event carries the rule, so you re-check it on the morning. - Two definitions of "Smart Money." Today's test uses flow-intelligence's smart_trader cohort. History usestgm/flows 'smart_money cohort. For PEPE that's 21 wallets vs 18 holders. - A nonexistent-but-valid address still spends 12 credits, because the calls fan out in parallel with no existence check first. - Stablecoins get no history strip. tgm/flows refuses them with a 422, and the plan says so. This is not financial advice. It is a pacing calculator with its reasoning shown. Try it - Live: https://glidepath.edycu.dev. Click the PEPE or BONK chip, then open "Every Nansen call" to see what ea

Read on DEV Community ↗ ← Back to News

Comments

No comments yet. Start the discussion.