# Pool Scanner — Brain On BNB AI # Reads any BNB Chain pool. No backend at all. # # This is the complete pool-scanner bundle as a single file, so it can be read in # one fetch. 6 files, 5648 lines. # Download as a zip: https://brainonbnb.com/code/pool-scanner.zip # Everything else: https://brainonbnb.com/code/index.txt # # No secrets are present: they are supplied at runtime through the environment. # MIT licensed. ============================================================================== === FILE: dashboard/scanner-chain.js ============================================================================== // SCANNER — the chain layer. Everything that produces a number lives here; the // page layer does nothing but display what this returns. // // The rule this file exists to enforce: a figure is either measured or it is // labelled. Nothing is inferred from a reputation service and then presented as // fact. That rule was not free — an earlier version took the transfer tax from // GoPlus, which reported 4.45% sell tax for $Max while four executed sells on // the chain charged exactly 3.000%. The label was wrong, our cost column was // wrong with it, and nothing on the page would have told you. // // Three endpoints, each chosen for one capability: // RPC eth_call. Binance's dataseed refuses eth_getLogs outright. // LOGS_RPC eth_getLogs, ~7,900 blocks (an hour) near the head. That is enough to find // real trades and measure what they were actually charged. // GOPLUS contract properties no call reveals (mintable, proxy, LP lockers). // Optional, always attributed, never silently trusted. // A single public endpoint cannot carry this page. Measured across a dozen of // them: Binance's own dataseeds drop whole batches once an address has been // scanned a few times in a row, Ankr and ninicoin refused every batch outright, // and one endpoint dropping is indistinguishable — from inside the page — from // a token having no pools. So there is a pool of endpoints, ordered by measured // latency at 25 calls, and a failure moves to the next rather than becoming a // claim about somebody's token. All of them send CORS: * , which is what makes // running this from the visitor's own browser possible at all. export const RPCS=['https://bsc.publicnode.com','https://bsc-rpc.publicnode.com', 'https://bsc-dataseed1.defibit.io','https://bsc-mainnet.public.blastapi.io', 'https://bsc-dataseed.binance.org','https://1rpc.io/bnb']; // Endpoints measured on 2026-08-29 against a 5,000-block eth_getLogs on a busy // pair: only these two answered it at all. defibit and Binance's own dataseed // said "limit exceeded", blastapi and drpc rate-limited, 1rpc caps the range at // fifty blocks, llamarpc did not resolve. They share an operator, so they // probably share a budget — but two hostnames spread a burst of five tier // queries better than one does, and the caller cannot be asked to go slower. export const LOGS_RPCS=['https://bsc-rpc.publicnode.com','https://bsc.publicnode.com']; // THE WINDOW. One eth_getLogs call of this many blocks at the head: an hour of // chain at BSC's 0.45 s blocks (7,900 blocks = 59 min, so hourly windows taken // at the same minute never overlap). Measured 2026-09-09: the free log // endpoint and the keyed one both answer 7,900 blocks in one call and refuse // only beyond ~12,000 as "archive". It was 4,999 (37.5 min) before, and the // pool and width records counted that as "hours" — a day of them took a day // and a half of runs, and the fee estimate rested on 62% of each hour's swaps. export const WINDOW_BLOCKS=7900; // Live bindings: useKeyedRpcs() below moves a keyed endpoint to the front. export let RPC=RPCS[0], LOGS_RPC=LOGS_RPCS[0]; export const GOPLUS='https://api.gopluslabs.io/api/v1/token_security/56?contract_addresses=', GOPLUS_TOKEN='https://api.gopluslabs.io/api/v1/token', V2FACTORY='0xca143ce32fe78f1f7019d7d551a6402fc5350c73', V3FACTORY='0x0bfbcf9fa4f9c56b0f40a671ad40e0805a091865', QUOTER='0xb048bbc1ee6b733fffcfb9e9cef7375518e25997', WBNB='0xbb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c', BNB_PAIR='0x58f876857a02d6762e0101bb5c46a8c1ed44dc16', DEAD='0x000000000000000000000000000000000000dead', NULLA='0x0000000000000000000000000000000000000000'; // All 18 decimals on BSC — checked, unlike on other chains where USDT/USDC are 6. export const QUOTES=[[WBNB,'BNB',0], ['0x55d398326f99059ff775485246999027b3197955','USDT',1], ['0xe9e7cea3dedca5984780bafc599bd69add087d56','BUSD',1], ['0x8ac76a51cc950d9822d68b83fe1ad97b32cd580d','USDC',1], ['0x8d0d000ee44948fc98c9b98a4fa4921476f08b0d','USD1',1]]; export const V3_FEES=[100,500,2500,10000]; // Constant-product venues, and the fee each one charges. // // Every fee here was DERIVED, not read off a docs page: for each router, its // factory() was confirmed on-chain, then getAmountsOut was solved against the // pool's live reserves — out*rIn / (in*(rOut-out)) — which returns the fee the // contract actually applies. PancakeSwap came back 0.2500% exactly, which is // what validates the method; Uniswap 0.3000%, Biswap 0.2000%. // // A factory that is not in this table is NOT scanned and NOT guessed at. That // matters more than it sounds: the previous build applied PancakeSwap's 0.25% // to any V2-shaped pair a visitor pasted, so a Uniswap pool's cost column came // out 0.05 points light with nothing on the page to say so. export const FACTORIES={ '0xca143ce32fe78f1f7019d7d551a6402fc5350c73':{name:'PancakeSwap V2',fee:0.0025}, '0x8909dc15e40173ff4699343b6eb8132c65e18ec6':{name:'Uniswap V2',fee:0.0030}, '0x858e3312ed3a876947ea49d572a7c42de08af7ee':{name:'Biswap',fee:0.0020}, }; export const V2_FEE=0.0025; export const STEPS=[100,150,250,500,1000,2500]; // A batch of 40 eth_calls comes back "method eth_call in batch triggered rate // limit"; 26 goes through in 86ms. Everything below chunks to stay under it. const MAX_BATCH=25; const S={reserves:'0x0902f1ac',token0:'0x0dfe1681',token1:'0xd21220a7',fee:'0xddca3f43', slot0:'0x3850c7bd',decimals:'0x313ce567',symbol:'0x95d89b41',name:'0x06fdde03', totalSupply:'0x18160ddd',factory:'0xc45a0155',feeTo:'0x017e7e58'}; const pad=a=>'0'.repeat(24)+a.slice(2).toLowerCase(); const num=v=>BigInt(v).toString(16).padStart(64,'0'); export const balOf=a=>'0x70a08231'+pad(a); export const getPair=(t,q)=>'0xe6a43905'+pad(t)+pad(q); export const getPool=(t,q,f)=>'0x1698ee82'+pad(t)+pad(q)+num(f); export const quoteCall=(tin,tout,amt,fee)=>'0xc6a5026a'+pad(tin)+pad(tout)+num(amt)+num(fee)+num(0); export const call=(to,data)=>({to,data}); export const hx=h=>(h&&h!=='0x')?BigInt(h):0n; export const addrAt=h=>h&&h.length>=42?('0x'+h.slice(-40)).toLowerCase():null; export const res2=h=>h&&h.length>=130 ?[Number(BigInt('0x'+h.slice(2,66))),Number(BigInt('0x'+h.slice(66,130)))]:null; export const SEL=S; // A dynamic string arrives as offset/length/data, but a few older tokens answer // name()/symbol() with a raw bytes32. Both decode or the label is lost for // no good reason. export function decStr(h){ if(!h||h==='0x')return ''; const b=h.slice(2); try{ if(b.length>=128){ const len=parseInt(b.slice(64,128),16); if(len>0&&len<=128){ // ABI string: decode the bytes as UTF-8, not as one char per byte. The // byte-per-char reading only accepted ASCII, so every token with a // Chinese, Cyrillic or emoji symbol came back as '' and was shown by // its address (seen on the four.meme feed, 2026-09-03). Control // characters are dropped; everything printable is kept. const bytes=new Uint8Array(len);for(let i=0;i=32&&c<127)s+=String.fromCharCode(c)} return s.trim(); }catch(e){return ''} } // Chunked, and loud when it fails. // // The node answers an over-long batch with one error object PER ENTRY rather // than one error for the request. Reading those as empty results is how a rate // limit turned into "this token has no liquidity" — a network condition // silently rendered as a statement about somebody's token. It cost a real // scan of $TUT, whose $2.2M pool simply vanished from the page. // // So: small chunks, one patient retry, and if the node still will not answer, // an exception that surfaces as an error message. Never a quiet empty. const sleep=ms=>new Promise(r=>setTimeout(r,ms)); // Sticky index: once an endpoint answers it keeps being used, so a healthy scan // costs no extra round trips. It only moves on when one actually fails. let epi=0; // A keyed endpoint first, when the caller has one. The browser page and the // installable skill never do — a key in a downloadable file is a published // key — but the Worker behind /api/* and /mcp shares its egress address with // the whole of Cloudflare, and the free publicnode budget it competes for // there gave one answer in ten a throttled read in September 2026: the tax // silently "labelled by GoPlus" instead of measured, the sell test "refused". // A personal token has its own budget. The free list stays behind it as the // fallback, and everything else — batching, throttle detection, the sticky // index — is unchanged. Idempotent, so a Worker may call it per request. export function useKeyedRpcs(urls){ const add=(urls||[]).map(u=>String(u||'').trim()).filter(u=>/^https:\/\//.test(u)); if(!add.length)return false; for(const list of [RPCS,LOGS_RPCS]){ const rest=list.filter(u=>!add.includes(u)); list.splice(0,list.length,...add,...rest); } RPC=RPCS[0];LOGS_RPC=LOGS_RPCS[0];epi=0; return true; } // "method eth_call in batch triggered rate limit", "capacity exceeded", 429s. // Anything mentioning a revert is a real answer and must never match here. const throttled=e=>{ const m=String(e&&e.message||'').toLowerCase(); if(m.includes('revert')||m.includes('execution'))return false; return e&&e.code===-32005||/rate|limit|capacity|too many|quota|busy|exceed/.test(m); }; async function tryPost(url,body){ try{ const r=await fetch(url,{method:'POST',headers:{'Content-Type':'application/json'}, body:JSON.stringify(body),signal:AbortSignal.timeout(12000)}); if(!r.ok)return null; return await r.json(); }catch(e){return null} } export async function rpcBatch(calls,url,block='latest'){ const out=[]; for(let i=0;i({jsonrpc:'2.0',id:k,method:'eth_call',params:[c,block]})); let slot=null; const pool=url?[url]:RPCS; for(let n=0;nx.error&&throttled(x.error)))continue; const s=[];for(const x of j)s[x.id]=x.result; slot=s;if(!url)epi=(epi+n)%pool.length; } if(!slot)throw new Error('every BSC endpoint refused this request'); for(let k=0;kMAX_BATCH)await sleep(40); } return out; } export async function rpc(method,params,url){ const pool=url?[url]:RPCS; for(let n=0;n=194&&res2(q[0])){ // Which venue built this pair decides the fee. Asked, never assumed. const fac=addrAt(q[5]); return {kind:'v2pair',token0:addrAt(q[3]),token1:addrAt(q[4]),reserves:res2(q[0]), factory:fac,venue:fac?FACTORIES[fac]:null}; } if(q[1]&&q[1]!=='0x'&&q[2]&&q[2].length>64) return {kind:'v3pool',fee:Number(hx(q[1])),token0:addrAt(q[3]),token1:addrAt(q[4]), sqrt:hx('0x'+q[2].slice(2,66))}; return {kind:'token'}; } // === PRICING THE QUOTE SIDE === // A pool quoted in BNB or a stablecoin can be stated in dollars directly. A pool // quoted in another meme token cannot — $MatthewCoin trades against $SpaceX, and // no amount of reading that pair reveals what a dollar is. One hop to BNB fixes // it, but honestly: the derived figure inherits the thinness of the hop, so the // depth of that intermediate pool is carried out of here and shown. export async function priceToken(addr,bnbUsd){ const known=QUOTES.find(([a])=>a===addr); if(known)return {usd:known[2]?1:bnbUsd,sym:known[1],direct:true}; const p=await rpcBatch([call(V2FACTORY,getPair(addr,WBNB)),call(addr,S.symbol),call(addr,S.decimals)]); const pair=addrAt(p[0]),sym=decStr(p[1]).slice(0,12)||'?',dec=Number(hx(p[2]))||18; if(!pair||pair===NULLA)return {usd:null,sym,direct:false}; const r=await rpcBatch([call(pair,S.reserves),call(pair,S.token0)]); const rr=res2(r[0]);if(!rr)return {usd:null,sym,direct:false}; const is0=addrAt(r[1])===addr, tok=(is0?rr[0]:rr[1])/Math.pow(10,dec),wb=(is0?rr[1]:rr[0])/1e18; if(!(tok>0)||!(wb>0))return {usd:null,sym,direct:false}; return {usd:(wb/tok)*bnbUsd,sym,direct:false,hopBnb:wb,hopPair:pair}; } // === POOL DISCOVERY === // Asked of the factories, not of an index. An index that has not caught up makes // a live token look dead — GoPlus does not list $CAKE's V2 pair at all, and does // not know $MatthewCoin's. The factory always answers. export async function discover(token,tokDec,bnbUsd){ const facs=Object.keys(FACTORIES); const v2=[];facs.forEach(f=>QUOTES.forEach(([q])=>v2.push(call(f,getPair(token,q))))); const v3=[];QUOTES.forEach(([q])=>V3_FEES.forEach(f=>v3.push(call(V3FACTORY,getPool(token,q,f))))); const found=await rpcBatch([...v2,...v3]); const cands=[]; facs.forEach((f,fi)=>QUOTES.forEach(([qa,sym,stable],i)=>{ const p=addrAt(found[fi*QUOTES.length+i]); if(p&&p!==NULLA)cands.push({kind:'v2',pair:p,quote:qa,sym,usd:stable?1:bnbUsd, fee:FACTORIES[f].fee,venue:FACTORIES[f].name,factory:f}); })); QUOTES.forEach(([qa,sym,stable],i)=>V3_FEES.forEach((f,k)=>{ const p=addrAt(found[facs.length*QUOTES.length+i*V3_FEES.length+k]); if(p&&p!==NULLA)cands.push({kind:'v3',pair:p,quote:qa,sym,usd:stable?1:bnbUsd, fee:f/1e6,feeRaw:f,venue:'PancakeSwap V3'}); })); if(!cands.length)return []; // Depth is measured, never assumed: V2 from reserves, V3 from what the pool // contract actually holds. A pool that exists but is empty must sort last. const calls=[]; cands.forEach(c=>{ if(c.kind==='v2')calls.push(call(c.pair,S.reserves),call(c.pair,S.token0)); else calls.push(call(c.quote,balOf(c.pair)),call(token,balOf(c.pair))); }); const m=await rpcBatch(calls); cands.forEach((c,i)=>{ if(c.kind==='v2'){ const rr=res2(m[i*2]);if(!rr)return; const is0=addrAt(m[i*2+1])===token; c.tok=(is0?rr[0]:rr[1])/Math.pow(10,tokDec);c.q=(is0?rr[1]:rr[0])/1e18; }else{ c.q=Number(hx(m[i*2]))/1e18;c.tok=Number(hx(m[i*2+1]))/Math.pow(10,tokDec); } c.hard=(c.q||0)*c.usd; }); return cands.filter(c=>c.hard>0&&c.tok>0).sort((a,b)=>b.hard-a.hard); } // === V2 MATH === // IMPACT is where the price ends up: reserves after against reserves before. The // pool fee stays in the pool and counts; a transfer tax never reaches the // reserves on a buy, so it cannot move the price at all. // COST is what the trader gives up against spot — a worse fill because the pool // moved underneath, plus the fee and the tax on top. export function ladderV2(tok,q,fee,taxB,taxS,px,quoteUsd){ const FEE=1-fee,TB=1-taxB,TS=1-taxS; return STEPS.map(u=>{ const dQ=u/quoteUsd,effB=dQ*FEE,outB=(tok*effB)/(q+effB), dT=u/px,effT=dT*TS*FEE,outS=(q*effT)/(tok+effT); return{usd:u, buyMove:(((q+dQ)/(tok-outB))/(q/tok)-1)*100, buyCost:(1-TB*FEE*q/(q+effB))*100, sellMove:(((q-outS)/(tok+dT*TS))/(q/tok)-1)*100, sellCost:(1-TS*FEE*tok/(tok+effT))*100}; }); } // (r + x)(r + FEE*x) = k*r^2 -> FEE*x^2 + r(1+FEE)x + r^2(1-k) = 0 export function onePctV2(r,fee,k){const FEE=1-fee,b=1+FEE; return r*((-b+Math.sqrt(b*b+4*FEE*(k-1)))/(2*FEE))} // === V3 MATH === // Concentrated liquidity has no closed form: impact depends on where the // liquidity is parked around the current price, not on two reserves. Rather than // approximate it, the pool's own quoter is asked — one eth_call per size, // returning the exact fill and the exact price afterwards, ticks crossed and // all. That is not an estimate of the trade; it is the trade, simulated. const Q96=2n**96n; export function sqrtToPrice(sqrt,decIn,decOut){ const s=Number(sqrt)/Number(Q96); return s*s*Math.pow(10,decIn-decOut); } // The baseline comes from a DUST QUOTE in the same batch, never from a separate // slot0 read. On a deep, busy pool the price moves more between two round trips // than a $2,500 trade moves it: $BTCB's buy impact came out NEGATIVE across // every rung, because what was being measured was one second of real trading, // not the trade. Quoting a near-zero amount alongside the real ones gives a // "before" from the same block state, so the difference is the trade and // nothing else. export async function ladderV3(pool,token,quote,feeRaw,tokDec,px,quoteUsd,taxB,taxS,sqrtBefore,tokenIs0){ // The sell side is quoted with the amount that SURVIVES the transfer tax, // because that is all the pool ever sees. Quoting the gross amount and // scaling the answer afterwards would be an approximation where an exact // figure was available for the same single call. const amtsBuy=STEPS.map(u=>BigInt(Math.floor(u/quoteUsd*1e18))), amtsSell=STEPS.map(u=>BigInt(Math.floor(u/px*(1-taxS)*Math.pow(10,tokDec)))); const dust=BigInt(Math.max(1,Math.floor(1/quoteUsd*1e18))); // ~$1 of the quote token const calls=[call(QUOTER,quoteCall(quote,token,dust,feeRaw)), ...amtsBuy.map(a=>call(QUOTER,quoteCall(quote,token,a,feeRaw))), ...amtsSell.map(a=>call(QUOTER,quoteCall(token,quote,a,feeRaw)))]; const all=await rpcBatch(calls); const baseHex=all[0],r=all.slice(1); const before=(baseHex&&baseHex.length>=130) ? Number(BigInt('0x'+baseHex.slice(66,130))) // same block state as the rungs : Number(sqrtBefore); // A rung the pool cannot fill. When a size takes more than the pool holds // in range, the quoter walks to the end of the liquidity and reports the // price at the limit — sqrtPriceX96After lands on MIN/MAX_SQRT_RATIO and // squaring that printed "+5.33e+41%" for KII (1% tier, $750 in range), // with "you pay 47.76%" for a fill that never happened. Such a rung says // "the pool runs out at this size", not a number. const MIN_SQRT=4295128739,MAX_SQRT=1.4614467034852101e48; const dry=h=>{ if(!h||h.length<130)return false; const after=Number(BigInt('0x'+h.slice(66,130))); return !(after>0)||after<=MIN_SQRT*1.0001||after>=MAX_SQRT*0.9999; }; const move=h=>{ if(!h||h.length<130||dry(h))return null; const after=Number(BigInt('0x'+h.slice(66,130))); if(!(after>0)||!(before>0))return null; const ratio=Math.pow(after/before,2); // price of token0 in token1 const m=((tokenIs0?ratio:1/ratio)-1)*100; // ...expressed for OUR token return Math.abs(m)>1000?null:m; // beyond ten-fold is the limit too }; const out=h=>h&&h.length>=66?Number(BigInt('0x'+h.slice(2,66))):null; // SPOT, from the same block state as the rungs — for the same reason the // impact baseline is taken from the dust quote and not from slot0. Cost is a // comparison against spot, and slot0 was read one round trip earlier: on a // busy pool the price drifts more in that second than a $100 trade moves it, // which produced a NEGATIVE cost ("you pay −0.06%", i.e. the pool pays you) // on $BLUAI and $DOS. The dust quote already has the pool fee taken out of // its input, so the fee is added back to recover the mid price — otherwise // the fee would quietly vanish from the cost it is part of. const dustOut=out(baseHex),feeFrac=feeRaw/1e6; const pxLive=(dustOut>0) ? (Number(dust)/1e18)*(1-feeFrac)/(dustOut/Math.pow(10,tokDec))*quoteUsd : px; const spot=pxLive>0?pxLive:px; return STEPS.map((u,i)=>{ const b=r[i],s=r[STEPS.length+i]; const outTok=out(b),outQ=out(s); // Buy: quote in, tokens out, then the transfer tax is taken off the top. const gotTok=outTok!=null?outTok/Math.pow(10,tokDec)*(1-taxB):null; const paidQ=u/quoteUsd; // Sell: the pair only ever sees the taxed amount, so the tax is applied to // the input before the quote, exactly as the chain does it. What the trader // gives up is the GROSS amount, valued at spot. const gotQ=outQ!=null?outQ/1e18:null; const survS=(1-taxS)>0?(1-taxS):1; const sentTok=Number(amtsSell[i])/Math.pow(10,tokDec)/survS; const RUNS_OUT='the pool runs out at this size'; const bDry=dry(b)||(b&&b.length>=130&&move(b)==null&&outTok!=null), sDry=dry(s)||(s&&s.length>=130&&move(s)==null&&outQ!=null); return {usd:u, buyMove:bDry?null:move(b), buyCost:(!bDry&&gotTok!=null)?(1-(gotTok*spot)/(paidQ*quoteUsd))*100:null, buyNote:bDry?RUNS_OUT:null, sellMove:(!sDry&&move(s)!=null)?-Math.abs(move(s)):null, sellCost:(!sDry&&gotQ!=null&&sentTok>0)?(1-(gotQ*quoteUsd)/(sentTok*spot))*100:null, sellNote:sDry?RUNS_OUT:null}; }); } // The ladder's six fixed sizes cannot express depth for a pool that is far // deeper or far thinner than they assume, so the quoter is swept geometrically // and the crossing of 1% is read off the curve. Interpolated in log space // because impact against size is very close to a straight line there. export async function onePctV3(pool,token,quote,feeRaw,tokDec,px,quoteUsd,sqrtBefore,tokenIs0,taxS=0){ // $20 to $976M. The old sweep stopped at $1.3M and simply gave up on anything // deeper: USDC/USDT at the 0.01% tier does not move one percent for any figure // in that range, so both fields printed "—" for a pool holding $27M — read as // "could not measure" when the truth was "more than we asked". Twelve probes // is also exactly the batch ceiling once the dust quote and both directions // are counted (1 + 12 + 12 = 25). const probes=Array.from({length:12},(_,i)=>20*Math.pow(5,i)); const dust=BigInt(Math.max(1,Math.floor(1/quoteUsd*1e18))); // The sell probes are quoted with what SURVIVES the transfer tax, because that // is all the pool ever sees — the same correction the V2 path applies when it // divides by (1-taxS). Without it a 5%-tax token's "moves the price −1%" was // the size that reaches the pool, not the size the seller has to send. const surv=1-(taxS||0); const calls=[call(QUOTER,quoteCall(quote,token,dust,feeRaw)), ...probes.map(u=>call(QUOTER,quoteCall(quote,token,BigInt(Math.floor(u/quoteUsd*1e18)),feeRaw))), ...probes.map(u=>call(QUOTER,quoteCall(token,quote,BigInt(Math.floor(u/px*surv*Math.pow(10,tokDec))),feeRaw)))]; const all=await rpcBatch(calls); const baseHex=all[0],r=all.slice(1); const before=(baseHex&&baseHex.length>=130)?Number(BigInt('0x'+baseHex.slice(66,130))):Number(sqrtBefore); const mv=h=>{if(!h||h.length<130)return null; const after=Number(BigInt('0x'+h.slice(66,130))); if(!(after>0)||!(before>0))return null; const ratio=Math.pow(after/before,2); return Math.abs((tokenIs0?ratio:1/ratio)-1)*100}; // Three outcomes, and they must not be flattened into one. A crossing found is // a figure. No crossing because every probe that the pool could quote stayed // under 1% is a LOWER BOUND, not an unknown — "more than $1.6M" is a real // answer and printing "—" for it understates a deep pool. Nothing quotable at // all is the only genuine unknown. const cross=off=>{ const pts=probes.map((u,i)=>({u,m:mv(r[off+i])})).filter(p=>p.m!=null&&p.m>0); if(!pts.length)return {v:null,min:null}; for(let i=1;i=1&&pts[i-1].m<1){ const a=pts[i-1],b=pts[i],t=(Math.log(1)-Math.log(a.m))/(Math.log(b.m)-Math.log(a.m)); return {v:Math.exp(Math.log(a.u)+t*(Math.log(b.u)-Math.log(a.u))),min:null}; } } const last=pts[pts.length-1]; // Below 1% at the largest size the pool would quote: a floor. Above 1% at // the smallest: too thin for this ladder to bracket, so no claim. return {v:null,min:last.m<1?last.u:null}; }; // Both figures are already GROSS: the probe list is denominated in what the // trader sends, and the tax was taken off inside the quoted amount, so there // is nothing left to scale here. const u=cross(0),d=cross(probes.length); return {up:u.v,down:d.v,upMin:u.min,downMin:d.min}; } // === WHERE ELSE DOES IT TRADE? === // This decides whether the pool we can measure is worth measuring at all, so it // cannot depend on a source that only knows the venues it happens to index. // GoPlus used to fill this role and does not know fstswap: a token with $107k // there showed up here as a $15 PancakeSwap dust pool with "+20,456% impact" // and no warning, because from GoPlus's side there was nothing to compare // against. DexScreener indexes the small venues, returns one consistent USD // figure per pool, and sends CORS: * — so the comparison is like for like and // the guard no longer depends on somebody else's coverage. export async function venues(token){ try{ const r=await fetch('https://api.dexscreener.com/latest/dex/tokens/'+token, {signal:AbortSignal.timeout(9000)}); if(!r.ok)return null; const j=await r.json(); const list=(j.pairs||[]).filter(p=>p.chainId==='bsc'&&p.liquidity) .map(p=>({pair:(p.pairAddress||'').toLowerCase(), name:(p.dexId||'?')+' '+((p.labels||[]).join('')||'v2'), quote:p.quoteToken&&p.quoteToken.symbol||'', liq:Math.round(p.liquidity.usd||0)})) .sort((a,b)=>b.liq-a.liq); return list.length?list:null; }catch(e){return null} } // === THE TAX, MEASURED === // Not read off a label — read off trades that actually happened. The pair says // how many tokens it moved; the token's own Transfer events in the same // transaction say how many arrived. The gap is what was charged, to the wallet // that paid it. On a taxed buy the pool emits two transfers, one to the tax sink // and one to the buyer; the buyer's is the larger, and the difference is the tax. export const SWAP_T='0xd78ad95fa46c994b6551d0da85fc275fe613ce37657fb8d5e3d130840159d822', // Concentrated liquidity emits a DIFFERENT Swap event, and asking for the // V2 one over a V3 pool returns an empty list — which this page then // printed as "this pool has not traded in the last two hours" for pools // trading every block. Every V3 token silently fell back to the GoPlus // label, which is the one thing the tax card exists not to do. // And PancakeSwap's V3 Swap is NOT Uniswap's: it carries two extra // protocol-fee words, so it hashes to a different topic. Taken off a live // pool's own logs rather than from a docs page — asking for Uniswap's // topic returned zero swaps on USDC/USDT, a pool that trades every block. // Both are accepted; only the first is ever seen at this venue. SWAP_V3_T='0x19b47279256b2a23a1665c810c8d55a1758940ee09377d4f8d26497a3577dc83', SWAP_V3_UNI='0xc42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67', XFER_T='0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'; // V3 states the two amounts as SIGNED integers from the pool's point of view: // positive went in, negative came out. V2 states four unsigned ones instead. const TWO256=1n<<256n,TWO255=1n<<255n; export const int256=h=>{const v=BigInt('0x'+h);return v>=TWO255?v-TWO256:v}; // How long the readable window actually is, in the reader's units. BSC's block // time is not a constant — it was 3s, then 1.5s, then 0.75s — so "5,000 blocks // is about two hours" was true when it was written and is wrong now. Asked of // the chain instead of assumed. async function windowSpan(from,to){ try{ const [a,b]=await Promise.all([ rpc('eth_getBlockByNumber',['0x'+from.toString(16),false],LOGS_RPC), rpc('eth_getBlockByNumber',['0x'+to.toString(16),false],LOGS_RPC)]); const s=parseInt(b.timestamp,16)-parseInt(a.timestamp,16); if(!(s>0))return null; return s<5400?Math.round(s/60)+' minutes':(s/3600).toFixed(1).replace(/\.0$/,'')+' hours'; }catch(e){return null} } export async function measureTax(token,pair,tokenIs0,kind){ try{ const head=parseInt(await rpc('eth_blockNumber',[],LOGS_RPC),16); // One window, and only one: the hour WINDOW_BLOCKS names (7,900 blocks, // 59 min — the endpoint answers that in one call and refuses only beyond // ~12,000 as "archive"). Until 2026-09-12 this read 4,999 blocks (38 min) // while every other reading on the page said "the last hour", and a thin // pool was simulated where an hour of trades would have measured it. const from=head-(WINDOW_BLOCKS-1); let logs=null; const topic=kind==='v3'?[[SWAP_V3_T,SWAP_V3_UNI]]:[SWAP_T]; // Both log hosts, and a second pass after a beat. The two publicnode names // throttle together (one operator, one budget), and a page that has just // fired a thirty-call batch at them gets a 403 for a second or two. With a // single host and no retry that second was reported to the visitor as // "could not be measured" — seen live on 2026-09-03 on a pool that had // traded minutes earlier. A retry is cheap; a false "unmeasured" is not. const filter={address:pair,topics:topic,fromBlock:'0x'+from.toString(16),toBlock:'0x'+head.toString(16)}; for(let pass=0;pass<2&&!logs;pass++){ if(pass)await new Promise(r=>setTimeout(r,900)); for(const url of LOGS_RPCS){ try{logs=await rpc('eth_getLogs',[filter],url);if(logs)break}catch(e){logs=null} } } // A refused range and a quiet pool arrive as the same emptiness and mean // opposite things. Only one of them may be stated as a fact about somebody // else's pool. if(!logs)return {ok:false,reason:'the log endpoint refused the range',block:head,windowBlocks:WINDOW_BLOCKS}; if(!logs.length){ const span=await windowSpan(from,head); return {ok:false,reason:'this pool has not traded in the last '+(span||'~7,900 blocks'),block:head,windowBlocks:WINDOW_BLOCKS}; } const U=h=>BigInt('0x'+h); const buys=[],sells=[];const seen=new Set(); // Receipts are one round trip each, and a busy pool can offer thousands of // swaps. Sixteen is enough to find three of each on any pool with two-sided // flow, and bounds the wait when a pool is all arb and nothing qualifies. let tried=0; for(const L of logs.slice().reverse()){ if(buys.length>=3&&sells.length>=3)break; if(tried>=16)break; if(seen.has(L.transactionHash))continue;seen.add(L.transactionHash); const d=L.data.slice(2); let tokOut,tokIn; if(kind==='v3'){ const a=int256(d.slice(0,64)),b=int256(d.slice(64,128)),mine=tokenIs0?a:b; tokOut=mine<0n?-mine:0n;tokIn=mine>0n?mine:0n; }else{ const a0i=U(d.slice(0,64)),a1i=U(d.slice(64,128)),a0o=U(d.slice(128,192)),a1o=U(d.slice(192,256)); tokOut=tokenIs0?a0o:a1o;tokIn=tokenIs0?a0i:a1i; } if(!(tokOut>0n)&&!(tokIn>0n))continue; if(tokOut>0n&&buys.length>=3)continue; if(tokIn>0n&&sells.length>=3)continue; // Receipts come from the main node, not the log node: the log endpoint // serves ranges but returns nothing useful for receipts, which cost an // earlier build every single tax measurement while looking like success. tried++; let rec;try{rec=await rpc('eth_getTransactionReceipt',[L.transactionHash])}catch(e){continue} if(!rec||!rec.logs)continue; // Which contracts in this transaction are POOLS. It matters because the // sell side works out the tax by comparing what the pair received against // the other token transfers the seller made — exact for a plain sell, and // nonsense for an arbitrage bot routing the same token through two pools, // where the second leg gets counted as if it were a fee. That is where a // 30.63% sell tax on $CAKE came from, a token with no tax at all. Anything // sent to another pool is a leg, not a fee, and is excluded by name. const isSwap=x=>x.topics[0]===SWAP_T||x.topics[0]===SWAP_V3_T||x.topics[0]===SWAP_V3_UNI; const pools=new Set(rec.logs.filter(isSwap).map(x=>x.address.toLowerCase())); // Our own pair swapped twice in one transaction cannot be matched to one // Swap event, so that transaction is skipped rather than misread. if(rec.logs.filter(x=>isSwap(x)&&x.address.toLowerCase()===pair).length!==1)continue; const xf=rec.logs.filter(x=>x.address.toLowerCase()===token&&x.topics[0]===XFER_T&&x.topics.length>=3) .map(x=>({from:'0x'+x.topics[1].slice(26),to:'0x'+x.topics[2].slice(26),v:U(x.data.slice(2)), i:parseInt(x.logIndex,16)})); if(!xf.length)continue; // A ratio outside [0, 1) is not a tax reading, it is a transfer this code // has mismatched — a rebasing token, a router that batches two swaps into // one receipt, a fee taken in a different token. Dropping it is right; // averaging it in would put a negative or a 300% tax on the page. const keep=(a,v)=>{if(isFinite(v)&&v>=-0.0001&&v<0.99)a.push(Math.max(0,v))}; if(tokOut>0n){ const outs=xf.filter(x=>x.from===pair); if(outs.length){const got=outs.reduce((m,x)=>x.v>m?x.v:m,0n); keep(buys,1-Number(got)/Number(tokOut))} }else{ const inn=xf.find(x=>x.to===pair); // A taxed sell emits its fee leg RIGHT NEXT to the transfer that funds // the swap — same call, adjacent log indices. Anything the same wallet // sends elsewhere in a long routed transaction is a different trade, not // a fee, and summing it in is what produced a 30% sell tax for $CAKE. // Two filters, because either alone leaves a hole: not to another pool, // and not four logs away from the transfer it is supposed to belong to. if(inn){const total=xf.filter(x=>x.from===inn.from&&Math.abs(x.i-inn.i)<=3&& (x.to===pair||!pools.has(x.to))) .reduce((s,x)=>s+x.v,0n); // Above 50% this is not a tax reading, it is a mismatch. Real taxes // that high exist, but they cannot be told apart from a bad match, and // guessing wrong here is worse than saying nothing. if(total>0n){const t=1-Number(inn.v)/Number(total);if(t<0.5)keep(sells,t)}} } } // Exempt wallets exist — the deployer, the tax sink, routers on an allow // list — and they trade at 0%. Taking the median rather than the mean keeps // one exempt trade from dragging the figure below what a normal wallet pays. const med=a=>{if(!a.length)return null;const s=a.slice().sort((x,y)=>x-y); return s.length%2?s[(s.length-1)/2]:(s[s.length/2-1]+s[s.length/2])/2}; const b=med(buys),s=med(sells); if(b==null&&s==null)return {ok:false,reason:'no readable transfers in recent trades',block:head,windowBlocks:WINDOW_BLOCKS}; return {ok:true,buy:b,sell:s,nBuy:buys.length,nSell:sells.length,block:head,windowBlocks:WINDOW_BLOCKS, spread:{buy:buys.map(x=>+(x*100).toFixed(2)),sell:sells.map(x=>+(x*100).toFixed(2))}}; }catch(e){return {ok:false,reason:'the log endpoint did not answer'}} } // === MANY READS, ONE REQUEST === // // Reading a V3 pool's tick book is hundreds of eth_calls, and a JSON-RPC batch // carries at most about 25 of them before this endpoint refuses the request. On // the Workers free plan a single incoming request may make 50 outgoing ones, so // the tick walk alone would spend a third of that budget and a busy pair with // one extra price hop would fall off the edge — as "too many subrequests", // which arrives as a failed scan rather than a slow one. // // Multicall3 is deployed on BNB Chain at the same address it uses everywhere, // and it turns any number of view calls into ONE. Measured 2026-09-01: 400 // ticks() reads returned in 210 ms in a single call, byte for byte identical to // the same 400 asked one at a time. // // It is a fast path, not a dependency. If the aggregate call fails for any // reason the plain batch runs instead and the answer is the same, only slower — // a helper contract must never be the reason a measurement cannot be made. export const MULTICALL3='0xca11bde05977b3631167028862be2a173976ca11'; const w256=v=>BigInt(v).toString(16).padStart(64,'0'); const MC_CHUNK=500; const encodeAggregate3=calls=>{ const structs=calls.map(c=>{ const d=c.data.slice(2),pad=d+'0'.repeat((64-(d.length%64))%64); // (address target, bool allowFailure, bytes callData) — allowFailure is on, // so one reverting call cannot take the other four hundred with it. return '0'.repeat(24)+c.to.slice(2).toLowerCase()+w256(0)+w256(0x60)+w256(d.length/2)+pad; }); let off=32*calls.length,offs=''; for(const s of structs){offs+=w256(off);off+=s.length/2} return '0x82ad56cb'+w256(0x20)+w256(calls.length)+offs+structs.join(''); }; // Every offset below is a BYTE offset into the returned data, which is how the // ABI states them. Reading one of them as a word index instead is not a crash: // it lands on a different word that also parses as a number, and the decode // then fails somewhere further along. The first port of this did exactly that, // and the only symptom was that the fast path silently stopped being taken — // the answers stayed right and the request count went UP by one. const decodeAggregate3=(hex,n)=>{ const b=hex.slice(2),at=o=>b.slice(o*2,o*2+64); const arr=Number(BigInt('0x'+at(0))); const len=Number(BigInt('0x'+at(arr))); if(len!==n)throw new Error('multicall returned '+len+' of '+n); const head=arr+32,out=[]; for(let i=0;i setTimeout(r, 200)); try { const [a, b] = await Promise.all([ rpc('eth_getBlockByNumber', ['0x' + from.toString(16), false], url), rpc('eth_getBlockByNumber', ['0x' + to.toString(16), false], url), ]); const s = parseInt(b.timestamp, 16) - parseInt(a.timestamp, 16); if (s > 0) return s / 60; } catch { /* try the other one */ } } return null; } // eth_getLogs over a range, and when the endpoint refuses it, over its two // halves, then their halves (three levels: eight slices at most). The busiest // pools — CAKE/BNB at 0.05% and 0.25% — came back "refused" in 3 of 3 runs // over 5,000 blocks from the Worker's shared egress while the same range // answered from a laptop; smaller answers pass where the big one does not. // The sample stays the same window, just read in pieces. null = refused. export async function getLogsSplit(params, from, to, url, depth = 0) { try { return await rpc('eth_getLogs', [{ ...params, fromBlock: '0x' + from.toString(16), toBlock: '0x' + to.toString(16) }], url); } catch { if (depth >= 3 || to - from < 64) return null; const mid = from + Math.floor((to - from) / 2); const a = await getLogsSplit(params, from, mid, url, depth + 1); if (a === null) return null; const b = await getLogsSplit(params, mid + 1, to, url, depth + 1); if (b === null) return null; return a.concat(b); } } // === WHERE THE CAPITAL ACTUALLY SITS === // // Every figure this project publishes about a V3 pool has so far divided by // what the pool CONTRACT HOLDS, and said so in a caveat: "in V3 that includes // liquidity sitting outside the current price range, which earns nothing." // A caveat is a promise to measure something later. This is later. // // A liquidity provider is not paid for holding tokens. They are paid for the // liquidity standing where the price is when a swap goes through, in proportion // to their share of it. Capital parked two hundred percent away is on the // books, in the balance, in every chart of "TVL" — and earns nothing. So the // denominator an LP needs is not the pool's balance but the capital standing in // the band the price is actually in. // // This reconstructs that from the pool's own tick data: the active liquidity at // the current price, then every initialised tick inside the band with the net // liquidity it adds or removes, walked outward in both directions. Each // resulting segment is converted to token amounts with the standard // concentrated-liquidity identities, so what comes back is not an index or a // score. It is an amount of each token, in the band, and it can be checked — it // can never exceed what the contract holds. // // V2 is measured the same way rather than exempted. A constant-product pool is // a full-range position with L = sqrt(x*y), so the same band question has the // same kind of answer — and that is the comparison that matters, because a V2 // pool holding forty million dollars may stand less capital at the price than a // V3 pool holding two. const TICK_BASE=1.0001; // sqrt(1.0001^t), the pool's sqrtPrice at a tick, in raw token units. Number // rather than the Q96 integer: every use below is a difference of two nearby // roots multiplied by a liquidity, and doubles carry that to about twelve // significant figures, which is nine more than any dollar figure here needs. const sqrtAtTick=t=>Math.pow(TICK_BASE,t/2); const fdiv=(a,b)=>Math.floor(a/b); // int24 and int16 arguments are two's complement, sign-extended to a full word. const iword=v=>{const b=BigInt(v);return ((b<0n?(1n<<256n)+b:b).toString(16)).padStart(64,'0')}; const TICKS_SEL='0xf30dba93',BITMAP_SEL='0x5339c296', LIQUIDITY_SEL='0x1a686502',SPACING_SEL='0xd0c93a7c'; const int128At=w=>{const v=BigInt('0x'+w);return v>=TWO255?v-TWO256:v}; // slot0 answers sqrtPriceX96 in word 0 and the current tick, signed, in word 1. const int24At=w=>{const v=BigInt('0x'+w);return Number(v>=TWO255?v-TWO256:v)}; // The amounts one liquidity segment holds between two roots, given where the // price stands. Above the price a segment is entirely token0, below it entirely // token1, and the segment containing the price holds both — which is why both // walks below start at the price itself rather than at a tick boundary. export function segAmounts(L,sLo,sHi,sP){ if(!(L>0)||!(sHi>sLo))return [0,0]; if(sHi<=sP)return [0,L*(sHi-sLo)]; if(sLo>=sP)return [L*(1/sLo-1/sHi),0]; return [L*(1/sP-1/sHi),L*(sP-sLo)]; } // A constant-product pool as the full-range position it is. No RPC: by the time // this is worth asking, the reserves are already known. export function bandDepthV2(r0,r1,bandPct){ if(!(r0>0)||!(r1>0))return null; const L=Math.sqrt(r0*r1),sP=Math.sqrt(r1/r0),k=Math.sqrt(1+bandPct/100); const [a0,a1]=segAmounts(L,sP/k,sP*k,sP); return {amount0:a0,amount1:a1,complete:true,initialized_ticks:null}; } // The same question asked of concentrated liquidity, which has to be walked. // // Three rounds for ALL pools at once rather than three rounds per pool: state, // then bitmap words, then the initialised ticks those words point at. These // endpoints rate-limit per request, and five tiers asked one after another is // exactly the pattern that came back half-unreadable before. // The cap is 400 because that is enough to never bite at the band this is used // with: two percent is 198 ticks either side, and the finest spacing PancakeSwap // runs is one, so 397 is the most a complete answer can ever need. It was 192 // first, and WBNB/USDT at the 0.01% tier came back truncated — understated by a // quarter, flagged, but still a smaller number that looked like a real one. export async function bandDepthV3(pools,bandPct,maxTicks=400){ if(!pools.length)return []; // ONE BLOCK FOR ALL THREE ROUNDS. // // The price, the active liquidity and every tick have to come from the same // state or they describe a pool that never existed: the price from one block // and the book from the next is a book with a hole in it. Multicall3 answers // its own block number in the same call that reads the state, so pinning the // two later rounds to it costs nothing — no extra request, no guess about // which block "latest" meant a moment ago. const stCalls=[call(MULTICALL3,'0x42cbb15c'), // getBlockNumber() ...pools.flatMap(p=>[call(p,S.slot0),call(p,LIQUIDITY_SEL),call(p,SPACING_SEL)])]; const st0=await multicall(stCalls); const blk=st0[0]&&st0[0]!=='0x'?'0x'+BigInt(st0[0]).toString(16):'latest'; const st=st0.slice(1); const base=pools.map((p,i)=>{ const s=st[i*3]; if(!s||s.length<130)return null; const sqrtP=Number(BigInt('0x'+s.slice(2,66)))/Number(Q96); const tick=int24At(s.slice(66,130)); const L=Number(hx(st[i*3+1])); const spacing=Number(hx(st[i*3+2]))||1; if(!(sqrtP>0))return null; // The band is set in price, not in ticks, so its edges are exact instead of // rounded to a spacing that differs per tier. The tick bounds only decide // which ticks have to be read. const k=Math.sqrt(1+bandPct/100); const span=Math.ceil(Math.log(1+bandPct/100)/Math.log(TICK_BASE)); return {pool:p,sqrtP,tick,L,spacing, sLo:sqrtP/k,sHi:sqrtP*k,tLo:tick-span,tHi:tick+span}; }); // Which bitmap words cover the band. Ticks are stored compressed by spacing // and packed 256 to a word, so a wide-spacing tier is one word and a // spacing-of-one tier at two percent is two or three. const wordCalls=[],wordOwner=[]; base.forEach((b,i)=>{ if(!b)return; const cLo=fdiv(b.tLo,b.spacing),cHi=fdiv(b.tHi,b.spacing); for(let w=fdiv(cLo,256);w<=fdiv(cHi,256);w++){ wordCalls.push(call(b.pool,BITMAP_SEL+iword(w)));wordOwner.push([i,w]); } }); const words=wordCalls.length?await multicall(wordCalls,undefined,blk):[]; const want=base.map(()=>[]); words.forEach((w,n)=>{ const [i,word]=wordOwner[n],b=base[i]; if(!w||w==='0x')return; const bits=BigInt(w); for(let bit=0;bit<256;bit++){ if((bits>>BigInt(bit))&1n){ const t=(word*256+bit)*b.spacing; if(t>=b.tLo&&t<=b.tHi)want[i].push(t); } } }); // A cap, and it is reported rather than silently applied. A spacing-of-one // tier on a busy pair can carry several hundred initialised ticks inside two // percent, and reading all of them costs more round trips than the answer is // worth — but a figure computed over a truncated tick set is a smaller number // that looks like a real one, so it says which one it is. const truncated=base.map(()=>false); want.forEach((list,i)=>{ if(!base[i])return; list.sort((a,b)=>a-b); if(list.length>maxTicks){ // Keep the ticks NEAREST the price: they carry the liquidity a swap meets // first, so dropping the far edge understates the band by the least. const c=base[i].tick; want[i]=list.slice().sort((a,b)=>Math.abs(a-c)-Math.abs(b-c)) .slice(0,maxTicks).sort((a,b)=>a-b); truncated[i]=true; } }); const tickCalls=[],tickOwner=[]; want.forEach((list,i)=>list.forEach(t=>{ tickCalls.push(call(base[i].pool,TICKS_SEL+iword(t)));tickOwner.push([i,t]); })); // The one round that is worth aggregating. State and bitmap words are a // dozen calls between them and fit in a single batch already; the ticks are // hundreds, and asked as a plain batch they cost sixteen of the fifty // outgoing requests a Worker gets. Through Multicall3 they cost one. const tickRes=tickCalls.length?await multicall(tickCalls,undefined,blk):[]; const nets=base.map(()=>new Map()); tickRes.forEach((r,n)=>{ const [i,t]=tickOwner[n]; if(r&&r.length>=130)nets[i].set(t,int128At(r.slice(66,130))); }); return base.map((b,i)=>{ if(!b)return null; const net=nets[i],inBand=want[i]; let a0=0,a1=0,inUp=0,inDown=0; // Upward from the price. Crossing an initialised tick from below adds its // net liquidity; the first segment starts at the price, because the tick // the price sits in is only partly above it. // // The same walk answers a second question for free, and it is the one that // makes this checkable: the token1 a buyer would have to put in to drag the // price to the upper edge is the y-side of exactly these segments. That // number can be handed to the pool's own quoter, and the quoter's answer // has to come back as the token0 counted here. Nothing else in this file // has an independent oracle; this does. let L=b.L,cur=b.sqrtP; for(const t of inBand.filter(t=>t>b.tick)){ const s=Math.min(sqrtAtTick(t),b.sHi); const [x,y]=segAmounts(L,cur,s,b.sqrtP);a0+=x;a1+=y; inUp+=L*(s-cur); cur=s; if(cur>=b.sHi)break; L+=Number(net.get(t)||0n); } if(curt<=b.tick).sort((x,y)=>y-x)){ const s=Math.max(sqrtAtTick(t),b.sLo); const [x,y]=segAmounts(L,s,cur,b.sqrtP);a0+=x;a1+=y; inDown+=L*(1/s-1/cur); cur=s; if(cur<=b.sLo)break; L-=Number(net.get(t)||0n); } if(cur>b.sLo){ const [x,y]=segAmounts(L,b.sLo,cur,b.sqrtP);a0+=x;a1+=y; inDown+=L*(1/b.sLo-1/cur); } return {amount0:a0,amount1:a1,tick:b.tick,spacing:b.spacing, complete:!truncated[i],initialized_ticks:inBand.length, band_ticks:[b.tLo,b.tHi], // What it would take to walk the price to either edge, before the pool // fee is added on top of the input. Amount out is the holding on that // side, which is why it is not repeated here. to_upper_in1:inUp,to_lower_in0:inDown}; }); } // === CAN THIS TOKEN BE SOLD? === // The question every buyer has and no label answers. GoPlus does not analyse a // fresh token at all (measured 2026-09-02: none of 370 tokens tried came back // analysed), so the page answered "sellability not checked" exactly where it // mattered most. This asks the chain directly. // // HOW, WITHOUT SPENDING ANYTHING // eth_call accepts a state override: for the length of one call, a probe // address is given a token balance, an allowance to the PancakeSwap V2 router // and some BNB, and the router is asked to sell. The public BSC nodes honour // the override (all three tested on 2026-09-02). The balance and allowance // live in mappings whose storage slot differs per contract, so the slot is // found first by writing a value into candidate slots and reading balanceOf // and allowance back, one batched request each. A contract that stores // balances somewhere no candidate reaches (a proxy with a detached store, a // packed struct) reports "could not place a test balance", which is an honest // "not checked", never a "safe". // // WHAT A REVERT MEANS AND DOES NOT MEAN // A sell that reverts for a fresh address with a normal balance is what a // honeypot looks like from the outside. It is also what a token with a // max-wallet rule or a trading pause looks like, so the revert reason is // passed through and the chip says "reverted", not "scam". A sell that // succeeds is proof for THIS size at THIS block from an address with no // history; an owner can still flip a switch tomorrow, and the page says so. const V2_ROUTER='0x10ed43c718714eb63d5aa57b78b54704e256024e'; const SEL_SELL_FOT='0x791ac947', SEL_BUY_FOT='0xb6f9de95', SEL_BAL='0x70a08231', SEL_ALLOW='0xdd62ed3e'; const PROBE='0x0000000000000000000000000000000000c0ffee'; // A contract as the seller. The router's fee-on-transfer swap returns nothing, // so a plain address can only learn that a sell went through; a contract placed // at PROBE by the same state override reads back what arrived, and the tax is // arithmetic on that. Source: scripts/probe/SellProbe.sol; the bytes are // reproduced by scripts/probe/build-probe.mjs (--check compares them). // solc 0.8.36 · optimizer 200 · evm paris · 2121 bytes const SELL_PROBE_CODE='0x60806040526004361061002d5760003560e01c80634279a6a8146100395780637b213b6c1461007257600080fd5b3661003457005b600080fd5b34801561004557600080fd5b506100596100543660046104ee565b610085565b6040805192835260208301919091520160405180910390f35b610059610080366004610548565b610250565b6000808383600081811061009b5761009b61059b565b90506020020160208101906100b091906105b1565b60405163095ea7b360e01b81526001600160a01b03888116600483015260248201889052919091169063095ea7b3906044016020604051808303816000875af1158015610101573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061012591906105d3565b5060405163d06ca61f60e01b81526001600160a01b0387169063d06ca61f906101569088908890889060040161063d565b600060405180830381865afa158015610173573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f1916820160405261019b9190810190610676565b6101a6600185610759565b815181106101b6576101b661059b565b60209081029190910101519150476001600160a01b03871663791ac9478760008888306101e542610258610772565b6040518763ffffffff1660e01b815260040161020696959493929190610785565b600060405180830381600087803b15801561022057600080fd5b505af1158015610234573d6000803e3d6000fd5b5050505080476102449190610759565b91505094509492505050565b600080808484610261600182610759565b8181106102705761027061059b565b905060200201602081019061028591906105b1565b60405163d06ca61f60e01b81529091506001600160a01b0387169063d06ca61f906102b89034908990899060040161063d565b600060405180830381865afa1580156102d5573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f191682016040526102fd9190810190610676565b610308600186610759565b815181106103185761031861059b565b60209081029190910101516040516370a0823160e01b81523060048201529093506000906001600160a01b038316906370a0823190602401602060405180830381865afa15801561036d573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061039191906107c3565b90506001600160a01b03871663b6f9de953460008989306103b442610258610772565b6040518763ffffffff1660e01b81526004016103d49594939291906107dc565b6000604051808303818588803b1580156103ed57600080fd5b505af1158015610401573d6000803e3d6000fd5b50506040516370a0823160e01b81523060048201528493506001600160a01b03861692506370a082319150602401602060405180830381865afa15801561044c573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061047091906107c3565b61047a9190610759565b92505050935093915050565b80356001600160a01b038116811461049d57600080fd5b919050565b60008083601f8401126104b457600080fd5b50813567ffffffffffffffff8111156104cc57600080fd5b6020830191508360208260051b85010111156104e757600080fd5b9250929050565b6000806000806060858703121561050457600080fd5b61050d85610486565b935060208501359250604085013567ffffffffffffffff81111561053057600080fd5b61053c878288016104a2565b95989497509550505050565b60008060006040848603121561055d57600080fd5b61056684610486565b9250602084013567ffffffffffffffff81111561058257600080fd5b61058e868287016104a2565b9497909650939450505050565b634e487b7160e01b600052603260045260246000fd5b6000602082840312156105c357600080fd5b6105cc82610486565b9392505050565b6000602082840312156105e557600080fd5b815180151581146105cc57600080fd5b81835260208301925060008160005b84811015610633576001600160a01b0361061d83610486565b1686526020958601959190910190600101610604565b5093949350505050565b8381526040602082015260006106576040830184866105f5565b95945050505050565b634e487b7160e01b600052604160045260246000fd5b60006020828403121561068857600080fd5b815167ffffffffffffffff81111561069f57600080fd5b8201601f810184136106b057600080fd5b805167ffffffffffffffff8111156106ca576106ca610660565b8060051b604051601f19603f830116810181811067ffffffffffffffff821117156106f7576106f7610660565b60405291825260208184018101929081018784111561071557600080fd5b6020850194505b838510156107385784518082526020958601959093500161071c565b509695505050505050565b634e487b7160e01b600052601160045260246000fd5b8181038181111561076c5761076c610743565b92915050565b8082018082111561076c5761076c610743565b86815285602082015260a0604082015260006107a560a0830186886105f5565b6001600160a01b039490941660608301525060800152949350505050565b6000602082840312156107d557600080fd5b5051919050565b8581526080602082015260006107f66080830186886105f5565b6001600160a01b039490941660408301525060600152939250505056fea26469706673582212207c8527339b57accc0677cb1e32c5eb3cebcdfa73637ae8a8847fd653e60ac77a64736f6c63430008240033'; const SEL_PROBE_SELL='0x4279a6a8', SEL_PROBE_BUY='0x7b213b6c'; const CALLER='0x000000000000000000000000000000000000beef'; // The token amount that must have reached a PancakeSwap V2 pair for it to pay // `received` of the quote side, from the constant product with the 0.25% fee // (getAmountIn without its +1). What was sent minus what arrived is the tax. // Exported so the checker can pin the arithmetic without a node in the loop. export function sellTaxFromReceived(reserveTok,reserveQ,amount,received){ if(!(reserveQ>received)||!(amount>0n)||!(received>0n))return null; const arrived=(reserveTok*received*10000n)/((reserveQ-received)*9975n); const t=1-Number(arrived)/Number(amount); return t<0?0:Math.min(t,1); } const pad32=v=>(typeof v==='bigint'?v.toString(16):String(v).replace(/^0x/,'')).padStart(64,'0'); const hexToBytes=h=>{const s=h.replace(/^0x/,'');const a=new Uint8Array(s.length/2);for(let i=0;in?(((x<>BigInt(64-n)))&M):x; const st=new Array(25).fill(0n); const rate=136; const msg=new Uint8Array(Math.ceil((bytes.length+1)/rate)*rate); msg.set(bytes); msg[bytes.length]^=0x01; msg[msg.length-1]^=0x80; for(let off=0;off=0;b--)w=(w<<8n)|BigInt(msg[off+i*8+b]);st[i]^=w} for(let r=0;r<24;r++){ const C=[0,1,2,3,4].map(x=>st[x]^st[x+5]^st[x+10]^st[x+15]^st[x+20]); const D=[0,1,2,3,4].map(x=>C[(x+4)%5]^rot(C[(x+1)%5],1)); for(let i=0;i<25;i++)st[i]^=D[i%5]; const B=new Array(25); for(let x=0;x<5;x++)for(let y=0;y<5;y++)B[y+5*((2*x+3*y)%5)]=rot(st[x+5*y],ROT[x][y]); for(let x=0;x<5;x++)for(let y=0;y<5;y++)st[x+5*y]=B[x+5*y]^((~B[(x+1)%5+5*y])&B[(x+2)%5+5*y]); st[0]^=RC[r]; } } let out='';for(let i=0;i<4;i++){let w=st[i];for(let b=0;b<8;b++){out+=Number(w&0xffn).toString(16).padStart(2,'0');w>>=8n}} return out; } export const keccakHex=hex=>'0x'+keccak256(hexToBytes(hex)); async function postRaw(url,body){ const r=await fetch(url,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body),signal:AbortSignal.timeout(15000)}); if(!r.ok)throw new Error('http '+r.status);return r.json(); } // The batched slot search is forty calls in one request, and a public node // under load answers that with 429 (seen from the worker on the first live // run). Every endpoint in the pool is tried before the simulation gives up, // and a node that answered once is kept for the rest of this simulation. async function postAny(body,pref){ const order=[pref,...RPCS.filter(u=>u!==pref)].filter(Boolean); let last=null; for(const u of order){try{const j=await postRaw(u,body);if(Array.isArray(j)||(j&&!j.error))return {url:u,j}}catch(e){last=e}} throw last||new Error('no endpoint answered'); } // A slot search is only conclusive when the node answered EVERY candidate // without an error. A throttled node answers a batch with some entries as // "rate limit" — and a search that read that as "no slot matched" reported // a perfectly ordinary token as "non-standard storage" on the second call // of the first live run. Errors inside the batch mean: ask the next node. async function searchSlot(calls,amount,pref){ const order=[pref,...RPCS.filter(u=>u!==pref)].filter(Boolean); let last=null; for(const u of order){ let j=null;try{j=await postRaw(u,calls)}catch(e){last=e;continue} if(!Array.isArray(j)){last=new Error('not a batch');continue} const hit=j.find(x=>x.result&&x.result!=='0x'&&BigInt(x.result)===amount); if(hit)return {url:u,hit}; if(j.some(x=>x.error)||j.length0n?reserveTok/1000n:1n; const amtHex='0x'+pad32(amount); const probeKey=pad32(PROBE); const balCalls=[],balKeys=[]; for(let slot=0;slot<40;slot++){const k=keccakHex(probeKey+pad32(BigInt(slot)));balKeys.push(k); balCalls.push({jsonrpc:'2.0',id:slot,method:'eth_call',params:[{to:token,data:SEL_BAL+probeKey},'latest',{[token]:{stateDiff:{[k]:amtHex}}}]})} // Twelve candidates first: nearly every ERC-20 keeps balances in one of // the first slots, and a batch a third the size is a batch a public node // answers under load. The long tail is only asked when the short one misses. let b1=await searchSlot(balCalls.slice(0,12),amount,url); if(!b1.hit)b1=await searchSlot(balCalls.slice(12),amount,b1.url); const node=b1.url;const balHit=b1.hit; if(!balHit)return {ok:false,reason:'could not place a test balance in this contract (non-standard storage) — not checked, not cleared'}; const balKey=balKeys[balHit.id]; const size_note='one part in a thousand of the pair\'s token reserve, sold from a fresh address with no history'; // 1. The probe: a contract at PROBE sells and buys, and reports what // arrived. That is the sell test AND the tax, in one call each. const pr=await probeRoundTrip(token,amount,amtHex,balKey,reserveTok,reserveQ,node); if(pr.supported&&pr.sell.ok&&pr.buy.ok){ return {ok:true,sellable:true,buyable:true,sell_error:null,buy_error:null,amount:amount.toString(),size_note, tax:{sell_pct:pr.sellTax==null?null:+(pr.sellTax*100).toFixed(2),buy_pct:pr.buyTax==null?null:+(pr.buyTax*100).toFixed(2), method:'simulated at this block: what the pair would pay for the whole amount against what arrived after the transfer, from a fresh address with no history'}, source:'eth_call with a state override on the PancakeSwap V2 router, at this block; the seller is a contract placed at a fresh address so what came back could be read'}; } // 2. A revert with a contract as the seller is not yet a verdict: a // "no contracts may trade" rule refuses it and a plain wallet sails // through. So the same sell is asked from a plain address before anything // is called refused — and when the node does not support code overrides at // all, the plain address is simply the only path. const plain=await plainRoundTrip(token,amount,amtHex,balKey,node); if(!plain.ok)return pr.supported ? {ok:true,sellable:pr.sell.ok,buyable:pr.buy.ok,sell_error:pr.sell.error,buy_error:pr.buy.error,amount:amount.toString(),size_note,tax:null, source:'eth_call with a state override on the PancakeSwap V2 router, at this block; the seller was a contract at a fresh address'} : plain; if(pr.supported&&plain.sellable&&!pr.sell.ok){ return {...plain,tax:null,contract_refused:pr.sell.error, note:'A plain wallet sells; a contract as the seller was refused ('+pr.sell.error+'). That is what an anti-bot rule looks like, and it means the tax could not be measured by simulation.'}; } return {...plain,tax:null}; }catch(e){return {ok:false,reason:'the simulation could not run: '+String(e.message||e).slice(0,80)}} } async function probeRoundTrip(token,amount,amtHex,balKey,reserveTok,reserveQ,node){ const override={[token]:{stateDiff:{[balKey]:amtHex}},[PROBE]:{code:SELL_PROBE_CODE,balance:'0x'+pad32(10n**18n)},[CALLER]:{balance:'0x'+pad32(10n**18n)}}; const sellData=SEL_PROBE_SELL+pad32(V2_ROUTER)+pad32(amount)+pad32(0x60n)+pad32(2n)+pad32(token)+pad32(WBNB); const buyData=SEL_PROBE_BUY+pad32(V2_ROUTER)+pad32(0x40n)+pad32(2n)+pad32(WBNB)+pad32(token); const calls=[ {jsonrpc:'2.0',id:1,method:'eth_call',params:[{from:CALLER,to:PROBE,data:sellData,gas:'0x1e8480'},'latest',override]}, {jsonrpc:'2.0',id:2,method:'eth_call',params:[{from:CALLER,to:PROBE,data:buyData,value:'0x'+pad32(10n**16n),gas:'0x1e8480'},'latest',override]}, ]; let sell=null,buy=null; for(const u of [node,...RPCS.filter(x=>x!==node)]){ let out=null;try{out=await postRaw(u,calls)}catch(e){continue} if(!Array.isArray(out))continue; const s1=out.find(x=>x.id===1),b1=out.find(x=>x.id===2); const settled=x=>x&&(!x.error||isRevert(x.error)); if(settled(s1)&&settled(b1)){sell=s1;buy=b1;break} } if(!sell||!buy)return {supported:false}; const two=h=>{const x=String(h||'').replace(/^0x/,'');return x.length>=128?[BigInt('0x'+x.slice(0,64)),BigInt('0x'+x.slice(64,128))]:null}; const sv=sell.error?null:two(sell.result),bv=buy.error?null:two(buy.result); // A result that decodes to nothing is a node that ran the call without the // code override and returned empty — that is "unsupported", not "sold". if((!sell.error&&!sv)||(!buy.error&&!bv))return {supported:false}; const sellTax=sv?sellTaxFromReceived(reserveTok,reserveQ,amount,sv[1]):null; const buyTax=bv&&bv[0]>0n?Math.max(0,Math.min(1,1-Number(bv[1])/Number(bv[0]))):null; return {supported:true, sell:{ok:!sell.error,error:sell.error?revertText(sell.error):null,quoted:sv?sv[0].toString():null,received:sv?sv[1].toString():null}, buy:{ok:!buy.error,error:buy.error?revertText(buy.error):null,quoted:bv?bv[0].toString():null,received:bv?bv[1].toString():null}, sellTax,buyTax}; } // The plain-address path: the original simulation. It needs the allowance // placed by storage override too, since a wallet cannot approve inside an // eth_call, and it learns only whether the router accepted the sell. async function plainRoundTrip(token,amount,amtHex,balKey,node){ try{ const probeKey=pad32(PROBE), routerKey=pad32(V2_ROUTER); const alCalls=[],alKeys=[]; for(let slot=0;slot<40;slot++){const inner=keccakHex(probeKey+pad32(BigInt(slot)));const k=keccakHex(routerKey+inner.slice(2));alKeys.push(k); alCalls.push({jsonrpc:'2.0',id:slot,method:'eth_call',params:[{to:token,data:SEL_ALLOW+probeKey+routerKey},'latest',{[token]:{stateDiff:{[k]:amtHex}}}]})} let a1=await searchSlot(alCalls.slice(0,12),amount,node); if(!a1.hit)a1=await searchSlot(alCalls.slice(12),amount,a1.url); node=a1.url;const alHit=a1.hit; if(!alHit)return {ok:false,reason:'could not place a test allowance in this contract — not checked, not cleared'}; const alKey=alKeys[alHit.id]; const deadline=pad32(BigInt(Math.floor(Date.now()/1000)+600)); const override={[token]:{stateDiff:{[balKey]:amtHex,[alKey]:amtHex}},[PROBE]:{balance:'0x'+pad32(10n**18n)}}; const sellData=SEL_SELL_FOT+pad32(amount)+pad32(0n)+pad32(0xa0n)+probeKey+deadline+pad32(2n)+pad32(token)+pad32(WBNB); const buyData=SEL_BUY_FOT+pad32(0n)+pad32(0x80n)+probeKey+deadline+pad32(2n)+pad32(WBNB)+pad32(token); const calls=[ {jsonrpc:'2.0',id:1,method:'eth_call',params:[{from:PROBE,to:V2_ROUTER,data:sellData,gas:'0x1e8480'},'latest',override]}, {jsonrpc:'2.0',id:2,method:'eth_call',params:[{from:PROBE,to:V2_ROUTER,data:buyData,value:'0x'+pad32(10n**16n),gas:'0x1e8480'},'latest',override]}, ]; // Every node in turn until both answers are either a result or a revert. let sell=null,buy=null; for(const u of [node,...RPCS.filter(x=>x!==node)]){ let out=null;try{out=await postRaw(u,calls)}catch(e){continue} if(!Array.isArray(out))continue; const s1=out.find(x=>x.id===1),b1=out.find(x=>x.id===2); const settled=x=>x&&(!x.error||isRevert(x.error)); if(settled(s1)&&settled(b1)){sell=s1;buy=b1;break} } if(!sell||!buy)return {ok:false,reason:'every node refused the simulation call (rate limit or unsupported) — not checked, not cleared'}; const sellOk=!sell.error,buyOk=!buy.error; return {ok:true,sellable:sellOk,buyable:buyOk, sell_error:sellOk?null:revertText(sell&&sell.error),buy_error:buyOk?null:revertText(buy&&buy.error), amount:amount.toString(), size_note:'one part in a thousand of the pair\'s token reserve, sold from a fresh address with no history', source:'eth_call with a state override on the PancakeSwap V2 router, at this block'}; }catch(e){return {ok:false,reason:'the simulation could not run: '+String(e.message||e).slice(0,80)}} } // ---- four.meme: a token still on its launch curve --------------------------- // A four.meme token has no PancakeSwap pool until its raise completes; until // then every trade goes through four.meme's TokenManager, and a scanner that // only knows pools said "no pool" about a token that trades all day. The // platform's own helper contract answers the three questions this page asks of // a pool — what is it worth, what does a trade cost, can you sell it — with one // view call each: getTokenInfo(address), tryBuy(address,uint256,uint256), // trySell(address,uint256). Prices come back as quote-wei per whole token; the // fee rate is in basis points of 1e4 (100 = 1 %). tryBuy's estimatedCost plus // estimatedFee is the money in; trySell's funds is the money out AFTER its fee // (checked on a live curve: fee was 1 % of funds+fee, not of funds). export const FOURMEME_HELPER='0xf251f83e40a78868fcfa3fa4599dad6494e46034'; export const FOURMEME_MANAGER='0x5c952063c7fc8610ffdb798152d69f0b9550762b'; const CURVE_SEL={info:'0x1f69565f',tryBuy:'0xe21b103a',trySell:'0xc6f43e8c'}; const wordAt=(h,i)=>h&&h.length>=66+64*i?hx('0x'+h.slice(2+64*i,66+64*i)):null; const addrWord=(h,i)=>h&&h.length>=66+64*i?addrAt('0x'+h.slice(2+64*i,66+64*i)):null; // null when the address is not a four.meme token at all (the helper returns a // zero version), an object otherwise — including for tokens that have long // since graduated, where `liquidityAdded` is true and the pool path applies. export async function curveInfo(token,url){ let r; try{r=(await rpcBatch([call(FOURMEME_HELPER,CURVE_SEL.info+pad(token))],url))[0]} catch(e){return null} const version=Number(wordAt(r,0)||0n); if(!version)return null; const quoteAddr=addrWord(r,2); const known=QUOTES.find(([a])=>a===quoteAddr); const isBnb=!quoteAddr||quoteAddr===NULLA||quoteAddr===WBNB; const offers=Number(wordAt(r,7))/1e18, maxOffers=Number(wordAt(r,8))/1e18; const funds=Number(wordAt(r,9))/1e18, maxRaising=Number(wordAt(r,10))/1e18; const launch=Number(wordAt(r,6)||0n); return { version, manager:addrWord(r,1), quoteAddr:isBnb?null:quoteAddr, quoteSym:isBnb?'BNB':known?known[1]:null, quoteIsStable:!isBnb&&!!known&&known[2]===1, // quote per whole token, as a plain number price:Number(wordAt(r,3))/1e18, feePct:Number(wordAt(r,4)||0n)/100, launchTime:launch>0?launch:null, offersLeft:offers, maxOffers, raised:funds, maxRaising, progressPct:maxRaising>0?Math.min(100,funds/maxRaising*100):null, liquidityAdded:(wordAt(r,11)||0n)!==0n, }; } // What a buy and a sell of each USD size would cost on the curve right now, // against the curve's own last price, fee included — the same definition the // pool ladder uses, so the two are comparable. A size the curve cannot fill // (more than is left to raise) comes back with null costs rather than a // number that describes a trade nobody could place. export async function curveLadder(token,info,quoteUsd,sizesUsd,url){ if(!(quoteUsd>0)||!(info.price>0))return []; const sizes=sizesUsd.filter(s=>s>0); const calls=[]; for(const usd of sizes){ const fundsWei=BigInt(Math.floor(usd/quoteUsd*1e18)); const tokensWei=BigInt(Math.floor(usd/quoteUsd/info.price*1e18)); calls.push(call(FOURMEME_HELPER,CURVE_SEL.tryBuy+pad(token)+num(0)+num(fundsWei))); calls.push(call(FOURMEME_HELPER,CURVE_SEL.trySell+pad(token)+num(tokensWei))); } let res; try{res=await rpcBatch(calls,url)}catch(e){return []} const rows=[]; sizes.forEach((usd,i)=>{ const b=res[2*i],s=res[2*i+1]; const row={usd,buyCost:null,sellCost:null,buyNote:null,sellNote:null}; const paid=usd/quoteUsd; const got=b?Number(wordAt(b,2))/1e18:0; const cost=b?Number(wordAt(b,3))/1e18:0, fee=b?Number(wordAt(b,4))/1e18:0; // The helper fills what it can: a buy that would overshoot the raise is // capped at what is left, and the money it would actually take (cost plus // fee) is then less than the money offered. Checked live: a $2,500 buy // against $1,300 left came back "cheaper" than a $1,000 one, because the // cost was measured on the capped part only. Such a row says so instead. const inPaid=cost+fee; if(b&&got>0&&inPaid>=paid*0.999){ row.buyCost=(inPaid/(got*info.price)-1)*100; }else if(b&&got>0){ row.buyNote='more than the curve has left to sell'; }else row.buyNote='the curve did not quote this size'; const tokens=paid/info.price; const out=s?Number(wordAt(s,2))/1e18:0; if(s&&out>0)row.sellCost=(1-out/(tokens*info.price))*100; else row.sellNote='the curve did not quote this size'; rows.push(row); }); return rows; } // What is trading on four.meme's curve right now, straight from the manager's // own logs: the most recently traded tokens that have not graduated, each with // its raise, price and what a $100 buy and sell would cost. Nobody shows the // cost of a curve trade before there is a pool; this is the list a person // wants before pressing buy on a launch. Runs in the browser, on demand — a // few dozen small reads against the public log nodes, which cap eth_getLogs // at a handful of blocks per call, hence the windows. No event signature is // assumed: every log the manager writes about a token carries that token's // address in its first data word, and that is all this reads. export async function curveFeed({blocks=300,window=10,max=8,quoteUsd={bnb:0},sizeUsd=100}={}){ const url=LOGS_RPCS[0]; const head=Number(await rpc('eth_blockNumber',[],url)); const windows=[]; for(let to=head;to>head-blocks;to-=window)windows.push([to-window+1,to]); const logs=(await Promise.all(windows.map(([from,to])=> rpc('eth_getLogs',[{address:FOURMEME_MANAGER,fromBlock:'0x'+from.toString(16),toBlock:'0x'+to.toString(16)}],url).catch(()=>[]) ))).flat().filter(l=>Array.isArray(l.topics)&&l.data&&l.data.length>=130); // Newest first; one entry per token, the block it was last seen in. logs.sort((a,b)=>Number(b.blockNumber)-Number(a.blockNumber)||Number(b.logIndex)-Number(a.logIndex)); const seen=new Map(); for(const l of logs){ const t=addrAt('0x'+l.data.slice(2,66)); if(!t||t===NULLA||seen.has(t))continue; seen.set(t,{token:t,lastBlock:Number(l.blockNumber),trades:1}); if(seen.size>=max*3)break; } for(const l of logs){const t=addrAt('0x'+l.data.slice(2,66));const e=t&&seen.get(t);if(e&&e.lastBlock!==Number(l.blockNumber))e.trades++;} const out=[]; for(const e of seen.values()){ if(out.length>=max)break; const cv=await curveInfo(e.token); if(!cv||cv.liquidityAdded)continue; const q=cv.quoteSym==='BNB'?quoteUsd.bnb:cv.quoteIsStable?1:0; let symbol=null; try{const s=await rpcBatch([call(e.token,S.symbol)]);symbol=decStr(s[0])||null}catch(err){} const rows=q>0?await curveLadder(e.token,cv,q,[sizeUsd]).catch(()=>[]):[]; const r=rows[0]||{}; out.push({token:e.token,symbol,lastBlock:e.lastBlock,blocksAgo:head-e.lastBlock,quoteSym:cv.quoteSym, raised:cv.raised,maxRaising:cv.maxRaising,progressPct:cv.progressPct,priceUsd:q>0?cv.price*q:null, buyCost:r.buyCost??null,sellCost:r.sellCost??null,buyNote:r.buyNote||null,feePct:cv.feePct}); } return {head,blocks,tokensSeen:seen.size,list:out}; } ============================================================================== === FILE: dashboard/scanner-scan.js ============================================================================== // Pool scan for BNB Smart Chain, as data — the same measurements the browser // scanner draws on a page, returned as JSON instead. // // THIS FILE IS THE ONE IMPLEMENTATION. Three surfaces import it and none of // them owns a second copy: // the installable skill (skills/bsc-pool-depth, pulled at build time) // the MCP tool (bsc_pool_scan, in dashboard/_worker.js) // the browser scanner (shares scanner-chain.js, the layer below this one) // A fee table or an impact formula that exists twice drifts, and a drifted cost // column is worse than no cost column. If a figure needs changing, it changes // here and everywhere at once. // // Nothing in this file computes a figure. It decides which figures to ask for, // applies the guards that decide whether a pool is worth quoting at all, and // assembles the answer. The arithmetic lives one layer down in // scanner-chain.js, which the browser page loads directly. import { WBNB, BNB_PAIR, DEAD, NULLA, QUOTES, SEL as S, GOPLUS, GOPLUS_TOKEN, balOf, call, hx, addrAt, res2, decStr, rpcBatch, classify, priceToken, discover, ladderV2, onePctV2, ladderV3, onePctV3, measureTax, venues, simulateRoundTrip, STEPS, curveInfo, curveLadder, } from './scanner-chain.js'; const parseInput = (s) => { const m = String(s || '').match(/0x[a-fA-F0-9]{40}/); return m ? m[0].toLowerCase() : null; }; export class ScanError extends Error { constructor(headline, detail) { super(headline); this.headline = headline; this.detail = detail; } } // GoPlus describes contract properties no eth_call reveals (mintable, proxy, // LP lockers). It is asked, always attributed, and never allowed to override a // figure that was measured on-chain. // // The explicit user-agent and timeout are not decoration. From a Cloudflare // Worker the bare call came back with nothing, and because a missing answer // used to read as "no tax", BOBAI was reported at 0% when it charges 3%. The // tax field now says "unknown" rather than zero in that case — this makes the // case rarer as well as harmless. // Asked twice, with a different presentation each time. The plain call works // from a browser and from Node and comes back with nothing from a Cloudflare // Worker, which is where the MCP tool runs — so the second attempt announces // itself as an ordinary client instead. One extra outbound call, and only when // the first one failed. // Why it failed is recorded, not swallowed. "unavailable" is a dead end for // whoever has to fix it; "HTTP 403" and "timed out" point at completely // different causes, and the difference decides whether a retry, a header or an // account is the answer. const gpWhy = { reason: null }; const goPlusOnce = async (a, init) => { try { const r = await fetch(GOPLUS + a, { ...init, signal: AbortSignal.timeout(7000) }); if (!r.ok) { gpWhy.reason = 'HTTP ' + r.status; return null; } const j = await r.json(); const hit = j && j.result && (j.result[a] || j.result[a.toLowerCase()]); // GoPlus answers 200 and puts the real outcome in `code`. Its own message // is repeated verbatim rather than paraphrased: the first version of this // guessed "no entry for this token" from an empty result, when what the // service actually said was that the request quota was gone. Those call for // opposite responses, and inventing the wrong one wasted an investigation. if (!hit) { gpWhy.reason = j && j.code != null ? 'answered code ' + j.code + (j.message ? ' (' + String(j.message).slice(0, 60) + ')' : '') : 'answered with no entry'; } return hit || null; } catch (e) { gpWhy.reason = /abort|timeout/i.test(String(e && e.name) + String(e && e.message)) ? 'timed out' : 'connection failed'; return null; } }; // Edge cache, and only for answers that worked. // // The quota that runs out is attached to the caller's IP, and on Cloudflare // that IP is shared with every other Worker in the world — so from there GoPlus // returns "rate limit" while the identical request from a laptop returns the // data. Caching a success for six hours means the second, tenth and hundredth // scan of the same token get the properties even when a fresh request would be // refused. It does not fix the first scan of a token nobody has asked about, // and nothing short of an account key will. // // Failures are deliberately NOT cached: storing "rate limited" would turn a // temporary refusal into six hours of certain refusal. const CACHE_SECONDS = 21600; const gpCached = async (a, fetcher) => { const store = (typeof caches !== 'undefined' && caches.default) ? caches.default : null; if (!store) return fetcher(); const key = new Request('https://goplus-cache.brainonbnb.com/' + a); try { const hit = await store.match(key); if (hit) return await hit.json(); } catch { /* a broken cache must never break the scan */ } const fresh = await fetcher(); if (fresh) { try { await store.put(key, new Response(JSON.stringify(fresh), { headers: { 'content-type': 'application/json', 'cache-control': 'max-age=' + CACHE_SECONDS }, })); } catch { /* same */ } } return fresh; }; // --- account key ----------------------------------------------------------- // Anonymous requests share a quota tied to the caller's IP, and a Worker's IP // belongs to all of Cloudflare, so the quota is usually spent before we ask. // Measured 2026-08-22 over eight tokens not in the edge cache: two answered, // six came back "code 4029 (too many requests)". It is not a hard wall, which // is worse than one — the contract section appears for some visitors and not // others, on the same token, for no reason anybody can see. An account key // moves the quota onto us and is the only thing that fixes it. // // Key set 2026-08-23, and the same eight tokens then answered eight of eight. // Note that the key only takes effect after a Pages deployment: uploading the // secret alone left the running deployment anonymous and still at 4029. // // The secret stays in the Worker. It is not handed to the browser page and not // to the packaged skill — both run on somebody else's machine, and a key in a // downloadable bundle is a published key. A caller that passes no env stays // anonymous and behaves exactly as before. // Web Crypto, which the Worker and any Node 19+ have. Older runtimes reach this // only through the packaged skill, and there the honest outcome is to stay // anonymous rather than to throw in the middle of a scan that otherwise works. const sha1Hex = async (s) => { if (typeof crypto === 'undefined' || !crypto.subtle) return null; const b = await crypto.subtle.digest('SHA-1', new TextEncoder().encode(s)); return [...new Uint8Array(b)].map((x) => x.toString(16).padStart(2, '0')).join(''); }; // sign = sha1(app_key + time + app_secret). Exported so scripts/goplus-check.mjs // can hold it against GoPlus's own worked example rather than trusting that the // concatenation order was read correctly. export const goPlusSign = (key, time, secret) => sha1Hex(`${key}${time}${secret}`); // One token per isolate, renewed a minute before it lapses. `inflight` matters: // several scans can land at once on a cold isolate, and without it each would // fetch its own token. let gpTok = { value: null, expires: 0, inflight: null }; const goPlusToken = (env) => { if (!env || !env.GOPLUS_APP_KEY || !env.GOPLUS_APP_SECRET) return Promise.resolve(null); const now = () => Math.floor(Date.now() / 1000); if (gpTok.value && gpTok.expires > now() + 60) return Promise.resolve(gpTok.value); if (gpTok.inflight) return gpTok.inflight; gpTok.inflight = (async () => { try { const time = now(); const sign = await goPlusSign(env.GOPLUS_APP_KEY, time, env.GOPLUS_APP_SECRET); if (!sign) { gpWhy.reason = 'no SHA-1 available in this runtime'; return null; } const r = await fetch(GOPLUS_TOKEN, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ app_key: env.GOPLUS_APP_KEY, sign, time }), signal: AbortSignal.timeout(7000), }); const j = await r.json().catch(() => null); const tok = j && j.result && j.result.access_token; if (!tok) { // Said plainly, because a wrong key and a reachable-but-refusing service // need different fixes and both otherwise show up as "unavailable". gpWhy.reason = 'account key rejected: ' + (j && j.code != null ? 'code ' + j.code + (j.message ? ' (' + String(j.message).slice(0, 60) + ')' : '') : 'HTTP ' + r.status); return null; } gpTok.value = tok; gpTok.expires = now() + Math.max(60, Number(j.result.expires_in) || 3600); return tok; } catch { gpWhy.reason = 'account key request failed'; return null; } finally { gpTok.inflight = null; } })(); return gpTok.inflight; }; const askGoPlus = (a, env) => gpCached(a, async () => { const tok = await goPlusToken(env); // With a key, one request is the whole story: a refusal is then about the // account, and repeating it dressed as a browser only blurs which of the two // paths failed. Without a key, the second attempt stays — it used to help. if (tok) return (await goPlusOnce(a, { headers: { authorization: tok } })) || null; return (await goPlusOnce(a, {})) || (await goPlusOnce(a, { headers: { accept: 'application/json, text/plain, */*', 'accept-language': 'en-US,en;q=0.9', 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', }, })) || null; }); // `env` is optional and only ever carries the GoPlus account key. The Worker // passes it; the browser page and the packaged skill call scan(input) with one // argument and keep running anonymously. export async function scan(input, env) { // Lowercased at the door, for the reason spelled out in tier-scan.js: every // address comparison downstream is a string comparison against a value that // came back from `addrAt`, which is lowercase. A checksummed address does not // error, it just matches nothing — and a scan that matches nothing still // returns a full, confident, wrong answer. input = String(input || '').toLowerCase(); // Fired against the input on the chance it IS the token, because it usually // is and this is the slow leg. If the input turns out to be a pool, the // answer describes the LP token instead, so it is asked again against the // real token once that is known and this first answer dropped. let gpP = askGoPlus(input, env); let what; try { what = await classify(input); } catch { throw new ScanError( 'The chain did not answer.', 'The public BSC node refused or timed out. Nothing is cached here, so a retry in a few seconds usually works.', ); } let token, pool = null, tokDec, bnbUsd, hop, deeper = null; const base = await rpcBatch([call(BNB_PAIR, S.reserves), call(BNB_PAIR, S.token0)]); const br = res2(base[0]); const bIs0 = addrAt(base[1]) === WBNB; bnbUsd = br ? (bIs0 ? br[1] / br[0] : br[0] / br[1]) : 0; if (!(bnbUsd > 0)) throw new ScanError( 'Could not price BNB.', 'The reference pool read back empty, so nothing could be stated in dollars.', ); if (what.kind === 'v2pair' || what.kind === 'v3pool') { // A pasted pool tells us the venue directly. Which side is "the token" is // then the only open question: it is the side that is not the quote, and // the quote is whichever side can be priced. const [a, b] = [what.token0, what.token1]; const qa = QUOTES.find(([x]) => x === a); const qb = QUOTES.find(([x]) => x === b); if (qa && !qb) token = b; else if (qb && !qa) token = a; else if (qa && qb) token = a; else { const pa = await priceToken(a, bnbUsd); const pb = await priceToken(b, bnbUsd); token = pb.usd != null && pa.usd == null ? a : pa.usd != null && pb.usd == null ? b : (pb.hopBnb || 0) >= (pa.hopBnb || 0) ? a : b; } const quote = token === a ? b : a; const info = await rpcBatch([call(token, S.decimals), call(token, S.symbol), call(token, S.name)]); tokDec = Number(hx(info[0])) || 18; hop = await priceToken(quote, bnbUsd); if (hop.usd == null) throw new ScanError( 'That pool cannot be priced.', 'It trades against a token with no BNB pool of its own — so there is no way to express its depth in dollars without inventing one.', ); if (what.kind === 'v2pair' && !what.venue) throw new ScanError( 'That pool is on a venue this tool does not price.', 'Its factory is not one of the constant-product venues whose swap fee has been derived and verified here (PancakeSwap V2, Uniswap V2, Biswap). Applying somebody else’s fee would quietly understate what a trade costs, so no figures are shown.', ); pool = what.kind === 'v2pair' ? { kind: 'v2', pair: input, quote, sym: hop.sym, usd: hop.usd, fee: what.venue.fee, venue: what.venue.name, factory: what.factory, tok: (addrAt(what.token0) === token ? what.reserves[0] : what.reserves[1]) / Math.pow(10, tokDec), q: (addrAt(what.token0) === token ? what.reserves[1] : what.reserves[0]) / 1e18, } : { kind: 'v3', pair: input, quote, sym: hop.sym, usd: hop.usd, fee: what.fee / 1e6, feeRaw: what.fee, sqrt: what.sqrt, tokenIs0: what.token0 === token, }; if (pool.kind === 'v3') { const bal = await rpcBatch([call(quote, balOf(input)), call(token, balOf(input))]); pool.q = Number(hx(bal[0])) / 1e18; pool.tok = Number(hx(bal[1])) / Math.pow(10, tokDec); } if (token !== input) gpP = askGoPlus(token, env); // A pasted pool is honoured — you asked about that one. But the factories // are still asked what else exists, because a link often points at a side // pool while the real depth sits one fee tier over. try { const alt = (await discover(token, tokDec, bnbUsd)).find( (c) => c.pair.toLowerCase() !== pool.pair.toLowerCase() && c.hard > (pool.q || 0) * pool.usd * 1.15, ); if (alt) deeper = alt; } catch { /* a missing alternative is not a failed scan */ } } else { token = input; const info = await rpcBatch([call(token, S.decimals), call(token, S.symbol), call(token, S.name)]); tokDec = Number(hx(info[0])) || 18; const cands = await discover(token, tokDec, bnbUsd); pool = cands[0] || null; hop = { direct: true, sym: pool ? pool.sym : 'BNB' }; if (pool && pool.kind === 'v3') { const s = await rpcBatch([call(pool.pair, S.slot0), call(pool.pair, S.token0)]); pool.sqrt = hx('0x' + s[0].slice(2, 66)); pool.tokenIs0 = addrAt(s[1]) === token; } } const gp = (await gpP) || {}; const gpOk = !!(gp.token_name || gp.dex || gp.is_open_source != null); const nameInfo = await rpcBatch([ call(token, S.symbol), call(token, S.name), call(token, S.totalSupply), call(token, balOf(DEAD)), call(token, balOf(NULLA)), ]); const symb = (gp.token_symbol || decStr(nameInfo[0]) || '?').trim().slice(0, 16); const name = (gp.token_name || decStr(nameInfo[1]) || 'Unknown token').trim().slice(0, 60); const supply = nameInfo[2] ? Number(hx(nameInfo[2])) / Math.pow(10, tokDec) : null; const burned = (Number(hx(nameInfo[3])) + Number(hx(nameInfo[4]))) / Math.pow(10, tokDec); // Venues from DexScreener, which indexes the small DEXes; GoPlus's list is // the fallback and only covers what it happens to know. const dsAll = await venues(token); const others = dsAll ? dsAll .filter((x) => !pool || x.pair !== pool.pair.toLowerCase()) .map((x) => ({ pair: x.pair, name: x.name + (x.quote ? ' · ' + x.quote : ''), liquidity: x.liq })) : (gp.dex || []) .filter((x) => x.pair && (!pool || x.pair.toLowerCase() !== pool.pair.toLowerCase())) .map((x) => ({ pair: x.pair, name: x.name || x.liquidity_type || 'Unknown', liquidity: parseFloat(x.liquidity) || 0 })) .sort((a, b) => b.liquidity - a.liquidity); const otherLiq = others.reduce((s, x) => s + (x.liquidity || 0), 0); const hard = pool ? (pool.q || 0) * pool.usd : 0; // "Is the pool I can measure representative?" — and the comparison must use // ONE yardstick. Weighing our own one-sided figure against a two-sided one // makes every V3 pool look like a rounding error, so when the index has a // figure for OUR pool, both sides of the ratio come from the index. const mineFrom = (list) => { if (!list || !pool) return null; const e = list.find((x) => (x.pair || '').toLowerCase() === pool.pair.toLowerCase()); return e ? (e.liq != null ? e.liq : parseFloat(e.liquidity) || 0) : null; }; const mine = mineFrom(dsAll) != null ? mineFrom(dsAll) : mineFrom(gp.dex); const share = !pool ? 0 : mine != null && mine + otherLiq > 0 ? mine / (mine + otherLiq) : otherLiq > 0 ? hard / (hard + otherLiq) : 1; if (!pool && !others.length && !gpOk && !(supply > 0) && !decStr(nameInfo[0])) throw new ScanError( 'That address is not a BSC token.', 'It answers nothing to symbol() or totalSupply(), has no pool at any venue this tool can read, and GoPlus does not list it. A wallet address, or a contract that is not a token, looks exactly like this.', ); const mineUsd = mine != null ? mine : hard * 2; const deepEnough = mineUsd >= 100000; // A readable pool holding a sliver of the real liquidity describes a side // pocket, and a ladder off it would describe a market nobody trades in. But // share alone refuses genuinely deep pools that are merely one of several, so // a pool also qualifies on its own absolute depth. // No pool at all: ask four.meme before concluding "no pool". A token still // raising there has no pool by design; its market is the platform's own // contract, which quotes a buy and a sell of each size on request. A // graduated token (liquidityAdded) falls through to the pool path. if (!pool && !others.length) { const cv = await curveInfo(token); if (cv && !cv.liquidityAdded) { const quoteUsd = cv.quoteSym === 'BNB' ? bnbUsd : cv.quoteIsStable ? 1 : 0; const rows = await curveLadder(token, cv, quoteUsd, STEPS); const sellRow = rows.find((r) => r.sellCost != null); return { address: token, name, symbol: symb, quotable: false, reason: 'Still on its four.meme launch curve — there is no PancakeSwap pool yet; trades go through four.meme’s contract.', curve: { platform: 'four.meme', stage: 'bonding curve', quoteSymbol: cv.quoteSym, priceQuote: cv.price, priceUsd: quoteUsd > 0 ? cv.price * quoteUsd : null, feePct: cv.feePct, raised: cv.raised, maxRaising: cv.maxRaising, progressPct: cv.progressPct == null ? null : +cv.progressPct.toFixed(2), tokensLeft: cv.offersLeft, maxOffers: cv.maxOffers, launchTime: cv.launchTime ? new Date(cv.launchTime * 1000).toISOString() : null, tradeCost: rows.map((r) => ({ sizeUsd: r.usd, buyCostPct: r.buyCost == null ? null : +r.buyCost.toFixed(3), sellCostPct: r.sellCost == null ? null : +r.sellCost.toFixed(3), note: r.buyNote || r.sellNote || undefined, })), sellQuoted: !!sellRow, custody: 'The money raised sits in four.meme’s TokenManager contract until the raise completes, not in the creator’s wallet; there is no pool and therefore no liquidity to withdraw.', onCompletion: 'When the raise completes, four.meme lists the token on PancakeSwap; the pool path of this tool applies from then on.', source: 'four.meme TokenManagerHelper3 (getTokenInfo, tryBuy, trySell) on BNB Smart Chain, at this block', }, venues: [], source: 'measured on BNB Smart Chain via public RPC', }; } } if (!pool || (share < 0.25 && !deepEnough)) return { address: token, name, symbol: symb, quotable: false, reason: pool ? 'The readable pool holds too small a share of this token’s liquidity to describe its market.' : 'No pool at a venue whose swap fee has been verified here.', liquidity: { readablePoolUsd: Math.round(hard), elsewhereUsd: Math.round(otherLiq), shareOfLiquidity: +share.toFixed(4) }, venues: others.slice(0, 12), source: 'measured on BNB Smart Chain via public RPC', }; const partial = share < 0.25 ? share : null; // For a constant-product pair the ratio of the two reserves IS the price. For // a concentrated-liquidity pool it is not — V3 keeps its price in // sqrtPriceX96, so that is where it is read from. let px; if (pool.kind === 'v3') { const d0 = pool.tokenIs0 ? tokDec : 18; const d1 = pool.tokenIs0 ? 18 : tokDec; const r = Math.pow(Number(pool.sqrt) / Math.pow(2, 96), 2) * Math.pow(10, d0 - d1); px = (pool.tokenIs0 ? r : 1 / r) * pool.usd; } else px = (pool.q / pool.tok) * pool.usd; if (!(px > 0)) throw new ScanError('That pool is empty.', 'Both sides read back as zero — there is nothing to measure.'); // The tax, read off trades that actually happened rather than off a label. const tokenIs0 = pool.kind === 'v2' ? await rpcBatch([call(pool.pair, S.token0)]).then((r) => addrAt(r[0]) === token) : pool.tokenIs0; let tax = await measureTax(token, pool.pair.toLowerCase(), tokenIs0, pool.kind); // Can it be sold at all? Asked of the chain, not of a label (see the // function's header). V2 pairs only; anything else says so. let sim = await simulateRoundTrip(token, pool.pair.toLowerCase(), tokenIs0, pool.kind); // One patient retry before the label wins. Measured on 2026-09-08: one scan // in ten came back with status 200 and the tax "labelled by GoPlus" and the // sell test "every BSC endpoint refused" — the same question, answered by // measurement nine times and by a label once, because the log endpoint was // throttled for the second the scan needed it. A caller cannot tell that // answer from a measured one by its status. A second try after a beat is // three to five calls; a label sold as a measurement costs more than that. const simMeasured = (s) => !!(s && s.tax && (s.tax.buy_pct != null || s.tax.sell_pct != null)); let secondTry = false; if (!tax.ok && !simMeasured(sim)) { await new Promise((r) => setTimeout(r, 1500)); secondTry = true; tax = await measureTax(token, pool.pair.toLowerCase(), tokenIs0, pool.kind); if (!simMeasured(sim)) sim = await simulateRoundTrip(token, pool.pair.toLowerCase(), tokenIs0, pool.kind); } const gB = Number(gp.buy_tax); const gS = Number(gp.sell_tax); // Per direction: an executed trade first, the simulated trade second (the // probe read what arrived at this block), the label last. const sB = sim && sim.tax && sim.tax.buy_pct != null ? sim.tax.buy_pct / 100 : null; const sS = sim && sim.tax && sim.tax.sell_pct != null ? sim.tax.sell_pct / 100 : null; const taxB = tax.ok && tax.buy != null ? tax.buy : sB != null ? sB : isFinite(gB) ? gB : 0; const taxS = tax.ok && tax.sell != null ? tax.sell : sS != null ? sS : isFinite(gS) ? gS : 0; const simulated = sB != null || sS != null; const usedTax = tax.ok || simulated || isFinite(gB) || isFinite(gS); let rows, up, down, upMin = null, downMin = null; if (pool.kind === 'v2') { rows = ladderV2(pool.tok, pool.q, pool.fee, taxB, taxS, px, pool.usd); up = onePctV2(pool.q, pool.fee, 1.01) * pool.usd; down = (onePctV2(pool.tok, pool.fee, 1 / 0.99) / (1 - taxS)) * px; } else { rows = await ladderV3(pool.pair, token, pool.quote, pool.feeRaw, tokDec, px, pool.usd, taxB, taxS, pool.sqrt, pool.tokenIs0); const oc = await onePctV3(pool.pair, token, pool.quote, pool.feeRaw, tokDec, px, pool.usd, pool.sqrt, pool.tokenIs0, taxS); up = oc.up; down = oc.down; upMin = oc.upMin; downMin = oc.downMin; } // LP custody. A constant-product pair mints LP to the factory's feeTo() on // every liquidity event, so on any pool that has run for a while some // unburned LP belongs to the exchange rather than to anybody near the token. let lpTot = 0, lpDead = 0, lpNull = 0, lpFee = 0, feeTo = null; if (pool.kind === 'v2') { const lp = await rpcBatch([ call(pool.pair, S.totalSupply), call(pool.pair, balOf(DEAD)), call(pool.pair, balOf(NULLA)), pool.factory ? call(pool.factory, S.feeTo) : call(pool.pair, S.totalSupply), ]); lpTot = Number(hx(lp[0])) / 1e18; lpDead = Number(hx(lp[1])) / 1e18; lpNull = Number(hx(lp[2])) / 1e18; if (pool.factory) { feeTo = addrAt(lp[3]); if (feeTo && feeTo !== NULLA) { const fb = await rpcBatch([call(pool.pair, balOf(feeTo))]); lpFee = Number(hx(fb[0])) / 1e18; } else feeTo = null; } } return { address: token, name, symbol: symb, quotable: true, // "At this block" has to name the block (2026-09-12): the head the tax // window ended at, and when this answer was made. block: tax.block ?? null, measuredAt: new Date().toISOString(), price: { usd: px, quoteSymbol: pool.sym, quoteUsd: pool.usd }, supply: { total: supply, burned, circulating: supply != null ? supply - burned : null }, pool: { address: pool.pair, kind: pool.kind, venue: pool.venue || (pool.kind === 'v3' ? 'PancakeSwap V3' : null), swapFeePct: +(pool.fee * 100).toFixed(4), tokenReserve: pool.tok, quoteReserve: pool.q, liquidityUsd: Math.round(hard), // Which of the two possible meanings this figure has, said out loud. // liquidityUsd is the QUOTE SIDE ONLY — the BNB or USDT actually in the // pool, the half that does not evaporate when the token's own price does. // pancakeswap_fee_tiers reports capital_usd for the same pool counting // BOTH sides, because an LP has to put up both, so the two figures differ // by roughly 2x on purpose. Without this line they read as a contradiction // between our own endpoints, which is how a correct number loses an // argument it should win. liquidityBasis: 'quote side only — the hard asset in the pool. Counting both sides, as an LP would, is roughly twice this; that is what pancakeswap_fee_tiers reports as capital_usd.', shareOfLiquidity: +share.toFixed(4), partialMarket: partial != null, }, // What a trade of each size actually costs, tax and slippage and swap fee // together — not the headline slippage a router shows. // A rung the pool cannot fill (V3, more than sits in range) carries null // figures and a note; before, KII's ladder said "+5.33e+41%" there. tradeCost: rows.map((r) => ({ sizeUsd: r.usd, buyCostPct: r.buyCost == null ? null : +r.buyCost.toFixed(3), buyPriceMovePct: r.buyMove == null ? null : +r.buyMove.toFixed(3), sellCostPct: r.sellCost == null ? null : +r.sellCost.toFixed(3), sellPriceMovePct: r.sellMove == null ? null : +r.sellMove.toFixed(3), ...(r.buyNote ? { buyNote: r.buyNote } : {}), ...(r.sellNote ? { sellNote: r.sellNote } : {}), })), onePercentDepth: { buyUsd: Math.round(up), sellUsd: Math.round(down), note: 'USD size that moves the price by 1% in each direction', ...(upMin != null ? { buyUsdLowerBound: Math.round(upMin), sellUsdLowerBound: Math.round(downMin) } : {}), }, // The sell test. `ok:false` with a reason is "not checked" and must never // be read as "safe"; `sellable:false` carries the router's own reason. sellability: sim, tax: { // null, not 0, when nothing could be established. Zero is a claim — it // says this token takes no cut on transfer — and printing it because a // lookup failed is how a reader ends up budgeting three percent short. // BOBAI itself surfaced this: with GoPlus unreachable the answer came // back "0% tax" for a token that charges 3%. The arithmetic below still // has to use a number, so it uses zero and says so here. buyPct: usedTax ? +(taxB * 100).toFixed(3) : null, sellPct: usedTax ? +(taxS * 100).toFixed(3) : null, measured: !!tax.ok, // Why not, when not: a quiet pool and a throttled log endpoint are // different answers for a caller — one is final, the other says retry. ...(tax.ok ? {} : { reason: tax.reason || null }), windowMinutes: tax.windowBlocks ? Math.round(tax.windowBlocks * 0.45 / 60) : null, // Named when the first read was throttled and the second answered: a // caller measuring the path can count how often the retry earned its keep. ...(secondTry ? { read_on_second_try: true } : {}), // The distinction that matters: measured means real executed trades were // read; simulated means the same trade was run on the chain at this block // from a fresh address and its gap read; labelled means a reputation // service said so and nothing verified it. Those disagree in practice, // sometimes by more than a point. source: tax.ok ? 'measured from executed trades on-chain' : simulated ? 'simulated on-chain at this block, from a fresh address' : usedTax ? 'labelled by GoPlus, unverified' : 'unknown', ...(simulated ? { simulated: { buyPct: sB == null ? null : +(sB * 100).toFixed(2), sellPct: sS == null ? null : +(sS * 100).toFixed(2), method: sim.tax.method } } : {}), ...(usedTax ? {} : { warning: 'No transfer tax could be established — neither from executed trades nor from a label. The cost figures below therefore EXCLUDE any transfer tax. If this token takes a cut on transfer, a real trade costs more than shown.' }), ...(tax.ok && tax.trades ? { tradesSampled: tax.trades } : {}), }, ...(pool.kind === 'v2' ? { lp: { totalSupply: lpTot, burnedPct: lpTot > 0 ? +(((lpDead + lpNull) / lpTot) * 100).toFixed(2) : null, exchangeFeeShare: lpFee > 0 ? +((lpFee / lpTot) * 100).toFixed(2) : 0, feeToAddress: feeTo, note: 'LP held at the burn addresses cannot be withdrawn. Any balance at the factory feeTo() belongs to the exchange, not to the token team.', }, } : {}), venues: others.slice(0, 12), ...(deeper ? { deeperPoolElsewhere: { pair: deeper.pair, liquidityUsd: Math.round(deeper.hard) } } : {}), contract: { openSource: gp.is_open_source === '1' ? true : gp.is_open_source === '0' ? false : null, proxy: gp.is_proxy === '1' ? true : gp.is_proxy === '0' ? false : null, mintable: gp.is_mintable === '1' ? true : gp.is_mintable === '0' ? false : null, source: gpOk ? 'GoPlus (contract properties only, never used to override a measured figure)' : 'unavailable' + (gpWhy.reason ? ' — GoPlus ' + gpWhy.reason : ''), }, source: 'measured on BNB Smart Chain via public RPC', disclaimer: 'Measurement, not advice. Figures describe what a trade would cost at the moment of the scan; depth and tax can change block to block.', }; } ============================================================================== === FILE: dashboard/scanner.html ============================================================================== Pool Scanner — what would buying this token really cost?
Tool · Pool Scanner

