# 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. 4 files, 2717 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, ~5000 blocks 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']; export const RPC=RPCS[0], LOGS_RPC='https://bsc-rpc.publicnode.com', GOPLUS='https://api.gopluslabs.io/api/v1/token_security/56?contract_addresses=', 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){ let s='';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; // "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){ const out=[]; for(let i=0;i({jsonrpc:'2.0',id:k,method:'eth_call',params:[c,'latest']})); 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); const move=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); // price of token0 in token1 return ((tokenIs0?ratio:1/ratio)-1)*100; // ...expressed for OUR token }; 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; return {usd:u, buyMove:move(b), buyCost:gotTok!=null?(1-(gotTok*spot)/(paidQ*quoteUsd))*100:null, sellMove:move(s)!=null?-Math.abs(move(s)):null, sellCost:(gotQ!=null&&sentTok>0)?(1-(gotQ*quoteUsd)/(sentTok*spot))*100: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. 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; 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: this endpoint serves ~5,000 blocks at the head // and answers anything older with "archive requests require a personal // token". The second window the earlier build asked for was refused every // single time, which cost a round trip and bought nothing. const from=head-4999; let logs=null; const topic=kind==='v3'?[[SWAP_V3_T,SWAP_V3_UNI]]:[SWAP_T]; try{logs=await rpc('eth_getLogs',[{address:pair,topics:topic, fromBlock:'0x'+from.toString(16),toBlock:'0x'+head.toString(16)}],LOGS_RPC)}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'}; if(!logs.length){ const span=await windowSpan(from,head); return {ok:false,reason:'this pool has not traded in the last '+(span||'~5,000 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'}; return {ok:true,buy:b,sell:s,nBuy:buys.length,nSell:sells.length, 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'}} } ============================================================================== === FILE: dashboard/scanner.html ============================================================================== Pool Scanner — how deep is any BSC token's liquidity?
Tool · Pool Scanner

How deep is any BSC pool, really?

A liquidity headline is half fiction: half of it is the token itself, valued at its own price, so it shrinks exactly when it would be needed. This reads a pool the way a trader meets it — what a trade of a given size does to the price, what it actually costs once the transfer tax is measured from real trades, and whether the LP is burned, merely locked, or sitting in a wallet that can pull it tonight.

What a trade really costs Price impact and true cost side by side at six sizes from $100 to $2,500 — two things routinely confused with each other.
The transfer 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 liquidity LP burned, merely locked, or sitting in a wallet — with the largest holder named and linked.
What it will not claim If the pool cannot be measured exactly, no depth figures are shown at all. A wrong number is worse than none.

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.

BOBAI

Read live from BNB Chain in your own browser — nothing stored, nothing logged · contract properties by GoPlus · venue index by DexScreener

Made by Brain On BNB AI · Whitepaper · not financial advice

============================================================================== === 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} from './scanner-chain.js?v=12'; const $=id=>document.getElementById(id); const nf=(n,d=0)=>Number(n).toLocaleString('en-US',{minimumFractionDigits:d,maximumFractionDigits:d}); const usd=n=>n==null?'—':n>=1000?'$'+nf(n):n>=1?'$'+nf(n,2):n>=0.01?'$'+nf(n,4):'$'+n.toPrecision(3); const short=a=>a?a.slice(0,6)+'…'+a.slice(-4):'—'; // 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)?v.toFixed(3)+'%':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'); c.appendChild(el('div','st-v'+(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; } 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; } function renderLadder(rows,taxNote,floors){ 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 t=el('span','lad-b'),bar=el('i',side==='sell'?'sell':null); bar.style.transform='scaleX('+(mv==null?0:Math.min(1,Math.abs(mv)/max)).toFixed(4)+')'; t.appendChild(bar);row.appendChild(t); row.appendChild(el('span','lad-p'+impactBand(mv),signed(mv))); row.appendChild(el('span','lad-x'+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)); box.appendChild(el('p','cd-legend', 'Colour is about cost, not quality. Green means you pay close to the unavoidable toll for this pool ('+ (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){ 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.'); 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; 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):(gB!=null?pc(gB):'—'),l:'Buy tax',dim:mB==null, tone:mB!=null?band(mB,0.01,5):'', s:tax.nBuy?'median of '+tax.nBuy+' executed buy'+(tax.nBuy===1?'':'s') :(gB!=null?'no buy in the window — GoPlus’s figure, unverified':'no buy in the window')}, {v:mS!=null?pc(mS):(gS!=null?pc(gS):'—'),l:'Sell tax',dim:mS==null, tone:mS!=null?band(mS,0.01,5):'', s:tax.nSell?'median of '+tax.nSell+' executed sell'+(tax.nSell===1?'':'s') :(gS!=null?'no sell in the window — GoPlus’s figure, unverified':'no sell in the window')}, ])); 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{ 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){ const c=card('What the contract can do', gpOk?'Contract properties as read from the verified source by GoPlus. These are 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 none of these properties could be checked. Every figure above is unaffected: it comes off the chain directly.'); if(!gpOk)return c; 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}; 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 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.')); 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','GoPlus ran no sell simulation for this token.')); // 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)); head.appendChild(lnk); o.appendChild(head); // 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)); // 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=>m?'measured':'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)+ ') and a '+pc(d.taxS*100)+' sell tax ('+src(tax.ok&&tax.sell!=null)+ '), plus the '+(pool.fee*100).toFixed(2)+'% pool fee.' : '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 pool above, which is not a place anyone trades.')); o.appendChild(ov); }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(flagsCard(gp,gpOk)); 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.')); } // ---- 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.'); 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); const gB=Number(gp.buy_tax),gS=Number(gp.sell_tax); const taxB=tax.ok&&tax.buy!=null?tax.buy:(isFinite(gB)?gB:0), taxS=tax.ok&&tax.sell!=null?tax.sell:(isFinite(gS)?gS:0), usedTax=tax.ok||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,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,partial,mineUsd,otherLiq}); try{history.replaceState(null,'','?token='+token)}catch(e){} }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); // 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()}); (function(){const t=parseInput(new URLSearchParams(location.search).get('token')); if(t){$('sc-in').value=t;scan(t)}})(); ============================================================================== === FILE: dashboard/styles.css ============================================================================== /* Split out of index.html. As an inline