Oracle3
Oracle3: 예측시장 거래 엔진·MCP 서버
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기Cash close vs the Aero book, in shares. No wallet connect — execution stays on Aerodrome.
저장소 README에서 가져옴
README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
Live at afterbook.app. Cash close vs the Aero book, in shares. No wallet ever signs anything — execution stays on Aerodrome.README 전문 보기 ↗
Live at afterbook.app.
Cash close vs the Aero book, in shares, for the Coinbase tokenized stocks on Base. The tape, Lot Lab and My Lots are read-only — no wallet ever signs anything there. Two places ask your wallet to sign, both non-custodial: Buy / Sell in the Execute panel (an in-app swap from your own wallet through KyberSwap's router, see "In-app swaps" below) and Baskets (/baskets), run on Glider, where you sign an enrollment and withdrawals and send USDC from your own wallet. See "Baskets (Basis Tilt)" below.
Pages: / and /[symbol] (the tape for a stock), /today ("Today's gap"), /borrow, /build (basket builder), /baskets, /baskets/risks.
Tape: cash-market last price vs Aerodrome's on-chain mid price for the 38 Coinbase tokenized stocks Afterbook tracks (NVDAc, AAPLc, METAc, GOOGLc, AMZNc, MSFTc, MSTRc, SNDKc, SPCXc, TSLAc, PLTRc and MUc, plus the 26 Base announced as live on 2026-10-27: NFLXc, SKHYc, HIMSc, ORCLc, AMDc, AVGOc, LLYc, MRVLc and 18 non-US companies' ADRs such as SONYc, SAPc and NVOc), basis in bp, plus real pool depth. Each stock is read from its deepest USDC pool: Aero's (Aerodrome's successor) for 20 of them, the older Aerodrome factory for the 18 seeded on 2026-10-27 (see "Contract addresses"). Each row is a card with the token's icon, a 24h change and a past-N-days chart (see "UI architecture"). During pre-market and after-hours, "cash" is the live extended-hours print, not the frozen 4pm close (Yahoo's preMarketPrice/postMarketPrice meta fields when present; since about October 2026 Yahoo stopped sending them, so the tape reads today's one-minute bars with includePrePost=true during those hours and takes the newest trade after the close from the last 4 hours, lastBarAfter in lib/tape.ts; on 2026-10-28 that turned GOOGLc's −1,025 bp "gap" against the 4pm close into +23 bp against its $290 after-hours trade) — comparing on-chain against a stale close would misreport a real overnight tradfi move as basis. Overnight and at weekends (cash fully closed) the hero's gap number and sentence (and /today) are anchored to the regular-session close; in pre-market and after-hours both use the row's live extended-hours trade when it has one (todayReference in lib/todaySnapshot.ts, the same rule as heroGapBp), so the home page, /today, its card and its share text never disagree about the same instant. In pre-market/after-hours the hero labels the cash price by the row's real cashPriceType: only when Yahoo has an extended-hours print does it say "cash pre-market/after-hours price"; with no print the cash price is still the last close, so it says "Friday's close" and the card says "Cash close". The tape's cash column header and the note under "Tape" follow the same rule, computed from all rows: no row on a print → "Cash close" and "No pre-market trades yet, so the basis is measured against the last regular-session close"; every row on a print → "Cash pre-market/after-hours"; a mix → "Cash (latest)" and a note saying it is a live print where there is one and the last close otherwise. Link-preview, share-card and footer copy use plain language, not the "Aero book" jargon. A symbol reporting earnings within 5 days gets a small "Earnings in Nd" flag in the tape and a caveat in Lot Lab — a widened spread near earnings is expected, not a sign the tape's basis math is broken. A line under the Tape heading says how fresh the numbers are (TapeFreshness in app/components/Tape.tsx, with the tape table, phone cards and sort helpers; moved out of HomeClient.tsx on 2026-10-29): "Live · updated 15s ago · refreshes every 20s", or, in amber, "Delayed · last updated 3 min ago" when the tape is over 90 s old, the server is serving last known values (error), or this page's last refresh failed; it also names the stocks whose cash price is a carried-forward last good value (cashStale, the STALE tag). Only the Live/Delayed word is a live region. The column headers are sort buttons (keyboard and screen readers: aria-sort, "Sort by Gap, now highest first"), rows can be picked with Enter, and "What the columns mean" opens a short legend (Gap, Onchain price, Pool liquidity, 24h, Trend). On phones each card shows the gap, cash → onchain price and pool liquidity; its Details button opens the 24h move, shares in the pool and the 30-day line with its change in words (sparkSummary, also every small chart's label). Earnings warnings stay on the card.
Liquid vs thin split: pools with real depth (≥ $1M; at the time of writing NVDAc/AAPLc/METAc/GOOGLc/AMZNc/MSTRc/SNDKc/SPCXc — MSFTc, TSLAc, PLTRc and MUc are thin) lead the tape and hero grid; everything thinner collapses behind a "Show N thin books" toggle at reduced size. A $10k pool's basis swings hundreds of bp on noise alone — giving it the same visual weight as a $2M pool made the tape read as broken. $100k used to be the bar, but depthUsd is a pool's total real token balance, not liquidity active near the current tick — a pool can clear $100k on stale/off-range positions while still swinging hundreds of bp (MSTRc did, at ~$965k depth and +257bp), so the bar moved to where the pools actually separate into a calm tier and a still-noisy one. The split is driven by live depth, not a hardcoded symbol list, so a thin pool graduates automatically once it actually has liquidity. Landing directly on a thin symbol (URL or dropdown) gets a small "thin book · $Xk depth" chip under the sticky bar, since the hero/tape's own thin-tier demotion is otherwise the only clue.
Every tokenized stock (/stocks): all of Coinbase's tokenized stocks (173 on 2026-10-18) as a table, searchable, filterable, sortable (24h volume, value on Base, 24h move, memes) and paged 25 at a time. Each row has price on Base, 24h move and 24h volume from the token's deepest pool on any Base exchange, "On Base" (that price × tokens in circulation on Base, not the company's market cap; Dexscreener's batch token endpoint, cached 5 minutes), the verified memes priced in it (logos and a count, linking to /memes?stock=…), its icon, contract (BaseScan link) and Aerodrome USDC pool status: Tracked (the STOCKS list, linking to its page, with the tape's live depth), Small pool (an Aerodrome USDC pool too shallow to track, with its depth, the three-hour low for any pool over $250k; 46 on 2026-10-18. LLYc reads up to ~$250k, but about $200k of that is a bot's liquidity that comes and goes; its three-hour low was $29k on 2026-10-20), No pool yet, or Not checked yet (listed by Coinbase after the last daily check). Untracked tokens get no gap, no alerts and no swaps: in a pool that small one trade moves the price, so the gap is noise. The pool column comes from the daily discover-tokens job, which already measures every untracked token and saves one snapshot (directory:pools, saveDirectoryPools in lib/stockDirectory.ts); the page reads it through Next's data cache (an hour) and Coinbase's list (cached an hour), so a page view costs no chain reads for untracked tokens and at most one Redis request an hour. Listed in the sitemap; in the header nav (desktop) and the phone menu.
Memestocks (/memes, /memes/<address>): Base meme tokens whose pool is quoted in one of the tracked stock tokens (BLUECHIP/NVDAc, IPOD/AAPLc…), each with its 1h/6h/24h move next to its stock's, price, 24h volume, liquidity, the stock held in the pool, market cap and pool age. A not-dismissable warning sits above the numbers ("Any memecoin can go to zero; the stock token in the pool does not make the meme safer"), the list is sorted by volume or by move, never by our pick, and pools under $10k of liquidity sit under "thin". The list is its own allowlist (memes:pools in Redis, lib/memestocks.ts), written by the daily discover-memes job; it never touches STOCKS or BASKET_SYMBOLS. The job reads Dexscreener's pairs for every tracked stock, drops majors and stablecoins (by address, and any symbol containing USD/EUR) and other Coinbase stock tokens, and lists a pool only if it is proven on the chain: Aerodrome Slipstream and Uniswap v3 through their factories' getPool; Uniswap v4 (most of this market, 2026-10-18) through PositionManager's poolKeys hashing to the pool id ("proven"), or, for pools created outside PositionManager (launchpads), the pool existing in StateView with liquidity and a price within 3% of the pair ("price-matched"; public RPCs refuse the old logs that would prove the key). A meme must also have a KyberSwap route back to USDC (a basic can-it-be-sold check). Memes are identified by contract address: several tokens share names (three "GPU"s on 2026-10-18). Live numbers come from Dexscreener (cached 2 minutes) and the allowlist through Next's data cache (an hour), so a view costs no chain reads. Each meme page has its own share card (/card?page=meme&addr=…). Meme logos are Dexscreener's (info.imageUrl, only https on cdn.dexscreener.com, the one meme image host in the CSP's img-src), requested at 128 px: that CDN serves only some sizes and answers 96 px with a 422. /memes?stock=NVDAc opens filtered to one stock. Prices are written out in full, never as "1.554e-7"; on a meme page a price under $0.001 shows its zeros as a count, "$0.0₆1554" (six zeros after the point), the way Dexscreener does, with the full number on hover.
Hero card + compact bar: the first thing on the tape page. It opens on the day's biggest liquid gap (the same pick /today uses, chosen on the server in app/page.tsx; /NVDAc etc. still open on that stock). It holds the stock chooser, the market-session pill, the big gap number with one plain sentence ("On Aero, Strategy (MSTR) is trading 2.7% (268.2 bp) above Friday's close. Cash reopens Mon 9:30 AM ET.": the venue leads, so "on Aero" never reads as where the cash price is; percent first, because strangers read it faster, bp in brackets; under 5 bp only the bp is shown, since the percent would round to 0.00% and look like a mistake: gapSizeText in lib/format.ts, also used for the share text), cash and Aero price cards, a primary "Size a SYMBOL trade" button (scrolls to Lot Lab, where the eligibility check lives; it never opens Aerodrome directly), a Telegram alerts link (to t.me/afterbook_bot; the bot's display name is "AfterbookAlertsBot", which is not its username), and a one-time note defining Aero and "100 bp = 1%". While cash is closed the number is anchored to the actual close (same as /today), not a live extended-hours print. A compact bar with the same number sticks to the top once the hero scrolls away. A visible <h1> sits above it (2026-10-28). /today and the share cards lead with the same percent-plus-bp figure and the same sentence (2026-10-29); the cards write prices as "Cash $1,323.55 Aero $1,325.65".
No tile grid: an earlier version repeated every liquid stock as a clickable tile under the hero (the same numbers as the ticker and the table). It was removed; the tape rows below are the way to switch stock, and clicking one still jumps to Lot Lab.
Basis since close: a sparkline of the selected symbol's basis from the last cash close to now, sampled into Redis every 5 minutes by the alerts cron (recordTapeSamples in lib/tape.ts, one write per stock per tick however many serverless instances are warm; until 2026-10-18 each instance sampled off its own traffic, which helped use up the Upstash plan's monthly request cap), and it works with zero setup (just renders empty) if no Redis store is configured. Recorded for every tracked stock regardless of the liquid/thin split above — it briefly reused that same $1M gate and silently stopped recording six symbols' history until that got decoupled; a thin pool's basis history is at least as useful to someone deciding whether to trade it. The same 30-day retained history also powers two one-line stats underneath, once there's enough sampled history: mean/max basis while the cash market was closed, and an open-snap convergence stat (getOpenSnapStats, lib/history.ts) — for each trading day, compares the last sample before that day's 9:30am NY open against samples ~30/90 min after, and reports how often |basis| shrank rather than widened ("basis moved toward $0 within 30 min in X% of the last N sessions"). Requires at least 8 valid trading days in the window before showing anything, same "collecting data" honesty as the sparkline itself rather than a percentage computed off too few days.
NY session clock: cash-open / pre-market / after-hours / weekend / holiday state, so the 24/7-onchain-vs-24/5-cash gap is explicit, not implied.
Plain labels (owner, 2026-10-19): the price on Aerodrome is the "Onchain price" everywhere a person reads it (hero, tape column, /today, /build, cards), the hero's big number is "Onchain price vs cash price", and its Onchain price card is the stronger of the two (firmer edge, heavier number, "Pool liquidity" under it) because it is the price you can trade at. /borrow keeps "Aero exit" / "Aero vs oracle", which that page defines.
First screen for newcomers (2026-10-28, from an external review): a visible heading ("Stock prices on Base 24/7 next to the real market") and one plain sentence above the hero; the gap as a percent with bp beside it; a visible three-step strip (see the gap, check the pool, trade from your wallet); the "since close" chart panel hidden until it has data; the tape listing every pool of at least $250k with smaller ones behind "Show N smaller pools"; and a plain line in the trade panel when trading is off for a US or unknown region. "What the words mean" (bottom of the Tape hero, closed by default, nothing remembered between visits; "New here?" until 2026-10-28): five short definitions (tokenized stocks, with the stock on screen as the example; cash price; onchain price; the gap in bp; when a wallet is needed) and the US line. It shares its look with the tape's "What the columns mean" (.explain / .tape-legend).
Trade calculator (called Lot Lab in the code, lotLabTab; shown as "Trade calculator" since 2026-10-19): type a USDC amount (or pick a size preset — $250 / $1k / $5k / 1% of the pool), see shares out, execution price, and a price-impact curve across a log-spaced range of trade sizes ($500–$1M) — computed from the pool's live slot0()/liquidity(), not a cached reserve snapshot. The panel is split into Trade / LP / Carry tabs (Trade is the default) rather than stacking trade sizing, the depth chart, and the carry math into one long card — keeps the primary execution flow front and center and makes the tool readable on mobile. While cash is closed and the basis spread exceeds 20bp, also shows a cash-and-carry edge calculator (lib/arb.ts) in its own Carry tab: gross basis, pool fee, price impact, and a flat gas estimate netted out, then annualized over the actual time remaining until the cash market reopens — the tab itself is hidden outside that window rather than shown with nothing worth acting on. Still buy-only in the sense that this app never shorts or borrows, but the Carry tab now has both directions: a buy basis side (on-chain priced below cash — the classic setup) and a short basis / inventory-unwind side (on-chain priced above cash — for someone who already holds spot shares and wants to sell into that premium now, then rebuy the same notional in cash pre-market once it reopens). computeCashAndCarryEdge (lib/arb.ts) takes a direction param that mirrors the sign; the toggle defaults to whichever side actually has an edge for the active symbol's current basis. A delta-neutral hedge note below the calculator explains pairing either side with a same-notional perp position to strip out the stock's own price risk — links to real, individually-verified venues (Hyperliquid, Avantis, Synthetix) for checking live funding, not a live integration this app computes itself (no third-party price/data API, on purpose — see "Why the math is done the way it is"). Also shows the same amount priced as a full-range LP position (≈50/50 by value — exact for full-range concentrated liquidity, with a caveat that Aerodrome defaults new deposits to a narrower range). Below that, a liquidity-depth chart (lib/depth.ts) reconstructs active liquidity across a ±25% price window from the pool's own tick-bitmap/ticks() data — no subgraph, no indexer. Slipstream's ticks() returns 10 fields, not upstream V3's 8; until 2026-10-12 it was decoded with the V3 layout, every read failed, and the chart showed the current tick's liquidity flat across the window. The fix was checked by summing the USDC the reconstruction puts below the price against each pool's real USDC balance (within ~2% for all twelve). The chart doubles as an interactive LP range selector: drag its two handles to size a candidate range and see, live, its capital efficiency ( — derived from the same virtual-reserve equations as the impact curve, not a recalled formula), an estimated fee APR for the range (the APR a full-range unstaked position earned, read from the pool's own counters, times that multiplier; in ), the probability the range is still in range at the same 7-day horizon My Lots uses for real positions, and a figure (, ) — the standard concentrated-liquidity impermanent-loss benchmark if price reaches exactly the low or high edge, verified against the classic Uniswap V2 IL formula in the full-range limit before trusting it. Dragging itself is gated behind an explicit "Adjust range" toggle, off by default — the range stays visible either way, but a finger landing on the chart on mobile scrolls the page instead of getting captured as a drag unless that toggle is on. Handles clamp to each other and to the current price, so a dragged range always brackets it — the underlying math assumes that. Below the range metrics, an note explains what re-centering an out-of-range position actually involves and links to Gelato Web3 Functions and Bankr for building that automation yourself — Afterbook doesn't run it, for the same reason it doesn't construct swap calldata (see "Why the math is done the way it is" above). A "Copy trade" button copies the sized trade as plain text. When a symbol has real earnings-move history (see below), Lot Lab also shows an : how many days of the stock's realized volatility the pool's own liquidity concentration ('s highest-density-region construction) would justify, compared to the actual days until the next report and the historical average size of this stock's post-earnings moves (). Not implied volatility in the options-market sense — there's no options market on these pools — a liquidity-concentration proxy translated into a comparable "days" unit via the same realized-vol model used elsewhere, not a fabricated number.
My Lots: a wallet chip, summary cards (total / spot / Aerodrome LP value) and one card per holding (spot as icon, USD value and share of holdings; LP with an in-range badge and range bar). Connect a wallet — a RainbowKit picker showing every installed browser-extension wallet (Rabby, Coinbase Wallet, etc., detected live via EIP-6963) plus a manual fallback, read-only — the connection only reveals your public address, nothing is ever signed — to see spot share balances and Aerodrome LP positions across the tracked stocks, priced live. Shows your Basename instead of the raw address when one's set, resolved directly against Base's own L2Resolver — no separate mainnet RPC, no extra dependency. LP position data comes from each CL factory's own NFT position manager (Aero's 0x4857…C60b; the older factory's 0xe1f8…8b53, not the publicly documented 0x8279… one, which is bound to a different factory), plus positions staked in each pool's gauge (stakedValues); amounts and uncollected fees are computed from the pool's ticks (lib/lots.ts). Until 2026-10-27 this used Aerodrome's "Sugar" helper (positionsByFactory), which stopped returning live positions after the Aero upgrade, and Aero has none. Positions in the 12 original stocks' old pools (before the Aero move) still show, marked as in the older pool. Each LP position also shows its price range with in-range/out-of-range status, and uncollected fees (emissions and lock expiry were Sugar fields and are no longer read). The range is now also a visual bar marking where the current price sits, plus two estimates: fee APR for the position's own range while it is in range (from daily on-chain snapshots of the pool's own feeGrowthGlobal0X128/feeGrowthGlobal1X128 counters, which are fees per unit of unstaked in-range liquidity, already net of Slipstream's 10% unstakedFee cut; turned into a full-range APR and multiplied by the range's capital efficiency, not a third-party volume API). Until 2026-10-12 this was "fees x one snapshot's active liquidity / pool value", which swung with whatever liquidity the snapshot caught, counted staked liquidity (up to 96% of a pool) that earns no fees, and then got multiplied by capital efficiency on top of already-concentrated LPs: Lot Lab showed 6,093% for an NVDAc range that earns about 12% and the odds the price is still in range 7 days out (from realized volatility of recently-sampled on-chain price, via a standard lognormal estimate). Both are on-chain-only by design — no third-party price/volume API. In-range odds still start as "collecting data" until enough samples exist, but fee APR no longer does: computeFeeAprWithFallback (lib/feeApr.ts) falls back to summing real Swap-event volume over the most recent ~1900 blocks (confirmed live: mainnet.base.org caps at a 2,000-block range) and annualizing that window's fee per unit of liquidity (fee on each swap's input, less the unstaked cut, over the swap's liquidity), whenever there isn't yet enough feeGrowthGlobal snapshot history — verified end-to-end against real chain data for three live pools before trusting it. Labeled "prelim." in the UI so it doesn't read as the same-confidence real figure. My Lots also shows a (): the AERO staking APR for the same range ( per unit of staked liquidity, on the same full-range-times-capital-efficiency basis as the fee APR) next to the unstaked fee APR above, so the panel says outright which currently earns more. Out-of-range positions say they earn neither. AERO's own USD price is read from a real on-chain Aerodrome pool (found via the CL factory's , not hardcoded) — this app previously had no AERO price source at all and showed emissions as a raw token amount for exactly that reason; that gap's closed now. Staked liquidity's USD value can't use the same virtual-reserve math the impact curve does (that describes trade-impact behavior, not actual holdings, and overstates a concentrated position's real value) — instead it scales the pool's real, balance-based depth by the staked/total liquidity ratio, an approximation that assumes staked and unstaked positions are concentrated similarly on average. A position also gets flagged when the most recent epoch's veAERO voting incentives (bribes, from Aerodrome's ) outpaid real trading fees for that same pool/epoch — a signal that a pool's gauge APR may be governance-subsidized rather than earned from organic volume.
Visitor counts (app/layout.tsx): Vercel Web Analytics (@vercel/analytics, the <Analytics /> tag, switched on in the Vercel project's Analytics tab). It is cookieless, stores no personal data and counts page views and visitors per page and referrer; it knows nothing about wallets. The script is served from the site's own /_vercel/insights/ path, so the CSP needed no change ('strict-dynamic' covers it), and it 404s under next start/next dev locally, which is expected: only the live site can confirm it. Outbound clicks are logged separately (lib/clicks.ts).
Deposit visibility (BasketPanel.tsx): the USDC held in the user's Basis Tilt smart account is read straight from the chain every 15 seconds (balanceOf on the deposit address), so a deposit is visible as soon as it confirms. If Glider's positions still say $0.00 the panel explains that the money is the user's, only they can withdraw it, and Withdraw stays off until Glider counts it; after a send it links the transaction on Basescan. Measured 2026-10-08: a 2 USDC deposit showed on-chain at once and Glider counted it in about an hour (not at the daily run, whose nextDueAt is shown by the API but is not what triggers the count).
Base Builder Code (ERC-8021, lib/builderCode.ts): bc_hebg61t4, registered at base.dev, is appended to the calldata of every transaction the app asks a wallet to send, so Base can attribute that activity to Afterbook (the dashboard is Settings → Project Settings → Builder Code; it tracks transactions, not website visitors). The app sends three kinds: the USDC deposit on the Baskets panel, and the exact-amount approval and the swap in the Execute panel's Buy / Sell and in a /build basket buy ("In-app swaps"). Liquidity still happens on Aerodrome's own site and cannot carry the code. It is passed per call as dataSuffix (the swap, sent with sendTransaction, has it appended to its calldata with concat): a code set in createConfig is ignored by useWriteContract (wevm/wagmi #5248, open in 2026-10). Check any transaction by its calldata ending in 80218021802180218021802180218021.
Wallets on phones (2026-10-05): a phone browser has no extensions, so the RainbowKit list was useless there (before NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID was set in Vercel Production it was just a dead "Browser Wallet" and Coinbase). Now, on a phone in a normal browser (no injected provider), "Connect wallet" opens WalletConnect's own "All Wallets" window directly (WalletConnectButton.tsx calls the connector RainbowKit builds with showQrModal): a search box over hundreds of wallets, with MetaMask, Rabby, Trust, Rainbow, OKX and Zerion listed first (explorerRecommendedWalletIds in lib/wagmiConfig.ts; the ids come from WalletConnect's registry: curl 'https://api.web3modal.org/getWallets?projectId=<id>&st=appkit&sv=html-core-1.7.8&page=1&entries=5&search=<name>&chains=eip155%3A8453', with curl, not Python urllib which gets a 403); pick one, tap Open, the wallet app opens, approves and returns, as on O1 exchange. The RainbowKit "Connect a Wallet" list with its "What is a Wallet?" box no longer appears there. Desktop, and a phone inside a wallet app's own browser (injected provider present), keep the RainbowKit list; modalSize="compact" removes the "What is a Wallet?" panel on desktop (a small "New to Ethereum wallets? Learn More" footer remains). MetaMask is not in the RainbowKit list: its entry uses MetaMask's own SDK relay and did nothing when tapped on a phone, while MetaMask works through WalletConnect's list; on desktop it also clashes with Rabby (see below). The project id is public (inlined in the browser bundle, so env changes need a redeploy) and afterbook.app is on the Reown Cloud project's allowlist. The CSP (middleware.ts) allows blob: images (the wallet icons) and verify.walletconnect.org/.com frames; Google Fonts for that window stay blocked on purpose (system fonts are used).
Wallet connect mechanics: app/providers.tsx/lib/wagmiConfig.ts (wagmi + RainbowKit), with a second always-visible "Connect wallet" entry point in the header (app/components/WalletConnectButton.tsx) alongside My Lots' own — both read the same global wagmi account state, so connecting or disconnecting from either stays in sync. Disconnect calls wallet_revokePermissions (MetaMask's real revoke) before clearing local state — clearing local state alone leaves the wallet extension still considering the site authorized, so the next Connect would silently reuse the same account with no way to switch. No metaMaskWallet entry, deliberately: RainbowKit's per-wallet detection is flag-based (window.ethereum.isMetaMask), not EIP-6963/rdns, and several alternative wallets (Rabby included) set that same compatibility flag for dapp-compat purposes — with both installed, whichever wallet currently holds window.ethereum answers to "MetaMask" regardless of which one is real, and confirmed in practice (Error: MetaMask extension not found, thrown from inside the click) rather than just connecting to the wrong wallet silently. rabbyWallet's own entry checks isRabby specifically, which only Rabby sets, so it doesn't have this ambiguity; the generic "Browser Wallet" entry remains the correct fallback for a real MetaMask-only setup with no other flag-setting wallet installed. WalletConnect/mobile-wallet options (and Trust/OKX/Zerion/Rainbow, whose desktop connectors fall back to WalletConnect when their own extension isn't installed) only appear when NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID (a free Reown Cloud project id) is set — without one, that connector eagerly opens a relay socket at page load and, observed directly, spins a Chrome renderer's CPU into a runaway climb rather than failing quietly, so it's left out entirely rather than shipped half-working. Every extension-based wallet (Coinbase Wallet, Rabby, plus any other EIP-6963-announced extension via the generic "Browser Wallet" entry) works with no env var at all. The base/"Base Account" wallet RainbowKit ships by default is deliberately excluded from the wallet list (lib/wagmiConfig.ts) — it transitively imports @coinbase/cdp-sdk's Solana code path, which this EVM-only app doesn't have installed, and breaks the production build outright.
Execute: Buy / Sell the stock in-app from the connected wallet (SwapPanel.tsx, see "In-app swaps"), plus Aerodrome's own swap and add-liquidity UIs under "Or trade on Aerodrome's own site", all gated behind a non-US geo check and an eligibility checkbox. /today links to the biggest gap's Execute panel (/<SYMBOL>#execute). A one-line trade summary ("AMZNc · $2,500 · ~9.67 sh · pool 0xd03B…") sits above the eligibility checkbox, visible regardless of whether it's checked — a user shouldn't have to affirm a legal checkbox just to see what they'd be trading — with its own "Copy lot" button that copies the whole line as plain text (AMZNc $2500 ~9.67 0xd03Bc8..., full untruncated pool address) for pasting into a chat/ticket/agent prompt. Aerodrome's swap URL only accepts from/to/chain params — no amount (verified live: typing into the Sell field updates the page but never the URL, and an &amount= param is silently ignored) — so inside "Or trade on Aerodrome's own site" a "Copy $X" button copies just the bare numeric amount for Aerodrome's own amount field, and a 3-step checklist (copy → open → paste and sign) spells out the sequence. Those Aerodrome links are secondary buttons now; the panel's one primary action is the swap's Review / Buy. Below the token/pool addresses, a backing & issuer note shows the token's live totalSupply() (real, on-chain, confirmed via a direct contract read) next to the custodian claim (Alpaca Securities, ADGM-supervised, 1:1, per Coinbase's own published docs) — explicitly labeled as the issuer's stated claim, not something this app verified, since there's no on-chain reserve-attestation contract for B20 tokens the way there is for some wrapped assets (checked directly: owner()/paused() both revert on the token contract, so there's no standard admin/pause interface to read either).
Log: a session-local, timestamped feed of notable events (starting with "cash just closed" plus each liquid symbol's basis at that instant).
Telegram alerts: /alert STOCK BP (NVDA or NVDAc, any case) pings when a stock's |basis| crosses a threshold, then re-arms only once the gap is back to 80% of the level or lower (REARM_RATIO and nextBasisState in lib/alerts.ts; before 2026-10-08 it re-armed the instant the gap touched the line and a gap hovering near 50 bp sent about 14 messages in a day and a half, visible in a user's public screenshot), and a missing tape reading keeps the state so a data blip can't re-arm it; /close pings once with every liquid name's basis the moment the cash market closes.
/watch 0xADDRESS [HEALTH] watches a wallet's stock-backed USDC loans on Base, Aave V4's MAG7 spoke and every listed Morpho stock market (lib/loanPositions.ts, on-chain reads; Morpho Blue 0xBBBB…EFFCb).
nextLoanState, LOAN_REARM_FACTOR in lib/loanHealth.ts).MAX_LOAN_RULES_PER_CHAT); a second /watch replaces the first. More wallets and custom thresholds are the planned paid tier./loans shows the watched wallet's loans now./stop. Addresses are public, and the bot never asks anyone to connect a wallet or sign anything.getUserAccountData and Morpho's API to 4 decimals (checked 2026-10-14 on real borrowers).t.me/afterbook_bot, linked from the tape hero); its display name, "AfterbookAlertsBot", is not a username and does not exist.Live ticker strip under the header on every page: each stock's on-chain price with a coloured ▲/▼ for its live basis, as a slow marquee. Each item links to that stock's page (/NVDAc etc.). It pauses while a mouse hovers it (only on devices that really hover — see the note in globals.css), runs again on leave and right after a tap, and never pauses on focus or press. prefers-reduced-motion stops it.
Today's gap (/today, "Biggest gap" until 2026-10-19): the single biggest cash-vs-chain gap right now as a hero card (icon, gap, cash and onchain prices, and what it takes to trade it: price impact of a $25k clip and the size needed to move the price 50/100 bp), liquidity-behind-the-tape stat cards, and a "who mean-reverts at the open" card per stock. The mean-revert stats are cached 10 minutes (unstable_cache) because they read thousands of history samples per symbol.
Borrow (/borrow): every USDC lending market on Base that takes a tracked stock as collateral, from two lenders, in one shape (lib/lendingMarket.ts):
lib/morpho.ts): listed markets only; the market list, rates and totals come from Morpho's public GraphQL API, and the oracle price is read on-chain from each market's oracle.lib/aave.ts, since 2026-10-14): read straight from the contracts, with no API or key. The hub 0xa4d5…864E is one shared USDC pool; the "MAG7" spoke 0x1790…7B8D takes 7 stock tokens. Addresses are from Aave's address book (bgd-labs/aave-address-book, AaveV4Base.sol).
getAssetDrawnRate, RAY per year) and the borrow cap belong to the shared pool, so an Aave market's borrowUsd is null and the page shows the pool once.1 + (maxBonus − 1) × liquidationBonusFactor × (1 − liquidationFee), the smallest bonus, about 4.46% today (aaveV4LiquidationIncentiveFactor).For each market: borrow and lend rates, collateral and borrows, USDC still free to borrow (Morpho per market; Aave from the shared pool, within its borrow cap), the Aero-vs-oracle gap, and how much a liquidator could sell on Aerodrome right now and still profit (lib/liquidationRisk.ts). The card shows that as "Aero exit": a word first (aeroExit in lib/borrowSummary.ts: Deep if the pool could take the market's whole collateral twice over, Tight if just, Short if not in one go), then the amount, in neutral colours on the page and the share card (owner decision 2026-10-15: it is a worst case, not an alarm). It is a worst case (every loan liquidated at once); the page says real liquidations are usually much smaller, especially on Aave. Each card's "Check a loan" link (#check=<marketId>) opens the calculator on that market with a 40% loan. The cards (.borrow-grid) are at least 280px wide, one line per row with values right-aligned, the rate labelled beside the big number, and a rule between the loan terms and the liquidation risk; Aave cards drop the Borrowed row (the pool is shared, the note above says so). Morpho Blue's liquidation incentive is min(1.15, 1/(1 - 0.3*(1 - LLTV))) (lib/liquidationMath.ts); each market carries its own incentive, so a liquidator who sells on Aerodrome profits only while the average fill stays above oracle / incentive. Fills are simulated by walking the pool's real tick liquidity within 25% of the price (, on top of ), since only models buys within one tick. A "Check a loan" calculator (, same pure math as the API) gives the liquidation price, room to fall, and whether Aerodrome could absorb that liquidation, assuming the same liquidity per tick and the same gap at the liquidation price (depth scales with the square root of price under that assumption, not linearly). Markets with no collateral collapse to one line. "Morpho ↗" links follow the same non-US gate as Aerodrome links; Aave cards link to the spoke contract ("BaseScan ↗"), because Aave's own app link for this market is not confirmed. Aave liquidates only enough of a loan to restore its target health (1.24), so the calculator's whole-loan seizure is an upper bound there. Afterbook signs nothing on this page.
Build a basket (/build, /build/<spec>): pick 2–4 tracked stocks and whole-percent weights; the basket lives in the URL (/build/NVDAc-40_AAPLc-30_METAc-30, or /build/NVDAc_AAPLc_METAc for equal weights; lib/customBasket.ts parses and renormalizes it, and a malformed one is a real 404 via app/build/[spec]/layout.tsx). Changing the basket keeps your scroll position: the picker navigates with scroll: false inside a transition, and the route deliberately has no loading.tsx, whose full-page placeholder replaced the page on every change and made it jump to the top. It shows the basket's weighted gap with the home hero's rules and labels (close-anchored while cash is fully closed, cashPriceType-driven wording otherwise), each leg's contribution, and what buying $1k / $10k / $50k of it costs on Aerodrome, plus the largest buy under 1% total cost (lib/basketCost.ts). Costs walk each leg's slice through that pool's real tick liquidity (simulateBuy in lib/swapSim.ts), not estimateLot's single-tick math, which matches it for small sizes but understates large ones (NVDAc $100k: 14.5 bp walked vs 8.2 bp single-tick on 2026-10-12). Each basket has its own share card (buildBasketOgImage). /build redirects to the three deepest pools at equal weights. "Buy or sell it" buys or sells the basket from the visitor's wallet (see In-app swaps → Basket buys); nothing here touches Glider or Basis Tilt. In the header nav as "Build", and linked from /baskets and /today. Five links fit the phone nav down to 320px only because "Biggest gap" reads "Gap" at 420px and below (owner decision, 2026-10-12; short in SiteHeader.tsx, screen readers still get the full label). The two-row phone header now applies up to 980px: at 641–900px the one-row nav used to be cut off.
Baskets (/baskets): Afterbook Basis Tilt — see "Baskets (Basis Tilt)" below.
Notification bell: see "Notifications" below.
Public API: a versioned, documented /api/v1/tape for external consumers, separate from the unauthenticated /api/tape the site itself polls — see "Public API" below.
Crawlers: app/robots.ts (everything public is allowed, /api/ is not) and app/sitemap.ts (home, /today, /borrow, /build, /baskets, /baskets/risks and one page per stock in lib/tokens.ts, so adding a stock updates it by itself). Both build their URLs from lib/siteUrl.ts (the production alias on Vercel, shared with the layout's metadataBase).
Card route and versions (app/card/route.tsx, drawn by lib/ogImage.tsx): one route draws every card, /card?page=home|stock|today|build|borrow&sym=…&spec=…&theme=light&v=N. Every page's og:image/twitter:image points at it (shareMetadata in lib/shareMeta.ts, which also sets the page's canonical URL) and the Share panel's "Download card" uses the same URL, so the preview, the download and the screen agree. Until 2026-10-13 cards came from eight opengraph-image/twitter-image files, which can't see a link's query string: /today's card re-picked the leading stock when it was fetched (a post about METAc showed an AAPLc card, the two 0.4 bp apart), and every card was dark even when shared from the light theme. Now a shared link carries the sharer's theme (theme=light) and, on /today, the stock on screen (sym). Card version: X and Telegram cache a card per URL, so v is CARD_VERSION in lib/cardUrl.ts; bump it whenever lib/ogImage.tsx changes (.claude/skills/card-version-bump/bump.sh).
Card layout rule: keep the bottom-left corner of every card free. X draws the image's alt text as a label there (it covered the footer line until the third card version), so the footer line (CardFooter in lib/ogImage.tsx: "Afterbook never holds your funds.", since 2026-10-14. It was shortened the same day because X's title label, which grows with the page title, covered the start of a longer line on the /borrow card. Before that it said "Trades are signed in your own wallet…", and from 2026-10-13 "Trades execute on Aerodrome", untrue once in-app swaps routed through KyberSwap; before that it said "No wallet connect", untrue once Baskets connected wallets) is bottom-right.
Share cards: the OG/Twitter preview images are generated live from the same tape data (lib/ogImage.tsx). The home card shows the biggest movers among the liquid names; /<SYMBOL> shows that stock; /today shows the single biggest gap with the Afterbook mark, the stock's Coinbase icon beside its symbol (downloaded server-side, PNG signature and size checked, embedded as a data URI because Coinbase serves the PNGs as binary/octet-stream; a 3-second limit, and on any failure the same tinted ticker tile the site uses), the gap, cash and Aero prices and, when the stock has at least 8 usable sessions, the line "MSTRc's gap shrank within 30 min of the 9:30 ET open in 13 of the last 13 sessions. Past sessions, not a forecast." That is the same statistic /today and the tape show (getOpenSnapStats in lib/history.ts: per session, was |basis| smaller 30 min after the open than just before it), with the exact count and sample size, never just a percentage. It is cached 10 minutes and given 3 seconds; if the read is slow or the stock has too little history (PLTRc, MUc for now) the line is simply left out and the rest of the card renders.
Base Mini App: registered on base.dev, verified via the base:app_id meta tag in app/layout.tsx. Base split from the Farcaster Mini App spec in April 2026 — registered apps are treated as standard web apps now, so there's no manifest file or SDK call here, just the base.dev listing itself.
Light and dark themes: dark is the default, light is opt-in via the sun/moon toggle in the header (saved in localStorage), falling back to the OS setting. See "UI architecture".
The stock/USDC pools are Aerodrome Slipstream (concentrated-liquidity, Uniswap-V3-style) pools, not classic constant-product pools — confirmed on-chain (stable()/getReserves() revert, slot0()/liquidity()/tickSpacing() work). Aerodrome's documented public Quoter and SwapRouter are bound to a different CL factory than the one these pools were deployed through (pool.factory() != quoter.factory(), verified on-chain), so the standard Quoter reverts on these pools and can't be used here.
Instead, lib/quote.ts reads slot0() and liquidity() directly and uses the fact that a CL pool behaves exactly like a constant-product pool with virtual reserves (L / sqrtP, L * sqrtP) as long as a trade stays within the current tick's liquidity — that's exact math, not an approximation, but it stops being exact once a trade is large enough to cross into a different tick range (flagged as largeTradeCaveat, and shaded on the impact curve). Since 2026-10-12 every served estimate walks the real tick liquidity instead (lib/lotQuote.ts → simulateBuy in lib/swapSim.ts, over the ±25% window lib/depth.ts reads): Lot Lab, /api/quote, the MCP get_quote tool, the x402 quote and execute-link routes and /today's sizing cards. Single-tick math was off in both directions past the current tick (NVDAc $100k: 8.2 bp single-tick vs 14.5 bp walked; SPCXc $100k: 45 vs 39), with no caveat shown. largeTradeCaveat keeps its name but now means the trade ran past that window; the cost shown is then for the part that fits in it, a lower bound the UI prefixes with "≥" (the first deploy divided the full amount by only the fillable shares and showed TSLAc $500k as +30,226 bp; fixed the same day). The single-tick estimateLot remains only as the fallback if the tick scan fails. The x402 execute-link route with direction=sell used to return the buy estimate; it now sizes a sell by its dollar value at the Aero price and returns sharesIn/usdcOut from the sell-side walk. This is explicitly framed in the UI as an estimate for sizing a trade, not a firm quote — the actual fill always happens on Aerodrome's own app.
Pool depth (the tape's Depth column, and the liquid/thin split) comes from real token.balanceOf(pool) reads — deliberately not the virtual reserves used for impact math, which describe in-range liquidity depth and run much larger than what's actually deployed.
All token and pool addresses in lib/tokens.ts were verified on-chain (decimals(), token0()/token1(), fee(), factory()) and cross-checked against Dexscreener's Aerodrome pair listings — the original four on 2026-09-04, plus AMZNc/MSFTc/MSTRc/SNDKc/SPCXc/TSLAc on 2026-09-05 when Base expanded to ten tokenized stocks. All ten share the same CL factory, 0.05% fee tier, and tick spacing. PLTRc and MUc were added on 2026-10-01 after being matched against Coinbase's own public list (api.coinbase.com/v1/tokenized-stocks: contract address, decimals, multiplier()), checked on-chain (USDC is token0, fee 500, tick spacing 10) and against Dexscreener's Aerodrome listings; both pools hold roughly $0.6M, so they sit in the thin tier. 2026-10-27, the Aero move: Aerodrome and Velodrome became Aero on 2026-10-22, with a new CL factory (0x9CB2…7a76). By the 27th every tracked stock was deeper on its Aero pool than on the one the tape read (METAc $933k vs $146k, PLTRc $401k vs $13.5k) and did most of its volume there, so all twelve moved to their Aero pools, and 26 more were added the same day (Base's "more tokenized stocks are live" post): 8 with Aero pools of about $200k each, seeded that day, and 18 with about $51k each in a tick-spacing-1 pool on the older factory. Each was matched against Coinbase's list (address, decimals, US ISIN), Yahoo (a USD listing whose price agreed with the pool's) and Dexscreener (pair address). Aero pools charge a dynamic fee, read live; their gauges pay a new AERO token and have no Sugar, so My Lots reads Aero LP positions from Aero's position manager (0x4857…C60b, wallet-held plus gauge-staked) and shows no gauge APR, voting-incentive flag or emissions for them. Fee-APR snapshots are keyed by pool, so the moved stocks started a new series. Liquidity on the newer six is much thinner ($10k–$110k vs $1–2M on the original four) — that's why the tape splits them out, and why their impact curves and largeTradeCaveat trip at far smaller sizes.
Each stock token also implements the B20 asset standard's multiplier() — a fixed-point scalar the issuer can change without migrating raw balances. This isn't split-only, despite how this file used to describe it: per Base's own B20 spec, cash dividends are reinvested into additional shares by raising the same multiplier, net of withholding tax and a Coinbase fee. All tracked tokens read 1.0x today, but lib/quote.ts reads it live on every quote rather than assuming 1.0 forever, so "shares out" stays correct the moment either happens — and lib/corporateActions.ts watches for exactly that: a Telegram ping to the maintainer whenever a symbol's multiplier changes from its last known value, since that's a real corporate action worth a human glance. The active stock's page also shows the redemption ratio directly whenever it's not 1.0x.
On-chain reads go through https://mainnet.base.org, Base's documented official RPC endpoint, with a public third-party RPC as a fallback.
Earnings dates come from Nasdaq's public, undocumented api.nasdaq.com/api/calendar/earnings endpoint — no key, same "open but unofficial" trust shape as the Yahoo endpoint above. Yahoo's own calendar endpoint (quoteSummary + calendarEvents, which used to return this) now needs a crumb+cookie handshake this app doesn't have, so it wasn't an option. A ticker with no date on file (expect this for SPCX, which only IPO'd in mid-2026) just shows nothing, not an error.
lib/earningsHistory.ts separately accumulates past reports (the calendar endpoint above only ever holds the next upcoming date, overwritten as time passes) — for each report, the % move from the last close before it to the first close on or after the day after, from Yahoo's historical daily-close data (same host, a longer period1/period2 window instead of range=1d). Seeded with a one-time ~400-day backfill; the daily earnings-sync cron extends it by a few days' lag going forward, so the sample grows every quarter. A symbol with no reports yet in the window just shows no historical-average stat, not a fabricated one.
Buy / Sell in the Execute panel swaps USDC ↔ a tracked stock token from the visitor's own wallet. The trade is routed by KyberSwap's aggregator, which splits it across Aerodrome and other Base pools (prop AMMs such as Metric, Bebop, Kipseli); in the spike on 2026-10-13 it routed all twelve tokens at $1k and $10k within −5 to +8 bp of the Aerodrome-only quote. KyberSwap was chosen over 0x because it needs no account or API key (no new secret), takes no protocol fee on swaps (it keeps positive slippage), and supports an integrator fee.
/api/swap/quote (price preview, refreshed every 15 s) → Review → /api/swap/build builds the transaction for the connected wallet and checks it (checkSwapCalldata in lib/swap.ts) → the review card shows pay, estimate, minimum received, Afterbook fee, route and router → Confirm: an exact-amount approve of the router for the reviewed amount if the allowance is short; after an approval, or if the card has been open over 60 s, a fresh rebuild that is sent only if it is the same trade with a minimum at least as good as the card's (otherwise the card shows the new numbers and asks again); then the swap with the Builder Code appended → /api/swap/log. The wallet never signs a minimum lower than the one on screen.MetaAggregationRouterV2 at 0x6131B5fae19EA4f9D964eAc0408E4408b66337b5 (verified on Sourcify). The only contract a swap calls or an approval names./memes/<address>, MemeBuySection.tsx / MemeBuyPanel.tsx, lib/memeSwap.ts): the same quote → review → build → approve → swap flow and the same checkSwapCalldata, buy only. The server accepts a meme only if it is on the verified allowlist (lib/memestocks.ts) and its pool holds at least $10k right now, paid with USDC or the stock token that meme's pool is priced in (nothing else), at most $1,000 a buy (MAX_MEME_SWAP_USD), and never with Afterbook's fee (feeFor in lib/swap.ts returns none for a meme even when SWAP_FEE_BPS is set). The panel re-checks that the built transaction is this meme, this pay token and a buy, shows the share of value lost to price impact and pool fees (with a warning over 5%), and repeats the "any memecoin can go to zero" warning above the Buy button. The page needs the non-US region and a box the visitor ticks (eligibility plus "can go to zero"). The swap log records a meme buy only if the mined call's tokens and recipient match the stored build (source meme).MAX_BUY_DEPTH_SHARE, owner 2026-10-27; about $5k for the 18 seeded ~$51k pools, no change for pools over $250k; /build basket legs too, refused at the review step before any approval, and again at each leg's rebuild just before signing, which stops the run and offers the rest; sells are not depth-capped), 30 swap API calls per minute per IP (swapLimiter; KyberSwap's public limit is 3 requests/s for the whole server)./baskets offers Afterbook Basis Tilt: a basket of ten Base tokenized stocks (NVDAc, AAPLc, METAc, GOOGLc, AMZNc, MSFTc, MSTRc, SNDKc, SPCXc, TSLAc) weighted by what the on-chain books show instead of equally or by market cap. It runs on Glider's B2B API: the user's funds sit in a smart account they own on Base, Glider's automation rebalances it (swaps route through LI.FI to Aerodrome), and Afterbook only chooses the target weights. Afterbook never holds funds, keys or executes trades for baskets. Not available in the US (the same best-effort, fail-closed x-vercel-ip-country gate as the rest of the app, applied to the page and to every /api/baskets/* route). No Afterbook fee.
User flow (app/components/BasketPanel.tsx): connect wallet → review and sign one message (creates the smart account; no funds move) → send USDC from the wallet to the account (an in-app plain ERC-20 transfer with a balance check and Max) → Glider buys the basket → withdraw any time, as USDC (Glider sells) or as the tokens, with an EIP-712 signature that expires in 10 minutes. Outcomes also show as toasts, and the withdrawal status is polled from Glider's operation endpoint. Enrolling requires ticking an acknowledgement linking to /baskets/risks.
Weights (lib/baskets/weights.ts, pure): half follows real pool depth, half follows |cash-vs-chain basis| counted only for liquid pools (≥ $1M; a thin pool's basis is mostly noise). Each name is bounded to 2–25%; bounds are enforced by scaling the raw scores with a bisection so the weights always sum to exactly 1, then rounded to 2-decimal percent strings that sum to exactly 100 (largest-remainder), which is the format Glider requires. lib/baskets/tilt.ts turns the tape into weights and CAIP-19 asset ids (eip155:8453/erc20:0x…).
The basket universe is explicit. BASKET_SYMBOLS in lib/baskets/tilt.ts is the ten names above and is deliberately separate from the tracked list in lib/tokens.ts: adding a stock to the tape must never silently change what enrolled users hold. Widening the basket is a deliberate decision plus a new strategy version.
Smoothing (smoothWeights): the published weights follow an exponential moving average (alpha 0.3) of the target, and a new Glider strategy version is published only when at least 10% of the basket would change hands (MIN_TURNOVER; it was 5% until 2026-10-07, when a replay of 19 days of stored history showed 5% would have changed the basket on about 2 days in 3, roughly 90 bp a month in trading cost at ~0.9% per unit moved, versus about 3 publishes and ~50 bp at 10%) — every published version makes every enrolled portfolio trade (about 0.9% round trip in a measured $5 test), so a noisy basis must not cause daily churn. The daily job (/api/cron/basket-tilt, 22:00 UTC) saves the moving average on every scheduled run, publishes only when BASKET_AUTOPUBLISH=1, and messages TELEGRAM_ADMIN_CHAT_ID each time it publishes; ?dry=1 reports the plan, writes nothing, and shows autopublishEnabled.
Strategy settings (set once on the Glider strategy, not in code): private (isPublic: false), daily schedule, 1% slippage, 1% price impact, $1 minimum swap (Glider's own floor).
Server routes (app/api/baskets/): enroll/signature, enroll, portfolio, start, withdraw/signature, withdraw, operation. All go through guardBasketRequest (lib/baskets/guard.ts): fail-closed geo gate, config check (GLIDER_API_KEY and GLIDER_STRATEGY_ID both set — removing either is the off switch for the whole feature), then a Redis rate limiter that refuses when Redis is missing. The strategy and chain are fixed server-side, and a user's portfolio is always resolved from their owner address, never from a caller-supplied id. lib/glider.ts is the typed client (retries only on 429 and API_506; env values are trimmed because a stray space from a dashboard paste once broke enrollment on the live site).
Daily job (/api/cron/basket-tilt, 22:00 UTC): plans the next update and publishes only when BASKET_AUTOPUBLISH=1 and ?dry=1 is not set; otherwise it just reports the plan and writes nothing. It refuses to touch a strategy whose asset list is not exactly the basket names. Auto-publish is off unless the variable is set, and it is on in Production since 2026-10-07 (owner decision, after the threshold went to 10%); unsetting it and redeploying is the off switch (see the basis-tilt-ops skill).
Environment variables: GLIDER_API_KEY (a production tenant key — Glider's B2B API has no staging; test with separate keys. Production needs the six scopes strategies:read/write, enroll:write, portfolios:read/write/withdraw; a local dev key needs only strategies:read, portfolios:read, enroll:write), GLIDER_API_BASE (optional; defaults to https://api.glider.fi/v2), GLIDER_STRATEGY_ID, BASKET_AUTOPUBLISH, and DEV_GEO_COUNTRY (honoured only under next dev, so a developer machine can pass the fail-closed gate; a production build ignores it).
The bell in the header opens a small settings popover: a Live market alerts switch (on by default), a locked Your transactions — always on row, and a short Recent list. The choice is stored in localStorage only (no accounts), and the bell's dot is a status light — shown while live alerts are on.
lib/liveAlerts.ts, pure; app/components/LiveAlerts.tsx polls /api/tape every 60s while the tab is visible): a toast when a liquid pool's |gap| crosses 100 bp (it must fall back under 80 bp before it can fire again) or the cash market opens or closes. The first read after load only sets a baseline, so nothing fires for something already true. Alerts exist only while the site is open; Telegram covers the closed-tab case.app/components/notify.tsx): enroll, deposit, withdrawal progress and failures in the basket panel always show; the switch cannot turn them off.app/globals.css): dark tokens live on :root, light is a token override on :root[data-theme='light']. A small inline script in <head> (app/components/theme.ts, nonce'd to satisfy the CSP) sets data-theme before first paint from the saved choice, else the OS setting. Hard-coded greys were replaced by tokens (--inset, --surface-hover, --overlay-*), and RainbowKit switches between its dark and light themes with the app.WalletConnectButton.tsx, used in the header, My Lots and Baskets): connected, it opens an account panel (address, wallet name, copy, Basescan link, network, Disconnect) and never disconnects on a bare click. The app is Base-only (lib/wagmiConfig.ts has one chain), so on any other network the button turns amber ("Wrong network") and the panel offers a one-click "Switch to Base". The component is split into a wagmi wrapper and a props-only WalletButtonView so every state can be rendered without a wallet.TickerStrip.tsx): each item shows icon, symbol, Aero price and gap, so a stranger can tell which stock is which; items link to the stock's page.ShareButton.tsx): on the tape hero (next to "Size a … trade"), on /today, on /build/<spec>, on /borrow and on each /memes/<address> page, one identical standard secondary button (.btn btn-secondary) on both pages. It opens a small panel: "Share via…" (the browser's system share sheet, only where navigator.share exists), "Post on X" (an x.com/intent/post link with the hero sentence and the page URL), "Copy link", and "Download card" (the same /card?… PNG the link preview uses, in the theme on screen). The link is built from the page's own origin, so previews only work on the live site. Every share gets its own URL (?s=<stamp>, set when the panel opens): X caches a link's preview card by URL for about a week and the card shows live numbers, so a bare /MSTRc kept showing the first card ever fetched, contradicting the post's sentence (seen 2026-10-07: post said 91.8 bp, cached card said +278.6 bp from two days earlier). Those URLs stay crawlable (X's and Telegram's preview crawlers obey robots.txt; a Disallow: /*?s= left every shared link on X without a card until 2026-10-13), and each page's canonical URL keeps them out of search results as duplicates. The link also carries the sharer's theme and, on , the stock on screen, and "Download card" is the same image as the preview. The text is the hero sentence's wording, facts only; keep it free of advice. The per-stock cards () now carry the same icon and fade line as the card. The card (, since 2026-10-14) shows the stock collateral and USDC borrowed across Aave and Morpho, and the four biggest markets with their Aero exit (Deep / Tight / Short and the amount, neutral colours). Its numbers and sentence come from one read, , frozen in the snapshot's field. A meme card () freezes the meme's whole row in the snapshot's field (only a meme on the verified allowlist gets one) and shows its own logo from Dexscreener's CDN, or the letter tile if that can't be fetched. Until 2026-10-19 meme shares had no snapshot, and a post saying BLUECHIP −3.2% went out over a card drawn later at −7.5%. (, ): when the panel opens, the server reads the tape once, writes the post's sentence from it (, the same functions the page uses for its hero sentence) and stores that frozen tape in Redis for 30 days under the share's id, which becomes the link's . The card for that link draws from the stored tape, so the sentence and the card always show the same number; the card's header says when ("Cash market open · 10:21 ET"). Before this, the sentence came from the screen and the card from live prices seconds later: a post said 8.0 bp over a +2.8 bp card. Numbers are computed on the server, never read from the link, so editing a URL can't put made-up numbers on a card. Rate limit 20 shares per 10 minutes per IP (). Until the snapshot is back the panel shows "Freezing the numbers…"; if it fails (no Redis, rate limit, 6 s), the button falls back to the on-screen sentence and a live card.Defined in vercel.json. Every route requires Authorization: Bearer $CRON_SECRET (cronAuthorized in lib/sharedSecret.ts); with the secret unset they stay open under next dev but refuse every call in any built deployment (production, previews, next start), so a missing variable can't make them public. The maintainer-only /api/clicks/stats (outbound-click log) uses the same check.
| Route | Schedule (UTC) | What it does |
|---|---|---|
/api/cron/alerts | every 5 min | evaluates Telegram alert rules against the live tape |
/api/cron/discover-tokens | 13:00 daily | reads Coinbase's token list, finds untracked names with a USDC pool (Aero or the older Aerodrome factory, any tick spacing; the deepest counts) and real depth, sends one Telegram digest, and saves every untracked token's pool and depth for /stocks (?dry=1 sends and saves nothing) |
/api/cron/discover-memes | 13:30 daily | finds and verifies meme pools quoted in tracked stock tokens and replaces the /memes allowlist (?dry=1 saves nothing; an empty result is never saved) |
/api/cron/fee-snapshot | 06:00 daily | snapshots pool fee-growth counters for fee APR |
/api/cron/earnings-sync | 11:00 daily | refreshes earnings dates and history |
/api/cron/basket-tilt | 22:00 daily | plans the Basis Tilt update; publishes only with BASKET_AUTOPUBLISH=1 |
/api/* routes.x-vercel-ip-country) is a best-effort UX gate, not a compliance control — it's defeated by any VPN, and the UI says so. Detected-US viewers see explicit "not available" copy rather than a silently-disabled button.lib/swap.ts): Afterbook deploys no contract and holds no funds; KyberSwap builds the transaction and the server refuses it unless the decoded fields that decide the outcome match the request (router 0x6131…37b5, input and output token, recipient = the connected wallet, amount and its split, a minimum received within 0.5% of the quote, Afterbook's fee or none, no permit; the route's internal steps are KyberSwap's, bounded by the router's minimum-received check); the wallet is only ever asked to approve that router for the exact amount, never unlimited; the review card shows the checked numbers before anything is signed. checkSwapCalldata is tested with tampered calldata (wrong recipient, lower minimum, other token, other fee recipient, a permit: all refused).aerodrome.finance's swap page (a token pair, routed by Aerodrome). Add liquidity opens the stock's own pool: app.aero.xyz/deposit/8453-<pool> for an Aero pool, Aerodrome's "New deposit" page for the others.<head> carries that nonce), HSTS, X-Frame-Options: DENY, no third-party scripts or fonts (Poppins is self-hosted by ). The only third-party images are the token icons from , allowed in and loaded with ; each falls back to a letter tile if it fails.All on Base mainnet, verified on-chain (decimals()/token0()/token1()/fee()/tickSpacing()/factory(), and the factory's getPool returning the pool) against Dexscreener's pair listings — see lib/tokens.ts for the full verification note. Each stock points at its deepest USDC pool; all 38 were re-verified on 2026-10-27.
| Symbol | Token address | Pool address (USDC pair) | Factory, tick spacing |
|---|---|---|---|
| NVDAc | 0xb20000000000000000000078ee7ce2fE4908108C | 0xF668e21993f0ACd7ab00BB1cFbAeD0df28A3C8EC | Aero, 10 |
| AAPLc | 0xb200000000000000000000C2e324d24d7eEcd1fb | 0xF5FCB9CDAFcE67d50AfEf298F88E709d2B946BA6 | Aero, 10 |
| METAc | 0xb2000000000000000000008bC8786B856E61707C | 0xd072410fa13D242e1d53721Bcf7AF02e3455DC96 | Aero, 10 |
| GOOGLc | 0xb2000000000000000000002D0BA3164cc74f58B7 | 0x16c70F04C3080a127486Cdf5a4C33c066bBF47C3 | Aero, 10 |
| AMZNc | 0xb200000000000000000000d9192b6B456483C2E8 | 0x532e0F46d1e528507FC9454d5847C231f23bDDf6 | Aero, 10 |
| MSFTc | 0xB200000000000000000000Ab99cFa739E253872B | 0x2740C0faAb738F6e346e545fDE02aEF9B1A01ee7 | Aero, 10 |
| MSTRc | 0xb2000000000000000000004884b426556b92883d | 0x66DCd42E6314B287110922AE36cB2a8bdEECc153 | Aero, 10 |
| SNDKc | 0xb200000000000000000000397293Cb8cda9a10c5 | 0x2836e062a3fe75943a7F4A552E2365b98E414E75 | Aero, 10 |
| SPCXc | 0xb2000000000000000000007b9fcbd005511aCBd5 | 0x880dB7Dda4407D99508574E680d067D1164B3cFE | Aero, 10 |
| TSLAc | 0xb2000000000000000000001e800a7f5189430cD0 | 0x7A33000372f3F028f074182CCb9d16cBEEfBF920 | Aero, 10 |
| PLTRc | 0xb2000000000000000000007d16372840dF4dAbbe | 0xA0473ceD0cd1bfB857e3E04B5FA4b6aF18C5e44d | Aero, 10 |
| MUc | 0xb200000000000000000000Fd2f87532B90095211 | 0x41e21950362B1014bC629252ecFFF8fcA5F1646C | Aero, 10 |
| NFLXc | 0xb20000000000000000000058B8c947e44011dFE6 |
USDC on Base: 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913. CL factories: Aero's 0x9CB220581b91b0C83223012FE69688b92aD07a76 (Aerodrome and Velodrome became Aero on 2026-10-22; address from Aero's dromos-labs/metadex-slipstream-public deployment files) and the older Aerodrome one 0xf8f2eB4940CFE7d13603DDDD87f123820Fc061Ef (not Aerodrome's public Quoter/SwapRouter factory — see security model above). Aero pools charge a dynamic fee (0.015–0.05% on 2026-10-27), so quotes read the pool's live fee() (PoolState.feePpm); feePpm in lib/tokens.ts is only the fallback.
Needs Node 20.9 or newer (Next.js 16).
npm install
npm run dev # http://localhost:3000
npm test # Vitest unit tests for the swap path (offline, mocked)
npm run typecheck # tsc --noEmit
npm run build # production build; CI runs all three on every PR
No environment variables required to run it — the app works fully without them. .env.example lists every variable the code reads, empty; copy it to .env.local and fill only what you need. (vercel env pull would bring the project's real values, production keys included: read CLAUDE.md first.) Optionally:
KV_REST_API_URL / KV_REST_API_TOKEN (or UPSTASH_REDIS_REST_URL / UPSTASH_REDIS_REST_TOKEN) — a Redis (Upstash) REST endpoint. Enables the "basis since close" sparkline's history, Telegram alerts, and the public API's key storage. Without it, the sparkline stays empty and both features return 503.TELEGRAM_BOT_TOKEN, TELEGRAM_WEBHOOK_SECRET — enables Telegram alerts. The token comes from @BotFather; the webhook secret is any random string you generate yourself, passed to Telegram's setWebhook as secret_token and checked against the X-Telegram-Bot-Api-Secret-Token header on every incoming update (in any built deployment the webhook refuses everything if it is unset). To confirm the bot's real username, call Telegram's getMe with the token (do not print the token).CRON_SECRET — a random string Vercel automatically attaches as Authorization: Bearer <secret> on cron-triggered requests (documented pattern); secures the cron routes against being triggered by anyone else.TELEGRAM_ADMIN_CHAT_ID — maintainer-only. A daily cron diffs Coinbase's public tokenized-stock list (api.coinbase.com/v1/tokenized-stocks, 106 names on 2026-10-08, 92 on 2026-10-07, 58 on 2026-10-01; the newest Aerodrome pools are still tiny (deepest $708), none near the $250k line; the older base.org/stocks page it used to scrape lists only ten) against the tickers tracked in lib/tokens.ts, checks each untracked one for an Aerodrome USDC pool and its real depth, and sends one Telegram digest to this chat ID (plus a ready-to-paste entry for any pool over $250k). A pool over $50k on one read (well under the $250k snippet line) is judged by the lowest its depth has been in the last three hours (today's depth walked back through every deposit and withdrawal in the pool's logs, lowestRecentDepthUsd in lib/discovery.ts), for the digest and for /stocks; if those logs can't be read it gets no snippet. On 2026-10-19 a bot was moving about $200k in and out of the LLYc pool (in half the time, for up to 32 minutes; out for up to 45), so a single read reported $254k for a pool whose lowest in three hours was $39k; the next day it read $229k, under $250k, and /stocks showed that until the check started at $50k (its low was $29k). A token is reported when it first has a pool and again when that pool first becomes liquid, never repeatedly. It never edits itself — a human still reviews and adds each one. Also used to flag a real B20 change (split or dividend) on any tracked stock./api/v1/tape is a separate, versioned, documented contract for external consumers (bots, agents) — distinct from the unauthenticated /api/tape the site's own frontend polls, so internal refactors never silently break external callers.
Get a key (free, instant, no signup):
curl -X POST https://afterbook.app/api/v1/keys
Call the tape with it:
curl https://afterbook.app/api/v1/tape -H "Authorization: Bearer ab_..."
Response shape (frozen — internal-only fields like cashTicker/name/cashStale are deliberately not part of this contract):
{
"session": { "state": "after-hours", "label": "After-hours", "nyTime": "16:30" },
"asOf": 1735000000000,
"rows": [
{ "symbol": "NVDAc", "cashUsd": 218.36, "midUsd": 218.65, "basisBp": 13.5, "depthUsd": 2500000 }
]
}
Rate limit: 1 request/second per key.
https://afterbook.app/api/mcp is a remote MCP server for agents (Claude, etc.) — no key, no signup, read-only. Add it as a remote MCP server in Claude Code, Claude Desktop, or any MCP-compatible client pointed at that URL.
Three tools:
get_tape — no input. Same data as /api/v1/tape above.get_quote — { symbol, usdcIn }. Shares out, execution price, and price impact for sizing a USDC → stock trade — the same estimate Lot Lab shows, not a firm quote; the actual fill always happens on Aerodrome's own app.get_basis_stats — { symbol }. Mean/mean-absolute/max-absolute basis in bp while the cash market was closed, over the last 30 days of sampled history. null if there isn't enough history yet./api/v1/x402/tape and /api/v1/x402/quote are the same data as /api/v1/tape and /api/quote above, gated by the x402 HTTP micropayment protocol instead of an API key — $0.02 in USDC per call on Base, paid directly from an agent's own wallet via EIP-3009 transferWithAuthorization. No signup, no key, no rate-limit ceiling — payment itself is the throttle. Same $0.02 tier: /api/v1/x402/execute-link — { symbol, usdcIn, direction?: 'buy' | 'sell' }, a sized trade estimate (same shape as /api/v1/x402/quote) plus a verified Aerodrome deep link for that exact direction. Deliberately a link, not raw calldata: this app has no confirmed-working router for these Slipstream pools outside Aerodrome's own frontend (their documented public SwapRouter/Quoter are bound to a different CL factory and revert on these specific pools; their Universal Router shows zero real usage against these pools in a live on-chain sample; their classic Router.sol is built for the old stable/volatile AMM pools, not Slipstream — all checked directly on-chain before deciding not to ship calldata construction). An agent's own execution layer opens/submits the link; this route never returns calldata or touches a key. (The site's own Buy / Sell, added later, does route these pools, through KyberSwap's aggregator with its calldata checked before signing — see "In-app swaps"; it is a browser flow, not part of this API.)
This isn't exclusive data — /api/quote and every MCP tool above stay fully free and unauthenticated, on purpose. This tier's actual value is zero setup friction for an agent that would rather pay $0.02 than provision a key and live under a rate limit.
Three higher, $0.05 endpoints for agents doing heavier analysis:
/api/v1/x402/history — { symbol }. 30 days of sampled basis history (same data /api/history serves free) plus the pool's current on-chain tick-liquidity distribution (same data /api/depth serves free) — bundled into one call for backtesting agents that want both the historical mispricing signal and how thick the book was at the time./api/v1/x402/lp-range — { symbol, holdingDays?, targetProbabilityPct? }. A recommended concentrated-liquidity tick range sized from realized volatility over a target holding period (default 7 days, 70% coverage), widened when the symbol's next earnings date falls inside that window. This one has no free equivalent — it's genuinely new derived output, not a convenience bundle of already-free routes. The earnings widening is a documented flat heuristic (no options-implied-vol data source exists in this app), not a real implied move. Also returns divergenceLossAtBoundary — the impermanent-loss benchmark at each edge of that range (lib/lpRange.ts)./api/v1/x402/liquidation-capacity — { symbol, collateralShares?, borrowUsd?, marketId? }. The /borrow numbers for one stock: its Morpho and Aave V4 USDC markets (each with venue; Aave ids are aave:<reserveId>, and borrowUsd is null because the pool is shared), the Aero-vs-oracle gap, the largest liquidation Aerodrome can absorb at a profit, a sell-side impact curve, and (with a position) its liquidation price and whether that liquidation could be sold on Aerodrome. marketId defaults to the market holding the most collateral. $0.05.Afterbook never holds a signing key for this. Settlement is executed by Coinbase's hosted CDP facilitator, not by this app — Afterbook only supplies a plain receiving wallet address (X402_PAYOUT_ADDRESS) and a CDP platform API key/secret pair that authenticates this server to Coinbase's facilitator service (not a blockchain key). Payment lands directly in that address (the owner's own wallet); nothing is ever custodied here, same as everywhere else in this app, and nothing swaps or spends it automatically.
All six routes 503 with { "error": "x402 tier not configured" } until CDP_API_KEY_ID, CDP_API_KEY_SECRET, and X402_PAYOUT_ADDRESS are all set.
npx vercel
or import the repo in the Vercel dashboard. No env vars required — add the Redis ones from "Running it" above (e.g. via a Vercel-connected Upstash integration) if you want the sparkline to accumulate history in production.
MIT.
lib/lpRange.tsfeeGrowthGlobalrangeAprPctlib/lpRange.tscomputeDivergenceLossAtBoundarylib/lpRange.tslib/lpRange.tslib/earningsHistory.tseth_getLogslib/gaugeYield.tsrewardRategetPoolRewardsSugar.epochsByAddresst.me/AfterbookAlertsBotlib/swapSim.tslib/depth.tsestimateLotapp/components/BorrowCalculator.tsxSWAP_FEE_BPS (1–100) to SWAP_FEE_RECIPIENT, both required, else no fee. Always taken in USDC: on the way in for a buy, out for a sell. The review card shows it in % and dollars. Owner decision: the fee stays at 0 until a legal check on charging a fee on tokenized-stock trades./api/swap/* route too) and the eligibility checkbox./api/swap/build remembers keccak256 of every calldata it returns (2 hours, with the account and fee); /api/swap/log re-reads the hash from the chain and records it only if the mined calldata (minus the Builder Code) is one of those, it succeeded, and it was sent by that account. Fee and size come from the stored build (size capped at $25,000), so a hand-made router call with the public Builder Code cannot inflate the numbers. Wallets are stored as a short pseudonymous hash (counts unique traders; not a privacy guarantee). /api/swap/stats?days=7 (cron bearer, like /api/clicks/stats) returns swaps, unique traders, volume, fees and per-stock counts./build/<spec>, "Buy it", since 2026-10-14; BasketBuyPanel.tsx, lib/basketBuy.ts):
splitBasketUsdc: floors each leg, gives the remainder to the biggest, always sums exactly)./api/swap/build like a single buy. The review card shows every leg's estimate and an "at least" amount, which is the leg's minimum less BASKET_DRIFT_BPS (0.5%).LEG_PAUSE_MS), because on the first real basket (AMZNc + MSTRc, 2026-10-14) Rabby showed a false TRANSFER_FROM_FAILED on the second leg: its simulation counted the first swap twice. The same swap simulated fine on-chain and went through on "Review the rest". A leg is sent only if its minimum is at least the card's "at least" figure, so the wallet never signs below what was shown.BasketSellPanel.tsx, basketSellAmounts):
/api/swap/build (side sell, source: 'basket'), rebuilt before signing and sent only if it returns at least the card's "at least" USDC. A stop lists what was sold, offers "Review the rest", and says if one stock's approval is still open.source: 'basket', so /api/swap/stats reports bySource (stock panel vs basket legs). The label is what the browser sends: it can't inflate counts or volume (those still need a mined, matching build), but a caller could mislabel their own swaps. Swaps logged before this have no source and count as stock.eth_call / eth_simulateV1 of server-built buys and a sell with the Builder Code appended (fee reached the recipient: $0.20 on a $100 buy at 20 bp; nothing with the fee off); tampered calldata refused; a fake wallet in a real browser was asked for an exact approval (USDC, router, 100 USDC, Builder Code) and then the swap (to the router, Builder Code), at 390px light and desktop dark./today/card?page=stock&sym=…/today/borrowpage=borrowgetBorrowSummary()lib/borrowSummary.tsborrowpage=memememelib/shareSnapshot.tsPOST /api/sharelib/shareText.tssshareLimiterlib/brand.ts and used by the header (BrandMark.tsx, themed: sky tile in dark mode, blue in light), the favicon (app/icon.svg, hand-copied from the same path) and the share cards (lib/ogImage.tsx). Apps that bookmark the site need raster icons (the Base app showed a letter "A" with only the SVG, 2026-10-18), so there are also PNGs rendered from icon.svg: app/favicon.ico (16/32/48), app/icon1.png (512, a PNG <link rel="icon">), app/apple-icon.png (180, iOS home screen) and public/icon-192.png/icon-512.png listed in the web app manifest (app/manifest.ts). If the mark changes, update icon.svg and re-render all of them.app/components/SiteHeader.tsx): a full-width bar that condenses into a floating centred pill after about 32px of scroll (with hysteresis; desktop and tablet only — phones keep a two-row bar that scrolls away). The element keeps its height so nothing below shifts.lib/coinbaseIcons.ts): the root layout reads Coinbase's public tokenized-stock list on the server (cached six hours), keeps an icon only when the entry's contract address matches lib/tokens.ts and the URL is on metadata.coinbase.com, and passes the URLs through context. SymbolTile renders the icon or a tinted letter tile (lib/symbolStyle.ts) when it is missing or fails. The icons are the companies' own logos served by Coinbase; the footer says they belong to their owners and Afterbook is not affiliated./api/sparklines): one request for every row — up to 30 days of time-bucketed on-chain price samples plus the 24h change (reported only when a sample exists within 3 hours of 24 hours ago). All rows are drawn on one shared time axis whose length is the oldest stored sample (up to 30 days), and the tape header reads "Past Nd" with that real span, so it never claims 30 days before there are 30. A token with less than 3 days of its own history shows a small "New · Nd" label instead of a chart (a line stretched across the axis would pass for a long trend); from 3 days it draws only the part of the axis it covers. A token's 24h change uses the sample nearest to 24 hours ago within a 3-hour tolerance, so for a day-old token it is measured over slightly under 24 hours.select and text input inside <main> (so components like the compact .sticky-symbol-select can still override it); the RainbowKit modal portals outside <main> and is untouched. Interactive elements without their own ring get a fallback :focus-visible outline.next/fontmetadata.coinbase.comimg-srcreferrerPolicy="no-referrer"GLIDER_API_KEY) — credentials for this server, never tied to a user or a wallet, and never sent to the browser (every /api/baskets/* route calls Glider from the server and resolves a user's portfolio from their address, never from a caller-supplied id). A full git-history secret scan was clean on 2026-10-13 (the only long hex strings are public transaction hashes). npm audit --omit=dev the same day: no critical, no high, 16 moderate. npm's suggested fix is wagmi 3, but RainbowKit (latest 2.2.11) only supports wagmi 2, so instead package.json overrides pin the patched versions inside the wallet libraries: ws@8 → ^8.21.0 (the high: memory disclosure / DoS; WalletConnect's v7 copy is untouched, it isn't affected) and uuid → ^11.1.1. The 16 moderates left are one advisory, decode-uri-component 0.2.2 under WalletConnect's query-string 7: its fix (0.5.0) is ESM-only and would break that CommonJS require, and the worst case is a visitor's own tab hanging on a malformed WalletConnect link. They go away when RainbowKit supports wagmi 3. After touching the overrides, re-check the phone and desktop Connect wallet windows (verify skill). Next.js was moved to 16.3.8 the same day for a critical next/og advisory. 2026-10-27 audit: Next.js 16.3.8 → 16.4.2 (two criticals, remote code execution through the image optimizer, which is on even though the app uses no next/image, and through use cache, plus a Server Actions DoS; all fixed in 16.4.1), @modelcontextprotocol/server 2.0.0 → 2.3.1 (two highs on /api/mcp: one HTTP client's messages delivered to another, and memory exhaustion from one request), zod 4.6.5 → 4.6.6 and a follow-redirects override to ^1.16.1. Left: 0 critical, 20 high and 7 moderate, all inside the x402 and wallet libraries. The highs are one zod advisory (memory exhaustion on oversized arrays) in the zod 3 copies those libraries bundle, which has no zod 3 fix; it goes away when @coinbase/x402, @x402/* and WalletConnect/Reown move to zod 4. The moderates are decode-uri-component (above), fast-uri and hono, also from those libraries.0x67Ba8dBeDC6D7f93f0F5967Aa2420715f1edC756| Aero, 10 |
| SKHYc | 0xb200000000000000000000187f7071D6E321a7d2 | 0xEBC746A47ebb274de1dbDC9a465E6C5C2301C5DA | Aero, 10 |
| HIMSc | 0xB20000000000000000000043a599976181Bcf336 | 0x41D7793FAa4Ca181B6f021423fF3F9015C00b2ff | Aero, 10 |
| ORCLc | 0xb200000000000000000000347AFbA223D7B6b63C | 0x62B5452E80E50B047dcF6dd3Fb6a1D266C678eB5 | Aero, 10 |
| AMDc | 0xB2000000000000000000000d8Ce462E99ee7A47B | 0x138Cb591C4A7D854D796E1906af8228d84CB7642 | Aero, 10 |
| AVGOc | 0xB200000000000000000000Fc737aeA6196aB5a4c | 0x086A1447D0851F0aeA9D09D6c547fEC8E97be413 | Aero, 10 |
| LLYc | 0xB200000000000000000000f1a0F91e34892E4718 | 0x2b7ee4623ec6AfbC1e587b2668c0ED8F3ff44949 | Aero, 10 |
| MRVLc | 0xB200000000000000000000eC3c4c7395Cc609813 | 0x611ab290422d534D9595b01e648911fefA963137 | Aero, 10 |
| SONYc | 0xb200000000000000000000a26326a922FFA87f6C | 0x26Dd2AC81eA9a212afC84848893EdC21c5A76FDC | Aerodrome, 1 |
| BILIc | 0xb200000000000000000000ca7C6D1438e7245eb6 | 0x32A3cE6C3a8D6c24C4A51A5cF2869410CbF4D637 | Aerodrome, 1 |
| SAPc | 0xb200000000000000000000da6F64c306234127E0 | 0x21313A06B7f8ed38D8bC1fb8Be2fBD847634A6ec | Aerodrome, 1 |
| SANc | 0xB20000000000000000000017BB66E9301E913A99 | 0xE531069DF73530bf59c954F3718945D5fAA2e7dF | Aerodrome, 1 |
| NOKc | 0xB2000000000000000000005ac674f707E357409c | 0xc541eAc49f32D46C1E2f88952f22215Ef0f2Db71 | Aerodrome, 1 |
| PDDc | 0xB200000000000000000000c8A7223510467D8563 | 0x84608BE6d655ee05032303FE7f0D6F3A51c2CF51 | Aerodrome, 1 |
| LIc | 0xb200000000000000000000D61eFD9F54A23ef9eB | 0xD71b2D9cB3532477Df77AE26e780963CCe784a0c | Aerodrome, 1 |
| BIDUc | 0xb200000000000000000000d2b7D9aeE52F6C6BEf | 0xD47b8F13ffC3198148B138D3f07CC89C92042950 | Aerodrome, 1 |
| VALEc | 0xB20000000000000000000063376b142C0764B489 | 0x0F48336b76ee52Fa6bb19edf5AF358529Df57574 | Aerodrome, 1 |
| BUDc | 0xB200000000000000000000c36ce081Dd09e86cA2 | 0x5ee52963d3d33196a47Fad722ABB897E0FA395d9 | Aerodrome, 1 |
| NIOc | 0xb2000000000000000000002D2B5dc53c61f1bA06 | 0x484cc2CA594ced3fE38362FE950C60cE66f53F6D | Aerodrome, 1 |
| ARMc | 0xB20000000000000000000026215D755356E5043f | 0x9A0F4Fd0766Ccdb792bA58fE631aA7720097623c | Aerodrome, 1 |
| INFYc | 0xb200000000000000000000F6b0417af5F52341FC | 0x356cC16Fe38A6ad0eB25e655759dD963F8D674BF | Aerodrome, 1 |
| ULc | 0xB200000000000000000000349ef3Aa93a0897476 | 0x013FCB940735d17c81D3B91557aCC62Da723d9D5 | Aerodrome, 1 |
| WRDc | 0xb200000000000000000000e88EFe88D8aDE3f0dA | 0xbc440AFf9A79cCed69d5B21c7B71e60BD0278EFB | Aerodrome, 1 |
| NVOc | 0xb2000000000000000000008D5328e9208773dC58 | 0xc1ba39D668F14Eee2462CA5877402e4a7c4d1f34 | Aerodrome, 1 |
| SEc | 0xB2000000000000000000003088798F6d0722AaAC | 0x0274e5078033cB3fE788cf76bb97150FD9bc4547 | Aerodrome, 1 |
| HSAIc | 0xb200000000000000000000f3049F4aa834b23b64 | 0xDc8D2C31de17bf4dF00C05E4E132BCEA22F24c3b | Aerodrome, 1 |
lib/tokens.tsmultiplier()GLIDER_API_KEY, GLIDER_STRATEGY_ID (and optionally GLIDER_API_BASE, BASKET_AUTOPUBLISH) — enable Baskets; see "Baskets (Basis Tilt)". Without the first two the /baskets enroll panel stays off and every /api/baskets/* route answers 503. DEV_GEO_COUNTRY (e.g. DE) works only under next dev.SWAP_FEE_BPS, SWAP_FEE_RECIPIENT — Afterbook's fee on in-app swaps (1–100 bp, and a public receiving address). The fee is off unless both are set and valid, and is always taken in USDC. See "In-app swaps".CDP_API_KEY_ID, CDP_API_KEY_SECRET, X402_PAYOUT_ADDRESS — enables the pay-per-call agent API (see "Pay-per-call agent API (x402)" below). The CDP keys are Coinbase Developer Platform platform credentials (from portal.cdp.coinbase.com), not a blockchain signing key — they authenticate this server to Coinbase's facilitator service, which is what actually settles payment on-chain. X402_PAYOUT_ADDRESS is any wallet address you control; USDC lands there directly. Without all three set, every /api/v1/x402/* route returns 503.