What would buying this token really cost?

Paste any BNB Chain token. You get the real cost of a trade at your size, the hidden tax measured from trades that actually happened, and whether the liquidity can be pulled out. Free, no wallet, nothing is sent from your account.

On four.meme right nowThe tokens traded on the launch curve in the last few minutes, and what $100 costs to buy and to sell there — before any pool exists. Read from four.meme’s contract when you ask.

What you get. Not the advertised price: the real one, including the part most sites do not show.

What a trade really costs Price impact and true cost side by side at six sizes from $100 to $2,500, so you see where it stops being worth it.
The hidden tax, measured Not read off a label. Taken from trades that actually executed, and flagged when it disagrees with what the security scanners report.
Who can pull the money out LP burned for good, only locked, or sitting in a wallet that can empty it tonight — with the largest holder named and linked.
Can you sell it A sell is simulated on the chain from a fresh address, at this block. A pass is a fact about now, not a promise about tomorrow.

A token still on four.meme’s launch curve has no pool yet; the curve is read instead. If a pool cannot be measured exactly, the page shows nothing rather than a wrong number. Everything comes straight from the chain.

Please read this next to any number above

We built this because reading a pool properly is genuinely interesting, and we are still building it. Every figure here is our own reading of public chain data. A price can be a block out of date, a token can trade in pools this page cannot see, and a tax can behave differently in your trade than in the ones we measured. Assume a number can be wrong, because sometimes it will be.

And none of it is a judgement. We make no claim about any token — we do not know who is behind them, and we are not in the business of telling you which ones deserve your money. A red figure says one thing only: at that size, that is what the trade would cost right now. It says nothing about the project, and nothing about the people.

Anything that matters to you, check yourself. BscScan and the pool contract are two clicks away, and everything above is derived from what they already say out loud.

============================================================================== === FILE: dashboard/scanner.js ============================================================================== // SCANNER — the page layer. Every number shown here is produced by // scanner-chain.js; this file decides only how it is presented and, just as // importantly, how uncertainty is presented. Two rules run through all of it: // // 1. Nothing is rendered as markup from a name a stranger chose. Token names // are attacker-controlled strings and a contract can call itself // "". Everything foreign goes in through a text node. // 2. Unknown is a state, not a blank. A property GoPlus did not check must // read "not checked" — never as an absent warning, which is how a reader // hears "fine". $Max returned undefined for is_honeypot and the old build // showed nothing at all, which is the most dangerous thing this page // could do. import {RPC,GOPLUS,V2FACTORY,WBNB,BNB_PAIR,DEAD,NULLA,QUOTES,V2_FEE,STEPS,SEL as S, balOf,call,hx,addrAt,res2,decStr,rpcBatch,classify,priceToken,discover, ladderV2,onePctV2,ladderV3,onePctV3,measureTax,venues,FACTORIES,simulateRoundTrip, curveInfo,curveLadder,curveFeed,FOURMEME_MANAGER} from './scanner-chain.js?v=26'; const $=id=>document.getElementById(id); const nf=(n,d=0)=>Number(n).toLocaleString('en-US',{minimumFractionDigits:d,maximumFractionDigits:d}); // toPrecision() switches to exponential notation below 1e-7, which printed a real // price as "$1.79e-8". Nobody quotes a token price that way. Write it out with // every zero instead, at three significant digits. Longer, but unambiguous — and // unlike the subscript-zero style ($0.0₈179) it survives being copied off the page, // where the subscript silently degrades into an ordinary digit. // The exponent comes from toExponential() rather than log10(), which is off by one // for exact powers of ten (log10(0.001) = -3.0000000000000004). const tiny=n=>{ if(!(n>0))return Number(n||0).toFixed(2); const e=parseInt(n.toExponential(2).split('e')[1],10); // toFixed() caps at 100 decimals. Past that the expansion would be all zeros // and no significant digit at all, so keep the exponent rather than print a // number that reads as zero. return e<-98?n.toExponential(2):n.toFixed(Math.max(2,2-e)); }; const usd=n=>n==null?'—':n>=1000?'$'+nf(n):n>=1?'$'+nf(n,2):n>=0.01?'$'+nf(n,4):'$'+tiny(n); const short=a=>a?a.slice(0,6)+'…'+a.slice(-4):'—'; // What a rebalance costs, said in units of what the position earns rather than // in dollars — a dollar figure means nothing without the thing it is compared // against. Rounded to whole windows only when there are whole windows to round // to: the first version printed "about 0 windows" whenever the fees for one // window happened to exceed the gas, which is the case this sentence exists to // describe as GOOD news. const costInWindows=(cost,fees)=>{ if(!(cost>0))return 'nothing measurable'; if(!(fees>0))return 'more than this range collected at all'; const n=cost/fees; if(n>=1.5)return Math.round(n)+' windows of what it collected'; if(n>=0.75)return 'about one window of what it collected'; return Math.round(n*100)+'% of what it collected in one window'; }; // Two decimals lie at both ends: 99.998% burned rounds to a flat "100.00%", // claiming more than the chain says, and a real 0.002% rounds to "0.00%", // claiming it is not there. A sell tax of 4.45% must never print as "4.5%". const pc=(v,d=2)=>v==null?'—':v>0&&v<0.01?'<0.01%':(v>=99.995&&v<100)?'>99.99%':v.toFixed(d)+'%'; const signed=v=>v==null?'—':(v<0?'':'+')+(Math.abs(v)<0.005?'0.00':v.toFixed(2))+'%'; function el(tag,cls,text){const e=document.createElement(tag); if(cls)e.className=cls;if(text!=null)e.textContent=text;return e} function frag(parent,...kids){kids.forEach(k=>parent.append(k));return parent} const link=(t,href,cls)=>{const a=el('a',cls||'lk',t);a.href=href;a.target='_blank';a.rel='noopener';return a}; function fail(msg,sub){ const o=$('sc-out');o.hidden=true;o.textContent=''; const e=$('sc-err');e.hidden=false;e.textContent=''; e.appendChild(el('b',null,msg));if(sub)e.appendChild(el('span',null,sub)); $('sc-status').textContent=''; } function busy(on,msg){ $('sc-go').disabled=on;$('sc-go').textContent=on?'Reading…':'Scan'; $('sc-status').textContent=on?(msg||''):''; } const step=m=>{if($('sc-go').disabled)$('sc-status').textContent=m}; // GoPlus flags. The third column says what a MISSING value means: for most // properties silence is just silence, and claiming otherwise would invent an // all-clear the service never gave. const FLAGS=[ ['is_mintable','Mintable','More tokens can be created — the supply is not fixed.'], ['is_proxy','Proxy contract','The logic sits behind an upgradeable pointer and can be replaced.'], ['can_take_back_ownership','Ownership reclaimable','A renounce can be undone.'], ['hidden_owner','Hidden owner','Ownership is held somewhere other than the usual slot.'], ['selfdestruct','Self-destruct','The contract can delete itself.'], ['transfer_pausable','Transfers pausable','Someone can freeze all transfers.'], ['is_blacklisted','Blacklist','Individual wallets can be blocked from trading.'], ['slippage_modifiable','Tax changeable','The tax rate is not fixed — it can be raised later.'], ['personal_slippage_modifiable','Per-wallet tax','A different tax can be set for individual wallets.'], ['trading_cooldown','Trading cooldown','A forced wait is enforced between trades.'], ['is_anti_whale','Max transaction limit','A cap on trade size is enforced.'], ['anti_whale_modifiable','Trade cap changeable','That cap can be changed later.'], ['cannot_sell_all','Cannot sell all','Selling the full balance in one go is blocked.'], ]; // COLOUR — and what it is allowed to mean. // // Green/amber/red here say ONE thing: how much this costs you, measured against // a floor that is not a matter of opinion. Every pool has an unavoidable toll — // the swap fee plus the transfer tax — that you pay at any size. Everything on // top of that is depth. So the bands compare what you actually pay against that // floor, and the impact bands are read straight off the price you move. // // What the colour deliberately does NOT mean: that a token is good, safe, or // worth buying. A deep pool with a renounced owner can still go to zero, and a // thin one can be perfectly honest. Publishing a verdict about somebody else's // token would put our name on a judgement we cannot stand behind — and the one // time we got it wrong, that is the only thing anyone would remember. // // An earlier draft coloured these by percentile against a sample of 46 pools. // That was dropped: a keyword-scraped sample of 46 is not a distribution, and // dressing it up as one would be inventing authority. const band=(v,ok,mid)=>v==null?'':v<=ok?' good':v<=mid?' mid':' bad'; const costBand=(pay,floor)=>{ if(pay==null||!(floor>0))return ''; return band(pay/floor,1.5,3); // at most half again over the toll, or triple it }; const impactBand=v=>v==null?'':band(Math.abs(v),1,5); // 1% and 5% of the price you move // ---- building blocks ------------------------------------------------------- function statRow(items){ const g=el('div','st-row'); items.forEach(it=>{ const c=el('div','st'); // A sub-cent price written out with every zero is far longer than a market // cap, and at the headline size it wrapped mid-number on a 390px phone — a // price broken across two lines invites a misread. Step the size down by // length instead of letting it break. const vs=String(it.v==null?'':it.v), fit=vs.length>=16?' st-xl':vs.length>=12?' st-lg':''; c.appendChild(el('div','st-v'+fit+(it.dim?' dim':'')+(it.tone||''),it.v)); c.appendChild(el('div','st-l',it.l)); if(it.s){ const s=el('div','st-s',it.s); // An address printed as plain text is a dead end — the reader wants to go // look at the wallet, and making them copy it by hand is the difference // between a claim and something they can check. if(it.link)s.append(' ',link(it.link.t,it.link.href,'lk')); c.appendChild(s); } g.appendChild(c); }); return g; } // ---- fee tiers: the question a liquidity provider has ---------------------- // // Everything above this card answers "what would a trade cost me". This one // answers the other side: a pair on PancakeSwap lives in up to five pools at // once — V2 at 0.25% and V3 at 0.01/0.05/0.25/1.00% — and every interface, // including the venue list further up this page, ranks them by the money // already parked in them. That is a record of what other people did. It is not // what the pool pays, and the two come apart constantly. // // WHY THIS ONE CALLS AN ENDPOINT ON A PAGE THAT OTHERWISE CALLS NONE // The rest of this page reads the chain straight from the visitor's browser and // costs nothing to run. This card asks brainonbnb.com/api/fee-tiers instead, // and that is deliberate rather than lazy: the identical measurement is sold to // agents through an MCP tool and an ERC-8183 agent, and a page that computed it // a second time here would eventually disagree with them about which tier pays // best. One number, one source. It is behind a button so the default scan is // unchanged, and nothing on this card is fetched unless somebody asks for it. function tierCard(token){ const c=card('Which fee tier is paying its liquidity providers', 'For providing liquidity, not for trading. Measured over a live window and deliberately not annualised.'); const btn=el('button','sc-tierbtn','Measure the PancakeSwap tiers'); btn.type='button'; const out=el('div','tier-out'); c.append(btn,out); btn.addEventListener('click',async()=>{ if(btn.disabled)return; btn.disabled=true;btn.textContent='Measuring…'; out.textContent=''; try{ const r=await fetch('/api/fee-tiers?address='+encodeURIComponent(token)); const d=await r.json(); if(d.error){renderTierError(out,d.error);return;} renderTiers(out,d); btn.remove(); }catch(e){ renderTierError(out,'The measurement did not come back. Nothing is cached here, so a retry usually works.'); }finally{ if(btn.isConnected){btn.disabled=false;btn.textContent='Measure the PancakeSwap tiers';} } }); return c; } function renderTierError(out,msg){ out.textContent=''; out.appendChild(el('p','cd-foot',msg)); } function renderTiers(out,d){ out.textContent=''; const measured=(d.tiers||[]).filter(t=>t.measured); // A refused range and a quiet pool arrive as the same emptiness and mean // opposite things — one of them is a fact about somebody's pool and the other // is a fact about our measurement. Never the same sentence. if(!measured.length){ out.appendChild(el('p','cd-foot','None of the '+((d.tiers||[]).length)+ ' tiers could be read this time: the log endpoint refused the range. That says nothing about whether the pair traded. Try again in a moment.')); return; } // THE ANSWER FIRST, THE EVIDENCE UNDER IT. // // The table below is five rows of numbers that a liquidity provider has to // hold in their head simultaneously to get an answer out of. The answer is // two sentences, so it goes first, in the same shape the scan result uses // further up this page. const best=d.best_paying_tier, most=d.most_capital_tier; const bestW=d.best_paying_tier_by_working_capital, mostW=d.most_working_capital_tier; const ans=el('div','vd tier-ans'); const line=(tone,head,body)=>{ const r=el('div','vd-r vd-'+tone); r.appendChild(el('b',null,head)); if(body)r.appendChild(el('span',null,body)); ans.appendChild(r); }; const row=t=>(d.tiers||[]).find(x=>x.tier===t); // 1. Where a new dollar earns most. Withheld — loudly — when any tier went // unread or any band came back truncated, because a ranking over a subset // names whichever tier happened to be readable. if(bestW){ const b=row(bestW); line('good',bestW+' pays the most per dollar that is actually working.', b&&b.fees_per_1000_usd_working!=null ?'It paid $'+b.fees_per_1000_usd_working.toFixed(4)+' per $1,000 of capital standing within '+ (d.band_pct||2)+'% of the price, over this window. That is the figure to compare, because a dollar you add only earns beside the capital that is at the price.' :'Measured over the capital standing at the price rather than the capital in the pool.'); }else if(d.comparison_complete===false){ const n=(d.tiers_unreadable||[]).length; line('unknown',n+' of '+(d.tiers_found||0)+' tiers could not be read, so no tier is called best.', (d.best_paying_tier_among_readable?'Of the ones that were read, '+d.best_paying_tier_among_readable+' paid most. ':'')+ 'A ranking over an unknown subset would name whichever tier happened to answer. Ask again in a moment.'); }else if(d.bands_complete===false){ line('unknown','The tick book was too dense to read whole, so no tier is called best.', 'One of these pools has more price levels inside the band than can be read in one pass. The capital shown for it is understated, and understating one tier flatters the others.'); }else{ line('unknown','Nothing traded on any tier that could be read in this window.', 'No fees were paid, so no tier can be ranked by what it paid. The capital figures below still hold.'); } // 2. The finding this panel exists for: what a pool HOLDS and what it has // standing at the price are different numbers, and every interface an LP // can consult shows the first one. if(most&&mostW&&most!==mostW){ const a=row(most), b=row(mostW); line('mid',most+' holds the most money. '+mostW+' has the most of it at the price.', (a&&a.capital_usd!=null&&a.working_capital_usd!=null&&b&&b.working_capital_usd!=null) ?most+' holds '+usd(a.capital_usd)+' and stands '+usd(a.working_capital_usd)+' within '+(d.band_pct||2)+ '% of the price. '+mostW+' stands '+usd(b.working_capital_usd)+'. Depth on a listing page is the first number; what you compete with is the second.' :''); }else if(d.working_capital_changes_the_answer===true&&best&&bestW&&best!==bestW){ line('mid','By the usual measure '+best+' looks best. By working capital '+bestW+' is.', 'Dividing the same fees by everything the pool holds rewards a pool for capital that earns nothing. Both figures are in the table.'); }else if(most&&mostW&&most===mostW&&bestW){ line('mid',most+' both holds the most and stands the most at the price.', 'The two measures agree here, which is worth knowing rather than assuming.'); } out.appendChild(ans); const rows=el('div','tier-t'); const head=el('div','tier-r tier-hr'); head.append(el('span','tier-n','tier'),el('span','tier-c','in the pool'), el('span','tier-w','at the price'),el('span','tier-v','traded'), el('span','tier-f','pays per $1,000 working')); rows.appendChild(head); // The header disappears below 560px, so each figure carries its own label // that only shows there. Two unexplained numbers side by side on a phone is // how a panel with more information ends up saying less. const cell=(cls,label,text,extra)=>{ const s=el('span',cls+(extra||'')); s.appendChild(el('i','tl',label)); s.appendChild(document.createTextNode(text)); return s; }; (d.tiers||[]).forEach(t=>{ const r=el('div','tier-r'+(t.tier===bestW?' tier-best':'')); const n=el('span','tier-n',t.tier); if(t.tier===most)n.appendChild(el('em','tier-tag','most capital')); else if(t.tier===mostW)n.appendChild(el('em','tier-tag','most at the price')); r.appendChild(n); r.appendChild(cell('tier-c','in the pool',t.capital_usd==null?'—':usd(t.capital_usd))); // Share as well as amount: "$171K of $17.4M" is the whole point, and one // percent reads harder than it should without the figure it is a share of. r.appendChild(cell('tier-w','at the price', t.working_capital_usd==null?'—':usd(t.working_capital_usd)+ (t.working_share_pct!=null?' ('+t.working_share_pct.toFixed(t.working_share_pct<10?1:0)+'%)':''))); if(!t.measured){ r.appendChild(cell('tier-v dim','traded','not readable')); r.appendChild(cell('tier-f dim','pays per $1,000','—')); }else{ r.appendChild(cell('tier-v','traded',t.volume_usd>0?usd(t.volume_usd):'nothing')); const pays=t.fees_per_1000_usd_working; r.appendChild(cell('tier-f','pays per $1,000',pays==null?'—':'$'+pays.toFixed(4), t.tier===bestW?' good':(pays===0?' dim':''))); } rows.appendChild(r); }); out.appendChild(rows); const idle=(d.idle_capital||[]).reduce((s,x)=>s+(x.capital_usd||0),0); if(idle>=100){ out.appendChild(el('p','cd-foot',usd(idle)+' sits in '+ (d.idle_capital.length===1?'a tier that':'tiers that')+' saw no trade at all in this window: '+ d.idle_capital.map(x=>x.tier).join(', ')+'.')); } const w=d.measured_window||{}; out.appendChild(el('p','cd-foot','Measured over '+(w.minutes??'~59')+ ' minutes of chain — a sample, not a rate, and not annualised. Capital is both sides of the pool. "At the price" is the part of it standing within '+ (d.band_pct||2)+'% of the current price, walked from the pool’s own tick data and checked against PancakeSwap’s quoter; the rest is on the balance sheet and earns nothing while the price is where it is. It assumes the price stays in that band, which it will not do forever. Impermanent loss is not in any of this.')); } // THE QUESTION AFTER THE TIER, and the only one V3 really forces. // // The card above answers which of the five pools sharing this pair is worth // being in. Having picked one, a liquidity provider still has to say between // which two prices the money sits, and that decision moves the outcome far more // than the tier does — three orders of magnitude, on the pair this was built // against. Every interface offers a preset for it. // // This is not a preset and not a forecast. The V3 Swap event carries the // liquidity that was active when each trade went through, so a position of a // chosen size is walked back through the trades that actually happened: in // range or not, and what share of the liquidity standing there it would have // been. Behind a button, like the tier card, because it costs a measurement. function rangeCard(token){ const c=card('Which price range, if you did provide liquidity', 'A V3 position is not in a pool, it is between two prices. Replayed against the trades that actually happened in a live window.'); const btn=el('button','sc-tierbtn','Replay the ranges'); btn.type='button'; const out=el('div','tier-out'); c.append(btn,out); btn.addEventListener('click',async()=>{ if(btn.disabled)return; btn.disabled=true;btn.textContent='Replaying…'; out.textContent=''; try{ const r=await fetch('/api/range-plan?address='+encodeURIComponent(token)); const d=await r.json(); if(d.error){renderTierError(out,d.error);return;} renderRanges(out,d); btn.remove(); }catch(e){ renderTierError(out,'The replay did not come back. Nothing is cached here, so a retry usually works.'); }finally{ if(btn.isConnected){btn.disabled=false;btn.textContent='Replay the ranges';} } }); return c; } function renderRanges(out,d){ out.textContent=''; const rows=d.ranges||[]; const w=d.measured_window||{}; if(!rows.length||!w.swaps){ out.appendChild(el('p','cd-foot','No swap in the measured window, so there is nothing to replay a position against. That is a fact about this pool in the last hour, not about the ranges.')); return; } const ans=el('div','vd tier-ans'); const line=(tone,head,body)=>{ const r=el('div','vd-r vd-'+tone); r.appendChild(el('b',null,head)); if(body)r.appendChild(el('span',null,body)); ans.appendChild(r); }; const held=d.narrowest_range_that_held_the_whole_window; const best=d.best_earning_range_in_this_window; // The honest headline is the narrowest range that HELD, not the one that // earned most. The best earner is regularly a range the price walked out of, // and naming that as the answer would be recommending a position on the // strength of the hour before it broke. if(held){ const row=rows.find(r=>r.width_pct!=null&&('±'+r.width_pct+'%')===held); const full=rows.find(r=>r.full_range); line('good',held+' is the narrowest range that held for the whole window.', (row&&full&&full.fees_usd_in_window>0) ? 'On $'+nf(d.capital_considered_usd)+' it would have collected '+usd(row.fees_usd_in_window)+ ' — about '+Math.round(row.fees_usd_in_window/full.fees_usd_in_window)+ ' times what the same money makes spread across every price. Narrow is where the fees are; it is also where the work is.' : 'Narrower ranges earn more per dollar and stop earning the moment the price leaves them.'); }else{ line('unknown','No range on this list held for the whole window.', 'On this pool, in this hour, the price was outside every width at some point. That is worth knowing before placing anything, and it is why the crossing count is a column rather than a footnote.'); } if(best&&held&&best!==held){ const b=rows.find(r=>('±'+r.width_pct+'%')===best); line('mid',best+' collected the most — and the price crossed its edge '+ (b?b.times_it_crossed_the_edge:'')+(b&&b.times_it_crossed_the_edge===1?' time':' times')+'.', 'A position there earns nothing while it is outside, and putting it back costs about $'+ (d.rebalance_cost_usd_assumed||0).toFixed(2)+' in gas — '+ costInWindows(d.rebalance_cost_usd_assumed,b&&b.fees_usd_in_window)+ '. After paying for that, '+(d.best_range_after_paying_to_put_it_back||'nothing here')+' came out ahead.'); } // The sentence that stops a narrow range looking free even when it held: the // cost of nursing it is a real number and it belongs beside the reward. else if(held){ const hr=rows.find(r=>('±'+r.width_pct+'%')===held); if(hr&&hr.fees_usd_in_window>0&&d.rebalance_cost_usd_assumed>0){ line('mid','One crossing costs '+costInWindows(d.rebalance_cost_usd_assumed,hr.fees_usd_in_window)+'.', 'Putting a position back is roughly $'+d.rebalance_cost_usd_assumed.toFixed(2)+ ' of gas — 700,000 units priced well above the current floor. It did not happen in this window. It is the thing to watch if it does.'); } } out.appendChild(ans); const t=el('div','tier-t'); const head=el('div','tier-r tier-hr'); head.append(el('span','tier-n','range'),el('span','tier-c','between'), el('span','tier-w','in range'),el('span','tier-v','edge crossed'), el('span','tier-f','collected on $'+nf(d.capital_considered_usd))); t.appendChild(head); const cell=(cls,label,text,extra)=>{ const s=el('span',cls+(extra||'')); s.appendChild(el('i','tl',label)); s.appendChild(document.createTextNode(text)); return s; }; rows.forEach(r=>{ const isHeld=held&&('±'+r.width_pct+'%')===held; const row=el('div','tier-r'+(isHeld?' tier-best':'')); const n=el('span','tier-n',r.full_range?'full range':'±'+r.width_pct+'%'); if(isHeld)n.appendChild(el('em','tier-tag','narrowest that held')); row.appendChild(n); row.appendChild(cell('tier-c','between', r.price_range?(r.price_range.low+' – '+r.price_range.high):'every price')); row.appendChild(cell('tier-w','in range', r.share_of_window_in_range_pct==null?'—':r.share_of_window_in_range_pct+'%')); row.appendChild(cell('tier-v','edge crossed', r.times_it_crossed_the_edge===0?'never':r.times_it_crossed_the_edge+'×')); row.appendChild(cell('tier-f','collected','$'+r.fees_usd_in_window.toFixed(6), isHeld?' good':(r.fees_usd_in_window===0?' dim':''))); t.appendChild(row); }); out.appendChild(t); out.appendChild(el('p','cd-foot','Measured over '+(w.minutes??'~59')+ ' minutes of chain — a sample, not a rate, and not annualised. Replayed against the '+w.swaps+ ' swaps that actually happened in it, using the liquidity the pool itself reported as active at each one — not a simulation of a market, arithmetic over trades that occurred. The pool paid $'+ (w.fees_the_pool_paid_usd||0).toFixed(2)+' in fees across all of them'+ (typeof w.paid_to_liquidity_pct==='number'&&w.paid_to_liquidity_pct<100?', of which the liquidity is credited '+w.paid_to_liquidity_pct.toFixed(0)+'% — the pool keeps the rest for the protocol, and every figure above is the part credited to the liquidity':'')+ '. Not annualised: what a range did in one hour is not what it does over a year. '+ (d.tier_chosen_because?'Pool picked for you: the '+d.tier_chosen_because+' — which tier PAYS best is the card above. ':'')+ 'Impermanent loss is not in any of this, and it is worst exactly where the fees are best.')); } function card(title,sub){ const c=el('section','cd'); const h=el('div','cd-h'); h.appendChild(el('h3',null,title)); if(sub)h.appendChild(el('p',null,sub)); c.appendChild(h); return c; } // ---- the answer, before the evidence --------------------------------------- // // Everything below this is a measurement and every one of them is worth having. // None of them is the sentence somebody came here for. The result used to open // with "Hard USDT backing" and "Liquidity / Mcap" — correct, and neither is a // phrase a person uses about their own money. So the first thing on the page is // now three plain answers: what a normal-sized trade costs you, whether the // token takes a cut of every trade, and whether anybody can walk off with the // liquidity. The numbers are the same ones the cards below carry; nothing new // is computed and nothing is rounded into a claim. // // A line that cannot be answered says so. "Could not be measured" is an honest // line; a green tick that means "we did not look" is not. function verdictCard(d,pool,gp,gpOk,tax){ const c=el('section','cd vd-card'); const h=el('div','cd-h'); h.appendChild(el('h3',null,'The short answer')); h.appendChild(el('p',null,'The three things worth knowing before you trade this, in plain words. Every one of them is measured below.')); c.appendChild(h); const list=el('div','vd'); const line=(tone,head,body)=>{ const r=el('div','vd-r vd-'+tone); r.appendChild(el('b',null,head)); r.appendChild(el('span',null,body)); list.appendChild(r); }; // 1. What a normal trade costs. The reference size is the row closest to $500 // rather than the smallest or the largest: the smallest flatters the pool // and the largest scares people away from one that would have been fine. const rows=(d.rows||[]).slice().sort((a,b)=>Math.abs(a.usd-500)-Math.abs(b.usd-500)); // ...among the sizes the pool can fill: a rung that runs the pool dry has // no cost, and the short answer must not quote a size that never fills. const ref=rows.find(r=>r.buyCost!=null&&r.sellCost!=null)||rows[0]; const floors={buy:(1-(1-d.taxB)*(1-pool.fee))*100, sell:(1-(1-d.taxS)*(1-pool.fee))*100}; if(ref&&ref.buyCost!=null&&ref.sellCost!=null){ const worst=Math.max(ref.buyCost,ref.sellCost); const toll=Math.max(floors.buy,floors.sell); // Measured against the unavoidable toll for THIS pool, not against a fixed // percentage: 3% is cheap in a 1% fee tier with a 2% tax and dreadful in a // 0.05% pool with none. const tone=toll>0?(worst<=toll*1.5?'good':worst<=toll*3?'mid':'bad'):'mid'; line(tone,'A $'+nf(ref.usd)+' trade costs you '+pc(ref.buyCost)+' to buy and '+pc(ref.sellCost)+' to sell.', 'That is the whole cost: the pool fee, any transfer tax, and how far your own trade moves the price. ' +(toll>0?'About '+toll.toFixed(toll<1?2:1)+'% of it is unavoidable at any size in this pool; the rest is depth.' :'Round trip, that is about '+pc(ref.buyCost+ref.sellCost)+' before the price moves at all.')); }else{ line('unknown','A trade of this size could not be priced.', 'The quoter did not return a price for every size, so no cost figure is shown at all rather than a partial one.'); } // 2. The tax. The figure most likely to be wrong elsewhere, which is why this // page measures it from executed trades instead of reading the label. const measured=tax&&tax.ok&&(tax.buy!=null||tax.sell!=null); if(d.taxB||d.taxS){ const both=Math.round(d.taxB*1000)===Math.round(d.taxS*1000); line(d.taxB>=0.10||d.taxS>=0.10?'bad':'mid', both?'This token takes '+pc(d.taxB*100)+' out of every trade.' :'This token takes '+pc(d.taxB*100)+' when you buy and '+pc(d.taxS*100)+' when you sell.', (measured?'Measured from trades that actually executed, not read off the contract label.' :(d.simB||d.simS)?'Simulated on the chain at this block from a fresh address — the same trade run and its gap read, not a label. No executed trade was available to measure it from.' :'Reported by GoPlus and not verified here — no executed trade was available to measure it from.') +' It is already included in the cost above.'); }else if(measured){ line('good','No transfer tax. You keep what you trade, minus the pool fee.', 'Measured from trades that actually executed. A token can still add one later if its contract allows it.'); }else{ line('unknown','Whether it takes a transfer tax could not be established.', 'No executed trade was available to measure it from, and no label is trusted in its place. Treat the cost above as a floor.'); } // 3. Who can remove the liquidity. On a concentrated-liquidity pool there are // no LP tokens to burn, so the honest line is that this question does not // apply rather than a reassuring one that does not mean anything. if(pool.kind==='v2'){ const burnedPct=d.lpTot>0?(d.lpDead+d.lpNull)/d.lpTot*100:0; const feePct=d.lpTot>0&&d.lpFee>0?d.lpFee/d.lpTot*100:0; const free=Math.max(0,100-burnedPct-feePct); if(burnedPct>=99){ line('good','Nobody can pull the liquidity out. It is burned.', pc(burnedPct)+' of the LP tokens sit at a dead address. Burned liquidity can never be withdrawn by anyone, including the people who put it there.'); }else if(free>=50){ // "Somebody can pull this" is the right warning for one wallet holding // the lot and the wrong one for a blue chip whose LP sits across // thousands of addresses. Both are "not burned"; only one is a person who // could empty the pool tonight, and the largest single holder is what // separates them. const others=(gp.lp_holders||[]).filter(x=>{const a=(x.address||'').toLowerCase(); return a!==DEAD&&a!==NULLA&&a!==(d.feeTo||'')&&x.is_locked!==1}); const top=others.sort((a,b)=>(parseFloat(b.percent)||0)-(parseFloat(a.percent)||0))[0]; const topPct=top?(parseFloat(top.percent)||0)*100:null; if(gpOk&&topPct!=null&&topPct<10){ line('mid','The liquidity is not burned, but no single wallet holds much of it.', pc(free)+' of the LP can be withdrawn in principle, spread across many holders — the largest one has '+pc(topPct)+ '. Nobody here can empty the pool on their own; a lot of them leaving at once is a different question, and not one this page can answer.'); }else if(gpOk&&topPct!=null){ line('bad','One wallet can withdraw most of this liquidity.', pc(free)+' of the LP is neither burned nor at the exchange, and a single wallet holds '+pc(topPct)+ ' of it. That is not proof of anything — plenty of honest pools look like this — but it is the risk that empties a pool overnight.'); }else{ // Not burned is measured on-chain and certain. WHO holds it is not: // without the holder list this page cannot tell one wallet from ten // thousand, and those are very different risks. Saying "somebody can // pull this" here would be a claim built on the half we could not read. line('unknown','The liquidity is not burned, and we could not see who holds it.', pc(free)+' of the LP is withdrawable in principle — that part is read from the chain. The holder list comes from GoPlus, which '+(gpOk?'lists no holders for this pool':'did not answer')+', so whether that is one wallet or thousands is unknown rather than fine.'); } }else{ line('mid','Part of the liquidity can still be withdrawn.', pc(burnedPct)+' is burned for good; about '+pc(free)+' is not. The breakdown, including who holds the largest unburned share, is further down.'); } }else{ line('unknown','“Is the liquidity burned?” does not apply to this pool.', 'It is a concentrated-liquidity pool: liquidity is held as individual positions rather than as LP tokens, so there is nothing to burn. Any position here can be closed by whoever opened it, at any time.'); } c.appendChild(list); c.appendChild(el('p','cd-legend', 'None of this says whether the token is a good idea. It says what trading it would cost you today and who could change that.')); return c; } function renderLadder(rows,taxNote,floors,venue){ venue=venue||'pool'; const wrap=el('div','lad'); [['buy','Buying','up'],['sell','Selling','down']].forEach(([side,label,dir])=>{ const floor=side==='buy'?floors.buy:floors.sell; const col=el('div','lad-c'); const hd=el('div','lad-h lad-'+side); hd.appendChild(el('span','lad-t',label)); hd.appendChild(el('span','lad-d','price '+dir)); col.appendChild(hd); const head=el('div','lad-r lad-hr'); head.append(el('span','lad-s','size'),el('span','lad-b',''), el('span','lad-p','impact'),el('span','lad-x','you pay')); col.appendChild(head); const vals=rows.map(r=>side==='buy'?r.buyMove:r.sellMove).filter(v=>v!=null).map(Math.abs); const max=Math.max(...vals,0.0001); rows.forEach(r=>{ const mv=side==='buy'?r.buyMove:r.sellMove,cs=side==='buy'?r.buyCost:r.sellCost; const row=el('div','lad-r'); row.appendChild(el('span','lad-s','$'+nf(r.usd))); const note=side==='buy'?r.buyNote:r.sellNote; // A size the pool cannot fill has no impact figure — it is beyond the // scale, not below it. The bar is drawn full and muted; an empty bar on // the largest size read as 'no impact' (found by the audit, 2026-09-09). const t=el('span','lad-b'),bar=el('i',[side==='sell'?'sell':'',note?'dry':''].filter(Boolean).join(' ')||null); bar.style.transform='scaleX('+(note?1:mv==null?0:Math.min(1,Math.abs(mv)/max)).toFixed(4)+')'; t.appendChild(bar);row.appendChild(t); row.appendChild(el('span','lad-p'+(note?' lad-dry':impactBand(mv)),note?'runs out':signed(mv))); row.appendChild(el('span','lad-x'+(note?'':costBand(cs,floor)),cs==null?'—':cs.toFixed(2)+'%')); col.appendChild(row); }); wrap.appendChild(col); }); const box=el('div');box.appendChild(wrap); if(taxNote)box.appendChild(el('p','cd-foot',taxNote)); const dryRows=rows.filter(r=>r.buyNote||r.sellNote); if(venue==='pool'&&dryRows.length)box.appendChild(el('p','cd-foot', '“Runs out” means the pool holds less in range than that size would take: the trade would not fill, so there is no price to quote.')); box.appendChild(el('p','cd-legend', 'Colour is about cost, not quality. Green means you pay close to the unavoidable toll for this '+venue+' ('+ (floors.buy>0?floors.buy.toFixed(2)+'% on a buy, '+floors.sell.toFixed(2)+'% on a sell':'fee plus tax')+ ', payable at any size); amber is noticeably above it; red means the pool is moving under you. '+ 'It says nothing about whether the token is any good — a deep pool can still go to zero.')); return box; } // ---- tax card -------------------------------------------------------------- // The centrepiece, because it is the figure most likely to be wrong elsewhere. // Measured values win; a label is shown as a label, with its disagreement // spelled out rather than quietly averaged away. function taxCard(tax,gp,gpOk,sim){ const c=card('The transfer tax, measured', 'Not taken from a label — read off trades that actually executed. The pool reports how many tokens it moved, the token’s own transfer events report how many arrived, and the gap is what the wallet was charged. Where the window holds no trade in a direction, the same trade is simulated on the chain at this block and its gap read the same way.'); const gB=gp.buy_tax!=null&&isFinite(Number(gp.buy_tax))?Number(gp.buy_tax)*100:null, gS=gp.sell_tax!=null&&isFinite(Number(gp.sell_tax))?Number(gp.sell_tax)*100:null; // The simulated pair, when the probe could read what arrived. Second to a // real executed trade, ahead of a label: it happened on the chain, at this // block, just not with anyone's money. const sB=sim&&sim.tax&&sim.tax.buy_pct!=null?sim.tax.buy_pct:null, sS=sim&&sim.tax&&sim.tax.sell_pct!=null?sim.tax.sell_pct:null; if(tax.ok){ const mB=tax.buy!=null?tax.buy*100:null,mS=tax.sell!=null?tax.sell*100:null; c.appendChild(statRow([ {v:mB!=null?pc(mB):(sB!=null?pc(sB):(gB!=null?pc(gB):'—')),l:'Buy tax',dim:mB==null&&sB==null, tone:mB!=null?band(mB,0.01,5):(sB!=null?band(sB,0.01,5):''), s:tax.nBuy?'median of '+tax.nBuy+' executed buy'+(tax.nBuy===1?'':'s') :(sB!=null?'no buy in the window — simulated buy at this block':(gB!=null?'no buy in the window — GoPlus’s figure, unverified':'no buy in the window'))}, {v:mS!=null?pc(mS):(sS!=null?pc(sS):(gS!=null?pc(gS):'—')),l:'Sell tax',dim:mS==null&&sS==null, tone:mS!=null?band(mS,0.01,5):(sS!=null?band(sS,0.01,5):''), s:tax.nSell?'median of '+tax.nSell+' executed sell'+(tax.nSell===1?'':'s') :(sS!=null?'no sell in the window — simulated sell at this block':(gS!=null?'no sell in the window — GoPlus’s figure, unverified':'no sell in the window'))}, ])); // Two readings of the same tax, when both exist: the executed trades and // the simulation. They should agree; when they do not, both are shown. const both=[]; if(mB!=null&&sB!=null&&Math.abs(mB-sB)>0.15)both.push('buy '+pc(mB)+' from executed trades vs '+pc(sB)+' simulated'); if(mS!=null&&sS!=null&&Math.abs(mS-sS)>0.15)both.push('sell '+pc(mS)+' from executed trades vs '+pc(sS)+' simulated'); if(both.length)c.appendChild(el('p','cd-foot','The simulation read a different gap than the executed trades: '+both.join('; ')+'. A tax that changes with the wallet, the size or the block does that; the executed trades are what real wallets paid.')); const parts=[]; if(tax.spread.buy.length>1)parts.push('buys charged '+tax.spread.buy.map(x=>x+'%').join(', ')); if(tax.spread.sell.length>1)parts.push('sells charged '+tax.spread.sell.map(x=>x+'%').join(', ')); if(parts.length)c.appendChild(el('p','cd-foot','Every trade read: '+parts.join('; ')+ '. A 0% entry is normal — deployers, tax sinks and allow-listed routers are usually exempt, which is why the median is used and not the average.')); const dis=[]; if(gB!=null&&mB!=null&&Math.abs(gB-mB)>0.15)dis.push('buy '+pc(gB)+' vs '+pc(mB)+' measured'); if(gS!=null&&mS!=null&&Math.abs(gS-mS)>0.15)dis.push('sell '+pc(gS)+' vs '+pc(mS)+' measured'); if(dis.length){ const w=el('div','warn warn-soft'); w.appendChild(el('b',null,'GoPlus reports a different tax than the chain charged.')); w.appendChild(el('span',null,dis.join(' · ')+'. The figures on this page use the measured value. A label can be stale, can come from a partial simulation, or can include slippage from whatever size was simulated.')); c.appendChild(w); } }else if(sB!=null||sS!=null){ c.appendChild(statRow([ {v:sB!=null?pc(sB):(gB!=null?pc(gB):'—'),l:'Buy tax',dim:sB==null,tone:sB!=null?band(sB,0.01,5):'',s:sB!=null?'simulated buy at this block':(gB!=null?'GoPlus label, unverified':'—')}, {v:sS!=null?pc(sS):(gS!=null?pc(gS):'—'),l:'Sell tax',dim:sS==null,tone:sS!=null?band(sS,0.01,5):'',s:sS!=null?'simulated sell at this block':(gS!=null?'GoPlus label, unverified':'—')}, ])); c.appendChild(el('p','cd-foot','No executed trade to read ('+tax.reason+'), so the tax comes from a buy and a sell simulated on the chain at this block, from a fresh address: what the pair paid for the whole amount against what arrived. Real wallets may be treated differently — an exempt list, a size rule — which is why an executed trade outranks this when there is one.')); const dis=[]; if(gB!=null&&sB!=null&&Math.abs(gB-sB)>0.15)dis.push('buy '+pc(gB)+' vs '+pc(sB)+' simulated'); if(gS!=null&&sS!=null&&Math.abs(gS-sS)>0.15)dis.push('sell '+pc(gS)+' vs '+pc(sS)+' simulated'); if(dis.length){ const w=el('div','warn warn-soft'); w.appendChild(el('b',null,'GoPlus reports a different tax than the simulation charged.')); w.appendChild(el('span',null,dis.join(' · ')+'. The figures on this page use the simulated value.')); c.appendChild(w); } }else{ c.appendChild(statRow([ {v:gB!=null?pc(gB):'—',l:'Buy tax (reported)',dim:true,s:'GoPlus label, unverified'}, {v:gS!=null?pc(gS):'—',l:'Sell tax (reported)',dim:true,s:'GoPlus label, unverified'}, ])); const w=el('div','warn warn-soft'); w.appendChild(el('b',null,'Could not be measured: '+tax.reason+'.')); w.appendChild(el('span',null,(gB!=null||gS!=null) ? 'The numbers above come from GoPlus and are used in the “you pay” column, but nothing on the chain has confirmed them. Treat that column as indicative until this token trades again.' : 'No tax figure is available at all, so the “you pay” column below counts the pool fee only and is a floor, not the real cost.')); c.appendChild(w); } return c; } // ---- flags ----------------------------------------------------------------- function flagsCard(gp,gpOk,sim){ // Three openings, because the card is reached three ways: a pool with the // simulation and GoPlus, a pool without GoPlus, and a token still on its // launch curve — where there is no router to sell into and the intro used // to promise a simulation the next line then called "not run". const c=card('Can you sell it, and what the contract can do', sim&&sim.curve ?(gpOk?'On the launch curve there is no pool and no router to sell into, so no sell is simulated here: whether a sell pays out is four.meme’s own answer above. The properties below are read from the verified source by GoPlus. Properties, not a rating — a token can carry several of them and be perfectly ordinary, or carry none and still go to zero.' :'On the launch curve there is no pool and no router to sell into, so no sell is simulated here: whether a sell pays out is four.meme’s own answer above. GoPlus did not answer for this token, so the contract properties could not be checked.') :gpOk?'The sell test is ours: a sell is simulated on the chain at this block, from a fresh address with no history. The properties below are read from the verified source by GoPlus. Properties, not a rating — a token can carry several of them and be perfectly ordinary, or carry none and still go to zero.' :'GoPlus did not answer for this token, so the contract properties could not be checked. The sell test below is ours and does not depend on it; every figure above comes off the chain directly.'); const g=el('div','fg'); const chip=(state,label,note)=>{const x=el('div','f f-'+state); x.appendChild(el('b',null,label));x.appendChild(el('span',null,note));return x}; // THE SELL TEST FIRST. It is the one line a buyer will act on. A simulation // that could not run says so and is never drawn as a pass. if(sim&&sim.ok&&sim.sellable)g.appendChild(chip('ok','Sell test: goes through','A sell of '+'one part in a thousand of the pool'+' went through on the chain just now, from an address with no history. It says nothing about tomorrow: an owner with a switch can still flip it.')); else if(sim&&sim.ok&&!sim.sellable)g.appendChild(chip('bad','Sell test: REVERTED','The router refused the sell'+(sim.sell_error?': '+sim.sell_error:'')+'. That is what a honeypot looks like from outside — and also what a trading pause or a max-wallet rule looks like. Do not buy what you cannot sell.')); else g.appendChild(chip('unk','Sell test: not run',(sim&&sim.reason)||'The simulation could not run for this token.')); if(!gpOk){c.appendChild(g);return c;} const owner=(gp.owner_address||'').toLowerCase(); if(gp.owner_address==null)g.appendChild(chip('unk','Ownership not checked','GoPlus returned no owner field for this contract.')); else if(owner===NULLA||owner==='')g.appendChild(chip('ok','Ownership renounced','No owner address left on the contract.')); else if(owner===FOURMEME_MANAGER)g.appendChild(chip('on','Owned by four.meme','The owner is four.meme’s token manager ('+short(owner)+'), the contract that runs the launch curve — not a person’s wallet.')); else g.appendChild(chip('on','Owner active','Owner is '+short(owner)+'.')); if(gp.is_open_source==null)g.appendChild(chip('unk','Verification not checked','GoPlus did not report whether the source is verified.')); else if(gp.is_open_source==='1')g.appendChild(chip('ok','Source verified','The published code matches the deployed bytecode.')); else g.appendChild(chip('on','Source not verified','Nothing here can be checked against source code — including every other line in this list.')); // GoPlus's own verdict stays beside ours: two independent sell tests that // disagree are worth more than one that says nothing. if(gp.is_honeypot==='1')g.appendChild(chip('bad','Honeypot','GoPlus could not sell this token in a simulation.')); else if(gp.is_honeypot==null)g.appendChild(chip('unk','Sellability not checked by GoPlus','GoPlus ran no sell simulation for this token'+(sim?'; the sell test above is ours.':', and none was run here either.'))); // The missing ones are listed by name. Silence about a property is not the // same as the property being absent, and only one of those two is safe to // let a reader assume. const unchecked=[]; FLAGS.forEach(([k,label,note])=>{ if(gp[k]==='1')g.appendChild(chip('on',label,note)); else if(gp[k]==null)unchecked.push(label.toLowerCase()); }); c.appendChild(g); if(unchecked.length)c.appendChild(el('p','cd-foot', 'Not checked for this token ('+unchecked.length+'): '+unchecked.join(', ')+ '. GoPlus returned no value for these — that is not the same as “no”, and this page will not pretend it is. Proxy contracts in particular often come back only partly analysed.')); return c; } // ---- main render ----------------------------------------------------------- function render(d){ const o=$('sc-out');o.hidden=false;$('sc-err').hidden=true;o.textContent=''; const teaser=$('sc-what');if(teaser)teaser.hidden=true; const {gp,gpOk,addr,pool,name,symb,px,quoteUsd,quoteSym,tax,supply,burned,hop,deeper,partial}=d; // Both sides valued for real. On a V2 pair this is exactly twice the quote // side by construction; on a V3 pool the two halves are not equal and // doubling would invent liquidity that is not there. const hard=d.q*quoteUsd,tvl=hard+d.tok*px; const circ=supply!=null?supply-burned:null,mcap=circ!=null&&px?circ*px:null; // header const head=el('header','hd'); const ttl=el('div','hd-t'); ttl.appendChild(el('h2',null,symb)); ttl.appendChild(el('span','hd-n',name)); head.appendChild(ttl); const meta=el('div','hd-m'); meta.appendChild(el('span','badge',pool.kind==='v3' ? 'PancakeSwap V3 · '+(pool.fee*100).toFixed(2).replace(/0+$/,'').replace(/\.$/,'')+'% tier' : (pool.venue||'PancakeSwap V2')+' · '+(pool.fee*100).toFixed(2)+'% fee')); meta.appendChild(el('span','badge badge-q',symb+' / '+quoteSym)); if(gp.launchpad_token&&gp.launchpad_token.launchpad_name) meta.appendChild(el('span','badge badge-d','via '+gp.launchpad_token.launchpad_name)); head.appendChild(meta); const lnk=el('div','hd-l'); lnk.append(link(short(addr),'https://bscscan.com/token/'+addr), link('Pool '+short(pool.pair),'https://bscscan.com/address/'+pool.pair), link('DexScreener ↗','https://dexscreener.com/bsc/'+pool.pair), // The same reading as an agent gets it: the page proves the API works, // so the page names the API (2026-09-12). link('Same reading as JSON ↗','/api/pool-scan?address='+addr)); head.appendChild(lnk); o.appendChild(head); // The answer first. Everything after it is why. o.appendChild(verdictCard(d,pool,gp,gpOk,tax)); // headline stats o.appendChild(statRow([ {v:usd(px),l:'Price'}, {v:mcap!=null?usd(mcap):'—',l:'Market Cap',s:circ!=null?nf(circ)+' circulating':'supply unreadable'}, {v:usd(tvl),l:'Liquidity',s:'both sides of the pool'}, {v:mcap?pc(tvl/mcap*100,1):'—',l:'Liquidity / Mcap',s:'how much of the valuation is actually in the pool'}, ])); // depth // The "half of it is the token itself" framing is a CONSTANT-PRODUCT fact: a // V2 pair is 50/50 by construction, so the quote side really is a floor. A // concentrated-liquidity pool is neither balanced nor a floor — $mubarak's V3 // pool holds 36% quote, and as the price falls its positions convert toward // the token side, buying the quote out. Printing the V2 sentence over a V3 // pool is right about the number and wrong about what it means. const v3=pool.kind==='v3'; const dep=card('How deep is it really?', v3?'The two sides of a concentrated-liquidity pool are not balanced — what sits here is whatever the current price has left in range. The '+quoteSym+' side is still the half that does not depend on this token being worth anything.' :'Half of any “liquidity” headline is the token itself, valued at its own price — it shrinks exactly when it would be needed. The '+quoteSym+' side is the half that holds.'); dep.appendChild(statRow([ {v:usd(hard),l:'Hard '+quoteSym+' backing', s:nf(d.q,d.q<100?3:2)+' '+quoteSym+(v3 ?' in the pool right now — not a fixed floor: as the price falls, positions convert toward the token side' :' — keeps its value if the price falls')}, {v:mcap?pc(hard/mcap*100,1):'—',l:'Hard backing / Mcap', s:v3?'how much of the valuation is currently backed by '+quoteSym+' in range' :'the floor under the market cap'}, // "More than X" is an answer; a dash is not. On a concentrated-liquidity // pool the sweep can run out of range before the price gives way — USDC/USDT // does not move one percent for any size the quoter will price — and // printing "—" there reads as a failure to measure when the finding is that // the pool is deeper than the largest size asked about. {v:d.up!=null?usd(d.up):(d.upMin!=null?'> '+usd(d.upMin):'—'),l:'Moves the price +1%', s:d.up!=null?'a buy this size, right now':(d.upMin!=null?'deeper than the largest size quoted':'a buy this size, right now')}, {v:d.down!=null?usd(d.down):(d.downMin!=null?'> '+usd(d.downMin):'—'),l:'Moves the price −1%', s:d.down!=null?'a sell this size, right now':(d.downMin!=null?'deeper than the largest size quoted':'a sell this size, right now')}, ])); o.appendChild(dep); // Qualified by absolute depth rather than by share: say so before any figure // is read, not in a footnote under it. if(partial!=null){ const w=el('div','warn warn-soft'); w.appendChild(el('b',null,'This is one pool of several for this token.')); w.appendChild(el('span',null,'It holds '+usd(d.mineUsd)+' — about '+pc(partial*100,1)+ ' of the '+usd(d.mineUsd+d.otherLiq)+' this token has across all venues. Every figure below describes '+ 'this pool exactly and says nothing about the others. It is deep enough to be worth measuring on its own, '+ 'which is why it is shown; a trade routed by an aggregator may well take a different path.')); o.appendChild(w); } if(deeper){ const w=el('div','warn warn-soft'); w.appendChild(el('b',null,'A deeper pool exists for this token.')); const s=el('span'); s.append('You asked about this pool, so this is the one measured. But the '+ (deeper.kind==='v3'?'PancakeSwap V3 '+(deeper.fee*100).toFixed(2).replace(/0+$/,'').replace(/\.$/,'')+'% tier':'PancakeSwap V2')+ ' pool against '+deeper.sym+' holds '+usd(deeper.hard)+' on its '+deeper.sym+ ' side against this one’s '+usd(d.q*quoteUsd)+'. '); const a=el('a','lk','Scan that one instead →'); a.href='?token='+deeper.pair;a.target='_self'; s.appendChild(a); w.appendChild(s); o.appendChild(w); } if(hop&&!hop.direct){ const w=el('div','warn warn-soft'); w.appendChild(el('b',null,'Every dollar figure here is derived, not direct.')); w.appendChild(el('span',null,'This pool is quoted in $'+hop.sym+', not in BNB or a stablecoin, so $'+hop.sym+ ' had to be priced through its own BNB pool first — which holds '+nf(hop.hopBnb||0,3)+ ' BNB. Everything above is only as trustworthy as that one pool: if it is thin or stale, so are these dollars. The percentages are unaffected.')); o.appendChild(w); } o.appendChild(taxCard(tax,gp,gpOk,d.sim)); // ladder const lad=card('What a trade does to the price — and what it costs', 'Two different things, routinely confused. Impact is how far this trade alone moves the price. “You pay” is what you give up against the spot price: a worse fill because the pool moves underneath you, plus the pool fee, plus the transfer tax.'); // One direction can be measured while the other is not: a quiet pool may show // three sells and no buys inside the window. Saying "measured" for both would // then be false for half the column, so each side names its own source. const src=(m,sim)=>m?'measured':sim?'simulated at this block':'reported by GoPlus, unverified'; const taxNote=(d.taxB||d.taxS) ? 'Costs include a '+pc(d.taxB*100)+' buy tax ('+src(tax.ok&&tax.buy!=null,d.simB)+ ') and a '+pc(d.taxS*100)+' sell tax ('+src(tax.ok&&tax.sell!=null,d.simS)+ '), plus the '+(pool.fee*100).toFixed(2)+'% pool fee.' : (tax.ok&&(tax.buy!=null||tax.sell!=null)) ? 'Costs include the '+(pool.fee*100).toFixed(2)+'% pool fee only — the transfer tax measured 0% on the executed trades above, so nothing is added for it.' : (d.simB||d.simS) ? 'Costs include the '+(pool.fee*100).toFixed(2)+'% pool fee only — the transfer tax came to 0% on the buy and sell simulated at this block, so nothing is added for it.' : 'Costs include the '+(pool.fee*100).toFixed(2)+'% pool fee only — no transfer tax could be established for this token, measured or reported, so treat this column as a floor.'; // The toll: what a trade of ANY size costs before depth enters the picture. const floors={buy:(1-(1-d.taxB)*(1-pool.fee))*100, sell:(1-(1-d.taxS)*(1-pool.fee))*100}; lad.appendChild(renderLadder(d.rows,taxNote,floors)); o.appendChild(lad); // LP if(pool.kind==='v2'){ const lp=card('Who holds the LP tokens', 'Burned LP can never be withdrawn by anyone. Locked LP sits in a timelock — a promise with an expiry date, not a burn. Everything else can be pulled at any moment.'); const burnedPct=d.lpTot>0?(d.lpDead+d.lpNull)/d.lpTot*100:0; // The exchange's own share is neither burned nor anybody's to pull, so it is // taken out of the free figure rather than counted as a risk. const feePct=d.lpTot>0&&d.lpFee>0?d.lpFee/d.lpTot*100:0; const holders=(gp.lp_holders||[]).filter(x=>{const a=(x.address||'').toLowerCase(); return a!==DEAD&&a!==NULLA&&a!==(d.feeTo||'')}); const lockedPct=holders.filter(x=>x.is_locked===1).reduce((s,x)=>s+(parseFloat(x.percent)||0),0)*100; const freePct=Math.max(0,100-burnedPct-lockedPct-feePct); const big=holders.filter(x=>x.is_locked!==1).sort((a,b)=>(parseFloat(b.percent)||0)-(parseFloat(a.percent)||0))[0]; lp.appendChild(statRow([ {v:pc(burnedPct),l:'Burned',tone:burnedPct>=99?' good':burnedPct>=1?' mid':' bad',s:nf(d.lpDead+d.lpNull,2)+' of '+nf(d.lpTot,2)+' LP, at the dead address'}, {v:gpOk?pc(lockedPct):'—',l:'Locked',dim:!gpOk, s:gpOk?(lockedPct>0?'in a locker GoPlus recognises':'none in a known locker'):'needs GoPlus, which did not answer'}, {v:gpOk?pc(freePct):'—',l:'Withdrawable',dim:!gpOk, s:gpOk?(big?'largest single holder '+pc((parseFloat(big.percent)||0)*100)+' —':'held across wallets') :'on-chain, '+pc(Math.max(0,100-burnedPct-feePct))+' of the LP is simply not burned', link:gpOk&&big?{t:short(big.address),href:'https://bscscan.com/address/'+big.address}:null}, ])); // Named rather than left in the withdrawable bucket. On a pool that has run // for a while this is usually the entire unburned remainder, and reading it // as "somebody can pull this" is the wrong conclusion about the one holder // here who is not connected to the token at all. if(feePct>0){ const f=el('p','cd-foot'); f.append(pc(feePct)+' of the LP sits at '+(pool.venue||'the exchange')+'’s own protocol-fee address ('); f.appendChild(link(short(d.feeTo),'https://bscscan.com/address/'+d.feeTo,'lk')); f.append('), which the pair mints to the venue every time liquidity moves. It grows on its own as the pool '+ 'trades and belongs to the exchange, not to the token — so it is counted separately from the figure above '+ 'rather than as liquidity somebody could pull.'); lp.appendChild(f); } o.appendChild(lp); }else{ const lp=card('LP ownership does not apply here', 'This is a concentrated-liquidity pool. Liquidity is held as individual positions rather than as fungible LP tokens, so “LP burned” has no meaning at this venue — there is no LP token to burn. Depth can still leave at any time if position holders withdraw.'); o.appendChild(lp); } // other venues // Dust is not a venue. A list of six pools holding fractions of a cent tells // the reader nothing and buries the one line that might matter, so anything // under $100 — or under a thousandth of the pool being measured — is dropped // and counted instead. const dustLine=Math.max(100,hard/1000), shown=(d.others||[]).filter(x=>(x.liquidity||0)>=dustLine), hidden=(d.others||[]).length-shown.length; if(shown.length){ const ov=card('Where else it trades', 'Everything above measures the deepest pool this page can read exactly. These are the rest, as indexed by DexScreener.'); const l=el('div','vn'); shown.slice(0,6).forEach(x=>{ const r=el('div','vn-r'); r.appendChild(el('span','vn-n',x.name||'Unknown')); r.appendChild(el('span','vn-v',usd(x.liquidity||0))); r.appendChild(/^0x[a-fA-F0-9]{40}$/.test(x.pair) ? link(short(x.pair),'https://bscscan.com/address/'+x.pair,'lk dim') : el('span','lk dim','position-based')); l.appendChild(r); }); ov.appendChild(l); // The cutoff scales with the pool being measured, so it has to be named // rather than assumed: writing "$100" while actually hiding everything // under $606 states a number that is not the one used. if(hidden>0)ov.appendChild(el('p','cd-foot',hidden+' further pool'+(hidden===1?'':'s')+ ' hold'+(hidden===1?'s':'')+' less than '+usd(dustLine)+' and '+(hidden===1?'is':'are')+ ' not listed — under a thousandth of the hard '+quoteSym+' backing above, which is not a place anyone trades.')); o.appendChild(ov); }else if(Array.isArray(d.others)&&!d.others.length){ o.appendChild(card('One pool only', 'DexScreener indexes no other pool for this token. Everything tradable sits in the pool measured above.')); }else if((d.others||[]).length){ o.appendChild(card('No other venue worth naming', 'DexScreener indexes '+(d.others||[]).length+' further pool'+((d.others||[]).length===1?'':'s')+ ' for this token, each holding less than '+usd(dustLine)+'. Everything tradable sits in the pool measured above.')); } o.appendChild(tierCard(addr)); o.appendChild(rangeCard(addr)); o.appendChild(flagsCard(gp,gpOk,d.sim)); o.appendChild(el('p','dis','Pool figures are read live from BNB Chain the moment you press Scan. The transfer tax is measured from recent executed trades where possible. Contract properties come from GoPlus and are attributed as such. This page describes a pool — it does not check the deployer’s history, the holder distribution, the socials, or anything off-chain; it cannot see an upgrade that has not happened yet; and it is not advice.')); } // ---- the "not measurable here" path --------------------------------------- function renderElsewhere(gp,addr,name,symb,hard,others,otherLiq,share,hasPool){ const o=$('sc-out');o.hidden=false;$('sc-err').hidden=true;o.textContent=''; const teaser=$('sc-what');if(teaser)teaser.hidden=true; const head=el('header','hd'); const ttl=el('div','hd-t');ttl.appendChild(el('h2',null,symb));ttl.appendChild(el('span','hd-n',name)); head.appendChild(ttl); head.appendChild(frag(el('div','hd-l'),link(short(addr),'https://bscscan.com/token/'+addr), link('DexScreener ↗','https://dexscreener.com/bsc/'+addr))); o.appendChild(head); const w=el('div','warn'); w.appendChild(el('b',null,'No pool here can be measured exactly.')); w.appendChild(el('span',null,(hasPool ? 'The readable pool holds '+usd(hard)+' — '+pc(share*100)+' of the '+usd(hard+otherLiq)+' GoPlus sees across all venues. The rest sits' : 'It has no readable PancakeSwap pool. Its '+usd(otherLiq)+' of liquidity sits')+ ' in venues this page cannot quote exactly. Deriving depth from the sliver that is readable would produce a number that is not merely imprecise but wrong, so none is shown. The contract properties below are unaffected — they belong to the token, not to a venue.')); o.appendChild(w); if(others.length){ const ov=card('Where it actually trades','As reported by GoPlus.'); const l=el('div','vn'); others.slice(0,6).forEach(x=>{ const r=el('div','vn-r'); r.appendChild(el('span','vn-n',x.name||'Unknown')); r.appendChild(el('span','vn-v',usd(x.liquidity||0))); r.appendChild(/^0x[a-fA-F0-9]{40}$/.test(x.pair) ? link(short(x.pair),'https://bscscan.com/address/'+x.pair,'lk dim') : el('span','lk dim','position-based')); l.appendChild(r); }); ov.appendChild(l);o.appendChild(ov); } o.appendChild(flagsCard(gp,!!(gp.token_name||gp.dex||gp.is_open_source!=null))); o.appendChild(el('p','dis','Contract properties come from GoPlus. Not advice.')); } // ---- the four.meme launch curve -------------------------------------------- // A token that is still raising on four.meme has no pool anywhere, and this // page used to answer it with the "trades elsewhere" card and "$0 of liquidity" // — a sentence about a market that does not exist, for a token that trades all // day inside four.meme's contract. The three questions are the same as for a // pool and are answered from four.meme's own helper contract: what a trade // costs (its own quote for a buy and a sell of each size, fee included), where // the money is (in the platform's contract until the raise completes — a // readable balance, not a promise), and whether a sell would be paid out. function renderCurve(gp,addr,name,symb,cv,rows,quoteUsd){ const o=$('sc-out');o.hidden=false;$('sc-err').hidden=true;o.textContent=''; const teaser=$('sc-what');if(teaser)teaser.hidden=true; const head=el('header','hd'); const ttl=el('div','hd-t');ttl.appendChild(el('h2',null,symb));ttl.appendChild(el('span','hd-n',name)); head.appendChild(ttl); head.appendChild(frag(el('div','hd-l'),link(short(addr),'https://bscscan.com/token/'+addr), link('four.meme ↗','https://four.meme/token/'+addr))); o.appendChild(head); const q=cv.quoteSym||'the quote token'; const priceUsd=quoteUsd>0?cv.price*quoteUsd:null; const qAmt=v=>v==null?'—':(v>=100?nf(v):v>=1?nf(v,2):v>=0.01?nf(v,4):tiny(v))+' '+q; // The answer first, in the same three lines a pool gets. const c=el('section','cd vd-card'); const h=el('div','cd-h'); h.appendChild(el('h3',null,'The short answer')); h.appendChild(el('p',null,'This token is still on its four.meme launch curve. There is no PancakeSwap pool yet; every trade goes through four.meme’s contract, and that is what was asked.')); c.appendChild(h); const list=el('div','vd'); const line=(tone,hd,body)=>{const r=el('div','vd-r vd-'+tone);r.appendChild(el('b',null,hd));r.appendChild(el('span',null,body));list.appendChild(r)}; const ref=rows.slice().sort((a,b)=>Math.abs(a.usd-500)-Math.abs(b.usd-500))[0]; if(ref&&(ref.buyCost!=null||ref.sellCost!=null)){ const worst=Math.max(ref.buyCost==null?0:ref.buyCost,ref.sellCost==null?0:ref.sellCost); const toll=cv.feePct; const tone=toll>0?(worst<=toll*1.5?'good':worst<=toll*3?'mid':'bad'):'mid'; line(tone,'A $'+nf(ref.usd)+' trade costs you '+(ref.buyCost==null?'—':pc(ref.buyCost))+' to buy and '+(ref.sellCost==null?'—':pc(ref.sellCost))+' to sell.', 'four.meme’s own quote for that size right now, its '+pc(toll)+' fee included. The rest is the curve moving under the trade'+(ref.buyNote?'; the buy side: '+ref.buyNote:'')+'.'); }else{ line('unknown','What a trade costs could not be quoted.', rows.length?'The curve answered no size this page asks about.':(quoteUsd>0?'The helper contract did not answer.':'The curve is raising in '+q+', which this page cannot express in dollars.')); } line('mid',(cv.progressPct==null?'The raise is in progress':pc(cv.progressPct,1)+' of the raise is done')+' — '+qAmt(cv.raised)+' of '+qAmt(cv.maxRaising)+'.', 'The money raised sits in four.meme’s contract until the raise completes, not in the creator’s wallet. When it completes, four.meme lists the token on PancakeSwap; until then there is no liquidity to pull, because there is no pool.'); const sellRow=rows.find(r=>r.sellCost!=null); if(sellRow)line('good','A sell would be paid out right now.', 'four.meme quoted '+q+' for a sell of the size above at this block. That is the curve’s answer, not a simulation of your wallet; the token contract itself is a four.meme template.'); else line('unknown','Whether a sell would be paid out could not be checked.','The helper contract quoted no sell at any size this page asks about.'); c.appendChild(list);o.appendChild(c); // The ladder, in the pool ladder’s own columns. "Impact" here is the cost // beyond the fee, which on a curve is the price moving under the trade. if(rows.length){ const lad=card('What a trade really costs on the curve','four.meme’s quote for each size, fee included, against its last price.'); const shaped=rows.map(r=>({usd:r.usd,buyCost:r.buyCost,sellCost:r.sellCost, buyMove:r.buyCost==null?null:Math.max(0,r.buyCost-cv.feePct), sellMove:r.sellCost==null?null:-Math.max(0,r.sellCost-cv.feePct)})); const capped=rows.filter(r=>r.buyNote).map(r=>'$'+nf(r.usd)); lad.appendChild(renderLadder(shaped,capped.length?'A dash on the buy side ('+capped.join(', ')+') means the curve has less left to sell than that size would take.':null,{buy:cv.feePct,sell:cv.feePct},'curve')); o.appendChild(lad); } // The facts the lines above were made from. const f=card('The curve, as four.meme’s contract reports it','Read from TokenManagerHelper3 at this block; nothing is cached.'); f.appendChild(statRow([ {v:priceUsd!=null?usd(priceUsd):qAmt(cv.price),l:'Price',s:priceUsd!=null?qAmt(cv.price)+' per token':'per token'}, {v:qAmt(cv.raised),l:'Raised',s:'of '+qAmt(cv.maxRaising)}, {v:nf(cv.offersLeft),l:'Tokens left to sell',s:'of '+nf(cv.maxOffers)}, {v:pc(cv.feePct),l:'Platform fee',s:'on every buy and sell'}, ...(cv.launchTime?[{v:new Date(cv.launchTime*1000).toISOString().slice(0,10),l:'Launched',s:'on four.meme'}]:[]), ])); o.appendChild(f); // The router sell test has nothing to sell into here; the chip says so and // points at the curve's own answer above rather than reading as a failure. o.appendChild(flagsCard(gp,!!(gp.token_name||gp.dex||gp.is_open_source!=null), {ok:false,curve:true,reason:'Not applicable on the launch curve: there is no pool and no router to sell into. Whether a sell would be paid out is answered above, from four.meme’s own contract.'})); o.appendChild(el('p','dis','Curve figures come from four.meme’s helper contract on BNB Smart Chain; contract properties from GoPlus. Measurement, not advice.')); } // ---- orchestration --------------------------------------------------------- // Every stage sets this. It exists for one reason: a scan that ends with an // empty page and no message is the worst thing this tool can do — the reader // cannot tell whether the token is fine, broken, or whether we are. Measured at // 2 blanks in 16 runs before this net went in. Now an empty result is caught // here, named by the stage it died in, and shown as an error like any other. let stage='start'; const at=s=>{stage=s;step(s)}; async function scan(input){ stage='start'; busy(true,'identifying the address…'); try{ const askGoPlus=a=>fetch(GOPLUS+a).then(r=>r.ok?r.json():null) .then(j=>j&&j.result&&(j.result[a]||j.result[a.toLowerCase()])).catch(()=>null); // Fired against the input on the chance it IS the token, because it usually // is and this is the slow leg. If the input turns out to be a pool, the // answer describes the LP token instead — "Pancake LPs / Cake-LP", with the // wrong name, the wrong supply and the wrong tax — so it is asked again // against the real token once that is known, and this first answer dropped. let gpP=askGoPlus(input); let what; try{what=await classify(input)} catch(e){return fail('The chain did not answer.','The public BSC node refused or timed out. Nothing is cached here, so a retry in a few seconds usually works.')} // A pasted pool tells us the venue directly. Which side is "the token" is // then the only open question: it is the side that is not the quote, and // the quote is whichever side can be priced. let token,pool=null,tokDec,bnbUsd,hop,deeper=null; const base=await rpcBatch([call(BNB_PAIR,S.reserves),call(BNB_PAIR,S.token0)]); const br=res2(base[0]),bIs0=addrAt(base[1])===WBNB; bnbUsd=br?(bIs0?br[1]/br[0]:br[0]/br[1]):0; if(!(bnbUsd>0))return fail('Could not price BNB.','The reference pool read back empty, so nothing could be stated in dollars.'); if(what.kind==='v2pair'||what.kind==='v3pool'){ at('reading the pool…'); const [a,b]=[what.token0,what.token1]; const qa=QUOTES.find(([x])=>x===a),qb=QUOTES.find(([x])=>x===b); if(qa&&!qb)token=b; else if(qb&&!qa)token=a; else if(qa&&qb)token=a; else{ // Neither side is a currency we know. The quote is the one that has its // own BNB pool — $MatthewCoin/$SpaceX resolves this way. const pa=await priceToken(a,bnbUsd),pb=await priceToken(b,bnbUsd); token=(pb.usd!=null&&pa.usd==null)?a:(pa.usd!=null&&pb.usd==null)?b :((pb.hopBnb||0)>=(pa.hopBnb||0)?a:b); } const quote=token===a?b:a; const info=await rpcBatch([call(token,S.decimals),call(token,S.symbol),call(token,S.name)]); tokDec=Number(hx(info[0]))||18; hop=await priceToken(quote,bnbUsd); if(hop.usd==null)return fail('That pool cannot be priced.', 'It trades '+(decStr(info[1])||'this token')+' against '+short(quote)+ ', which has no BNB pool of its own — so there is no way to express its depth in dollars without inventing one.'); if(what.kind==='v2pair'&&!what.venue) return fail('That pool is on a venue this page does not price.', 'Its factory is '+short(what.factory||'')+', which is not one of the constant-product venues whose swap fee has been derived and verified here (PancakeSwap V2, Uniswap V2, Biswap). Applying somebody else’s fee would quietly understate what a trade costs, so no figures are shown.'); pool=what.kind==='v2pair' ?{kind:'v2',pair:input,quote,sym:hop.sym,usd:hop.usd, fee:what.venue.fee,venue:what.venue.name,factory:what.factory, tok:(addrAt(what.token0)===token?what.reserves[0]:what.reserves[1])/Math.pow(10,tokDec), q:(addrAt(what.token0)===token?what.reserves[1]:what.reserves[0])/1e18} :{kind:'v3',pair:input,quote,sym:hop.sym,usd:hop.usd,fee:what.fee/1e6,feeRaw:what.fee, sqrt:what.sqrt,tokenIs0:what.token0===token}; if(pool.kind==='v3'){ const bal=await rpcBatch([call(quote,balOf(input)),call(token,balOf(input))]); pool.q=Number(hx(bal[0]))/1e18;pool.tok=Number(hx(bal[1]))/Math.pow(10,tokDec); } pool.usd=hop.usd;pool.sym=hop.sym; if(token!==input)gpP=askGoPlus(token); // A pasted pool is honoured — you asked about that one. But the factories // are still asked what else exists, because a link often points at a side // pool while the real depth sits one fee tier over, and staying silent // about that would answer the question asked instead of the one meant. try{ const alt=(await discover(token,tokDec,bnbUsd)) .find(c=>c.pair.toLowerCase()!==pool.pair.toLowerCase()&&c.hard>(pool.q||0)*pool.usd*1.15); if(alt)deeper=alt; }catch(e){} }else{ token=input; at('asking the factories which pools exist…'); const info=await rpcBatch([call(token,S.decimals),call(token,S.symbol),call(token,S.name)]); tokDec=Number(hx(info[0]))||18; const cands=await discover(token,tokDec,bnbUsd); pool=cands[0]||null; hop={direct:true,sym:pool?pool.sym:'BNB'}; if(pool&&pool.kind==='v3'){ const s=await rpcBatch([call(pool.pair,S.slot0),call(pool.pair,S.token0)]); pool.sqrt=hx('0x'+s[0].slice(2,66));pool.tokenIs0=addrAt(s[1])===token; } } const gp=(await gpP)||{},gpOk=!!(gp.token_name||gp.dex||gp.is_open_source!=null); const nameInfo=await rpcBatch([call(token,S.symbol),call(token,S.name), call(token,S.totalSupply),call(token,balOf(DEAD)),call(token,balOf(NULLA))]); const symb=(gp.token_symbol||decStr(nameInfo[0])||'?').trim().slice(0,16), name=(gp.token_name||decStr(nameInfo[1])||'Unknown token').trim().slice(0,60), supply=nameInfo[2]?Number(hx(nameInfo[2]))/Math.pow(10,tokDec):null, burned=(Number(hx(nameInfo[3]))+Number(hx(nameInfo[4])))/Math.pow(10,tokDec); // Venues from DexScreener, which indexes the small DEXes; GoPlus's list is // the fallback and only covers what it happens to know. at('checking where else it trades…'); const dsAll=await venues(token); const others=dsAll ? dsAll.filter(x=>!pool||x.pair!==pool.pair.toLowerCase()) .map(x=>({pair:x.pair,name:x.name+(x.quote?' · '+x.quote:''),liquidity:x.liq})) : (gp.dex||[]).filter(x=>x.pair&&(!pool||x.pair.toLowerCase()!==pool.pair.toLowerCase())) .map(x=>({pair:x.pair,name:x.name||x.liquidity_type||'Unknown',liquidity:parseFloat(x.liquidity)||0})) .sort((a,b)=>b.liquidity-a.liquidity); const otherLiq=others.reduce((s,x)=>s+(x.liquidity||0),0); const hard=pool?(pool.q||0)*pool.usd:0; // "Is the pool I can measure representative?" — and the comparison must use // ONE yardstick. It used to weigh our own one-sided figure (the quote tokens // actually sitting in the pool) against GoPlus's two-sided one, which values // both halves. On a V3 pool those differ by a factor of eight: $153k of real // USDT against their $868k. Every V3 pool therefore looked like a rounding // error next to the others and got refused — $MarsCoin's did, while trading // perfectly well. So when GoPlus has a figure for OUR pool, both sides of // the ratio come from GoPlus; only when it does not do we fall back to // measuring ours against theirs, which is the imperfect case. const mineFrom=list=>{if(!list||!pool)return null; const e=list.find(x=>(x.pair||'').toLowerCase()===pool.pair.toLowerCase()); return e?(e.liq!=null?e.liq:parseFloat(e.liquidity)||0):null}; const mine=mineFrom(dsAll)!=null?mineFrom(dsAll):mineFrom(gp.dex); const share=!pool?0 :(mine!=null&&mine+otherLiq>0)?mine/(mine+otherLiq) :(otherLiq>0?hard/(hard+otherLiq):1); // A readable pool that holds a sliver of the real liquidity describes a side // pocket. $TUT keeps $2.0M in V3 and $338 in V2; a ladder off that pair says // "+68% on a $100 buy" for a token with two million dollars of depth. A // footnote does not survive a screenshot, so the ladder is not drawn at all. // Two questions, and the old guard only asked one of them. "What share of // the token's liquidity is this?" catches the side-pocket case — $v$ keeps // $19k here against $1.5M elsewhere, and a ladder off that would describe a // market nobody trades in. But share alone refused $BTCB, whose pool here // holds THIRTEEN MILLION DOLLARS and is merely one of several: perfectly // measurable, just not the whole story. So a pool also qualifies on its own // absolute depth, and when it qualifies that way the reader is told plainly // what share it is. const mineUsd=mine!=null?mine:hard*2; const deepEnough=mineUsd>=100000; // An address that is not a token at all used to land in the "trades // elsewhere" path and be told its "$0.00 of liquidity sits in venues this // page cannot quote exactly" — a sentence about a market that does not // exist. A wallet address pasted by mistake deserves to be told that. if(!pool&&!others.length&&!gpOk&&!(supply>0)&&!decStr(nameInfo[0])) return fail('That address is not a BSC token.', 'It answers nothing to symbol() or totalSupply(), has no pool at any venue this page can read, and GoPlus does not list it. A wallet address, or a contract that is not a token, looks exactly like this.'); // No pool at all: ask four.meme before concluding "trades elsewhere". A // token still raising there has no pool by design, and its market lives in // the platform's contract. A graduated token falls through to the pool path. if(!pool&&!others.length){ at('asking four.meme whether it is still on the curve…'); const cv=await curveInfo(token); if(cv&&!cv.liquidityAdded){ const quoteUsd=cv.quoteSym==='BNB'?bnbUsd:cv.quoteIsStable?1:0; const rows=await curveLadder(token,cv,quoteUsd,STEPS); renderCurve(gp,token,name,symb,cv,rows,quoteUsd); remember(token,symb); return; } } if(!pool||(share<0.25&&!deepEnough)) return renderElsewhere(gp,token,name,symb,hard,others,otherLiq,share,!!pool); const partial=share<0.25?share:null; // PRICE. For a constant-product pair the ratio of the two reserves IS the // price. For a concentrated-liquidity pool it is not, and using it anyway // put $TUT at $0.081 when the pool was quoting $0.127 — a 36% error that // then reappeared as a nonsensical "you pay 32.8%". V3 keeps its price in // sqrtPriceX96, so that is where it is read from. let px; if(pool.kind==='v3'){ const d0=pool.tokenIs0?tokDec:18,d1=pool.tokenIs0?18:tokDec, r=Math.pow(Number(pool.sqrt)/Math.pow(2,96),2)*Math.pow(10,d0-d1); px=(pool.tokenIs0?r:1/r)*pool.usd; }else px=(pool.q/pool.tok)*pool.usd; if(!(px>0))return fail('That pool is empty.','Both sides read back as zero — there is nothing to measure.'); at('measuring the tax from real trades…'); const tokenIs0=pool.kind==='v2' ? (await rpcBatch([call(pool.pair,S.token0)]).then(r=>addrAt(r[0])===token)) : pool.tokenIs0; const tax=await measureTax(token,pool.pair.toLowerCase(),tokenIs0,pool.kind); at('simulating a sell…'); const sim=await simulateRoundTrip(token,pool.pair.toLowerCase(),tokenIs0,pool.kind); const gB=Number(gp.buy_tax),gS=Number(gp.sell_tax); // Which tax the cost columns use, per direction: an executed trade first, // the simulation second, the label last, zero (and said so) never quietly. const sB=sim&&sim.tax&&sim.tax.buy_pct!=null?sim.tax.buy_pct/100:null, sS=sim&&sim.tax&&sim.tax.sell_pct!=null?sim.tax.sell_pct/100:null; const taxB=tax.ok&&tax.buy!=null?tax.buy:(sB!=null?sB:(isFinite(gB)?gB:0)), taxS=tax.ok&&tax.sell!=null?tax.sell:(sS!=null?sS:(isFinite(gS)?gS:0)), simB=!(tax.ok&&tax.buy!=null)&&sB!=null,simS=!(tax.ok&&tax.sell!=null)&&sS!=null, usedTax=tax.ok||sB!=null||sS!=null||isFinite(gB)||isFinite(gS); at('quoting trade sizes…'); let rows,up,down,upMin=null,downMin=null; if(pool.kind==='v2'){ rows=ladderV2(pool.tok,pool.q,pool.fee,taxB,taxS,px,pool.usd); up=onePctV2(pool.q,pool.fee,1.01)*pool.usd; down=onePctV2(pool.tok,pool.fee,1/0.99)/(1-taxS)*px; }else{ rows=await ladderV3(pool.pair,token,pool.quote,pool.feeRaw,tokDec,px,pool.usd, taxB,taxS,pool.sqrt,pool.tokenIs0); const oc=await onePctV3(pool.pair,token,pool.quote,pool.feeRaw,tokDec,px,pool.usd, pool.sqrt,pool.tokenIs0,taxS); up=oc.up;down=oc.down;upMin=oc.upMin;downMin=oc.downMin; } let lpTot=0,lpDead=0,lpNull=0,lpFee=0,feeTo=null; if(pool.kind==='v2'){ const lp=await rpcBatch([call(pool.pair,S.totalSupply),call(pool.pair,balOf(DEAD)), call(pool.pair,balOf(NULLA)), // The venue's own cut. A constant-product pair mints LP to the factory's // feeTo() on every liquidity event, so on any pool that has run for a // while some unburned LP belongs to the exchange, not to anybody near // the token. Listed as "withdrawable" without saying so, it reads as a // rug waiting to happen. pool.factory?call(pool.factory,S.feeTo):call(pool.pair,S.totalSupply)]); lpTot=Number(hx(lp[0]))/1e18;lpDead=Number(hx(lp[1]))/1e18;lpNull=Number(hx(lp[2]))/1e18; if(pool.factory){ feeTo=addrAt(lp[3]); if(feeTo&&feeTo!==NULLA){ const fb=await rpcBatch([call(pool.pair,balOf(feeTo))]); lpFee=Number(hx(fb[0]))/1e18; }else feeTo=null; } } render({gp,gpOk,sim,addr:token,pool,name,symb,px,q:pool.q,tok:pool.tok, quoteUsd:pool.usd,quoteSym:pool.sym,rows,up,down,upMin,downMin,tax,usedTax, supply,burned,lpTot,lpDead,lpNull,lpFee,feeTo,others,hop,deeper,taxB,taxS,simB,simS,partial,mineUsd,otherLiq}); try{history.replaceState(null,'','?token='+token)}catch(e){} remember(token,symb); }catch(e){ fail('Something went wrong reading this token.', (e&&e.message?e.message+'. ':'')+'Nothing here is cached, so trying again often works. If it keeps failing, the address may not be a BSC token or pool.'); }finally{ busy(false); again.hidden=false; // The net. If nothing was drawn and no error was shown, say so plainly // rather than leaving a blank page that looks like the token's fault. const out=$('sc-out'); if((out.hidden||!out.children.length)&&$('sc-err').hidden) fail('The scan ended without a result.', 'It stopped at “'+stage+'” without producing figures and without an error — almost always a public BSC node dropping a request mid-scan. Press Scan again; it normally works on the second try.'); } } // Accept what people actually paste: a bare address, a BscScan link, a // DexScreener link (which carries the POOL, not the token), a PancakeSwap URL. const parseInput=s=>{const m=String(s||'').match(/0x[a-fA-F0-9]{40}/);return m?m[0].toLowerCase():null}; function submit(){ const a=parseInput($('sc-in').value); if(!a)return fail('That is not a contract address.', 'Paste a BSC token address, a pool address, or a BscScan / DexScreener link that contains one.'); scan(a); } $('sc-go').addEventListener('click',submit); $('sc-in').addEventListener('keydown',e=>{if(e.key==='Enter')submit()}); // ── Scanning a second token ──────────────────────────────────────────────── // The scanned address stays in the field on purpose, but that made the next // scan a chore: select the whole thing, delete it, then paste. Three ways out, // because the field is far above the fold once a result is drawn: a cross in // the field, a button under the result, and select-on-click so a paste simply // replaces what is there. const again=$('sc-again'),clearBtn=$('sc-clear'); const showClear=()=>{clearBtn.hidden=!$('sc-in').value}; function startOver(scroll){ $('sc-in').value='';showClear(); const o=$('sc-out');o.hidden=true;o.textContent=''; $('sc-err').hidden=true;$('sc-status').textContent=''; const teaser=$('sc-what');if(teaser)teaser.hidden=false; again.hidden=true; // The URL still carried the old token; left alone, a reload would scan a // token that is no longer on the screen. try{history.replaceState(null,'',location.pathname)}catch(e){} if(scroll)$('sc-in').scrollIntoView({block:'center',behavior:'smooth'}); $('sc-in').focus(); } clearBtn.addEventListener('click',()=>startOver(false)); $('sc-again-btn').addEventListener('click',()=>startOver(true)); $('sc-in').addEventListener('input',showClear); $('sc-in').addEventListener('focus',()=>{if(!again.hidden)$('sc-in').select()}); showClear(); // THE WAY IN FOR SOMEBODY WITH NO ADDRESS. Three tokens anyone on BNB Chain // has heard of, plus whatever this visitor scanned before (kept in this // browser only, never sent anywhere). Without this the page is a headline // and an empty box, and the second visit starts from nothing again. const TRY=[['CAKE','0x0e09fabb73bd3ade0a17ecc321fd13a19e81ce82'],['USD1','0x8d0d000ee44948fc98c9b98a4fa4921476f08b0d'],['BOBAI','0x245c386dcfed896f5c346107596141e5edcbffff']]; const RECENT_KEY='sc-recent'; function recent(){try{const r=JSON.parse(localStorage.getItem(RECENT_KEY)||'[]');return Array.isArray(r)?r.filter(x=>x&&/^0x[0-9a-f]{40}$/i.test(x.a)):[]}catch(e){return[]}} function remember(addr,symb){ try{ const a=String(addr).toLowerCase(); const list=[{a,s:String(symb||'').slice(0,12),t:Date.now()}].concat(recent().filter(x=>x.a!==a)).slice(0,5); localStorage.setItem(RECENT_KEY,JSON.stringify(list)); }catch(e){} drawTry(); } function drawTry(){ const row=$('sc-try');if(!row)return; row.textContent=''; const add=(label,addr,cls)=>{const b=document.createElement('button');b.type='button';b.textContent=label;b.title=addr;if(cls)b.className=cls; b.addEventListener('click',()=>{$('sc-in').value=addr;showClear();scan(addr)});row.appendChild(b)}; const l=document.createElement('span');l.className='sc-try-l';l.textContent='Try one:';row.appendChild(l); for(const [s,a] of TRY)add(s,a); const rec=recent().filter(x=>!TRY.some(([,a])=>a===x.a)); if(rec.length){const l2=document.createElement('span');l2.className='sc-try-l';l2.textContent='· you scanned:';row.appendChild(l2); for(const x of rec)add(x.s||x.a.slice(0,6)+'…'+x.a.slice(-4),x.a,'sc-try-recent')} } drawTry(); // ---- what is on four.meme's curve right now ------------------------------- // The list a person wants before pressing buy on a launch: the tokens traded // on the curve in the last few minutes, with the raise, the price and what a // $100 trade costs each way — read from four.meme's own contract, before any // pool exists. Behind a button, not on load: it is a few dozen reads against // the public log nodes, and a visitor who came with an address in hand should // not pay for it. One tap on a row fills the field and scans it. function drawFeed(){ const box=$('sc-feed');if(!box)return; const btn=$('sc-feed-go'); const body=$('sc-feed-body'); btn.addEventListener('click',async()=>{ btn.disabled=true;btn.textContent='Reading four.meme…'; body.textContent=''; try{ const base=await rpcBatch([call(BNB_PAIR,S.reserves),call(BNB_PAIR,S.token0)]); const br=res2(base[0]),bIs0=addrAt(base[1])===WBNB; const bnbUsd=br?(bIs0?br[1]/br[0]:br[0]/br[1]):0; const f=await curveFeed({quoteUsd:{bnb:bnbUsd}}); if(!f.list.length){body.appendChild(el('p','sc-feed-note','Nothing traded on the curve in the last '+f.blocks+' blocks. Try again in a minute.'));return} const t=el('table','sc-feed-t'); const hd=el('tr');['Token','Raised','Price','$100 buy','$100 sell','Last trade'].forEach(h=>hd.appendChild(el('th',null,h))); t.appendChild(hd); for(const x of f.list){ const r=el('tr');r.tabIndex=0;r.title='Scan '+x.token; const name=el('td');const b=el('b',null,x.symbol||short(x.token));name.appendChild(b); name.appendChild(el('span','sc-feed-a',short(x.token)));r.appendChild(name); const q=x.quoteSym||'?'; r.appendChild(el('td',null,x.progressPct==null?'—':pc(x.progressPct,1)+' · '+(x.raised>=100?nf(x.raised):nf(x.raised,2))+' / '+nf(x.maxRaising)+' '+q)); r.appendChild(el('td',null,x.priceUsd!=null?usd(x.priceUsd):'raising in '+q)); r.appendChild(el('td','sc-feed-c'+(x.buyCost==null?'':costBand(x.buyCost,x.feePct)),x.buyCost==null?(x.buyNote?'more than is left':'—'):pc(x.buyCost))); r.appendChild(el('td','sc-feed-c'+(x.sellCost==null?'':costBand(x.sellCost,x.feePct)),x.sellCost==null?'—':pc(x.sellCost))); r.appendChild(el('td','sc-feed-m',(()=>{const sec=Math.round(x.blocksAgo*0.45);return x.blocksAgo<=2?'just now':sec<90?'~'+sec+' s ago':'~'+Math.round(sec/60)+' min ago'})())); const go=()=>{$('sc-in').value=x.token;showClear();scan(x.token);window.scrollTo({top:0,behavior:'smooth'})}; r.addEventListener('click',go);r.addEventListener('keydown',e=>{if(e.key==='Enter'||e.key===' '){e.preventDefault();go()}}); t.appendChild(r); } body.appendChild(t); body.appendChild(el('p','sc-feed-note','Read from four.meme’s contract at block '+nf(f.head)+': the last '+f.blocks+' blocks, newest first, tokens that have not graduated. Cost is four.meme’s own quote for that size, its fee included. Tap a row to scan it. Not a recommendation of anything.')); }catch(e){ body.appendChild(el('p','sc-feed-note','The log nodes did not answer just now. Try again in a few seconds.')); }finally{btn.disabled=false;btn.textContent='Refresh'} }); } drawFeed(); (function(){const raw=new URLSearchParams(location.search).get('token'),t=parseInput(raw); // A link that carries something that is not an address gets the same answer // a typed one does, instead of a page that silently shows nothing. if(raw&&!t){$('sc-in').value=raw;showClear();fail('That is not a contract address.','Paste a BSC token address, a pool address, or a BscScan / DexScreener link that contains one.');return} // Setting .value from script fires no input event, so the clear cross has // to be told by hand — otherwise arriving via ?token= shows an address with // no way to clear it, which is the one arrival that matters most. if(t){$('sc-in').value=t;showClear();scan(t)}})(); // Exported for scripts/dashboard-check/scanner-honeypot.mjs, which feeds this // card GoPlus answers we could not find in the wild (no analysed honeypot in // 370 tokens tried on 2 September) and pins what it draws for each. Nothing // else imports it; the page runs exactly as before. export { flagsCard }; ============================================================================== === FILE: dashboard/styles.css ============================================================================== /* Split out of index.html. As an inline