Estimated buy: about {fmtCompact(buyPreview.tokensOut, 0)} {symbolClean || "tokens"}{" "}
({fmtPct(buyPreview.pctOfSupply)} of supply{buyUsd ? ` · ≈ ${fmtUsd(buyUsd)}` : ""}). Estimated market cap after your buy:{" "}
- {fmtMcap(buyPreview.fdvAfter)}. Includes price impact and the pool fee; the exact amount is quoted on-chain right before the buy.
+ {cap(buyPreview.fdvAfter).main} · {cap(buyPreview.fdvAfter).detail}. Includes price impact and the pool fee; the exact amount is quoted on-chain right before the buy.
) : suggestion.reason === "insufficient" ? (
Suggested {fmtQuoteUnits(Number(defaultFirstBuy(quote)), quote.decimals)} {quote.symbol}, but this wallet holds only gas. The launch stays free; you can buy on the token page later.
diff --git a/app/src/components/launchpad/market-cap-sites.test.ts b/app/src/components/launchpad/market-cap-sites.test.ts
new file mode 100644
index 00000000..6d3fdd15
--- /dev/null
+++ b/app/src/components/launchpad/market-cap-sites.test.ts
@@ -0,0 +1,29 @@
+import assert from "node:assert/strict";
+import { readFileSync } from "node:fs";
+import test from "node:test";
+
+/** Every market-cap site renders the dollar-first display and never drops the "no USD price" mark (CodeRabbit, PR #27). */
+const read = (p: string) => readFileSync(new URL(p, import.meta.url), "utf8");
+
+test("the home row shows the quote detail on every width", () => {
+ const row = read("./LaunchRow.tsx");
+ assert.match(row, /const cap = capDisplay\(l\.fdv_quote, l\.quote_usd/);
+ assert.equal((row.match(/>\{cap\.detail\}\{cap\.detail\}<\/span>/, "the mobile block carries the detail");
+});
+
+test("one-string sites use the compact form, which carries the mark for an unpriced quote", () => {
+ for (const [file, expected] of [["./TrendingStrip.tsx", 1], ["./MeDashboard.tsx", 2], ["../../app/t/[chain]/[token]/page.tsx", 1]] as const) {
+ const src = read(file);
+ assert.equal((src.match(/capDisplay\([^)]*\)\.compact/g) ?? []).length, expected, `${file} uses .compact`);
+ assert.doesNotMatch(src, /capDisplay\([^)]*\)\.main/, `${file} never renders .main alone`);
+ }
+});
+
+test("the launch form renders main + detail for the opening cap, the preview card and the post-buy estimate", () => {
+ const form = read("./LaunchForm.tsx");
+ assert.equal((form.match(/cap\(fdvPreview\)\.main/g) ?? []).length, 2);
+ assert.equal((form.match(/cap\(fdvPreview\)\.detail/g) ?? []).length, 2);
+ assert.match(form, /cap\(buyPreview\.fdvAfter\)\.main\}<\/span>]*> · \{cap\(buyPreview\.fdvAfter\)\.detail\}/);
+ assert.doesNotMatch(form, /fmtMcap\(/, "the old quote-first formatter is gone");
+});
diff --git a/app/src/lib/launchpad/market-cap.test.ts b/app/src/lib/launchpad/market-cap.test.ts
new file mode 100644
index 00000000..b378696c
--- /dev/null
+++ b/app/src/lib/launchpad/market-cap.test.ts
@@ -0,0 +1,69 @@
+import { test } from "node:test";
+import assert from "node:assert/strict";
+import { MCAP_DEFAULT_INDEX, MCAP_USD_TARGETS, NO_USD_PRICE, capChipLabel, capDisplay, capEntry, capPick, capPresets, capQuoteLabel, capToQuote } from "./market-cap.ts";
+import { fdvForStartTick, startTickForFdv } from "./math.ts";
+
+const eth = { key: "eth" as const, symbol: "ETH", decimals: 18 };
+const usdg = { key: "usdg" as const, symbol: "USDG", decimals: 6 };
+const nvda = { key: "stock" as const, symbol: "NVDA", decimals: 18 };
+const gitlawb = { key: "gitlawb" as const, symbol: "GITLAWB", decimals: 18 };
+
+test("capEntry: dollars whenever the quote has a positive price, quote units otherwise", () => {
+ assert.deepEqual(capEntry(2464.49), { unit: "usd", quoteUsd: 2464.49 });
+ assert.deepEqual(capEntry(1), { unit: "usd", quoteUsd: 1 });
+ assert.deepEqual(capEntry(null), { unit: "quote" });
+ assert.deepEqual(capEntry(0), { unit: "quote" });
+ assert.deepEqual(capEntry(Number.NaN), { unit: "quote" });
+});
+
+test("capPresets: the same four dollar targets for every priced quote; a quote's own units without a price", () => {
+ const usd = capEntry(2464.49);
+ for (const k of ["eth", "usdg", "gitlawb", "stock"] as const) assert.deepEqual(capPresets(usd, k), MCAP_USD_TARGETS);
+ assert.deepEqual(capPresets(capEntry(null), "eth"), [1, 5, 10, 25], "ETH without a price: the old ETH presets");
+ assert.deepEqual(capPresets(capEntry(null), "usdg"), [5_000, 10_000, 25_000, 100_000]);
+ assert.deepEqual(capPresets(capEntry(null), "stock"), [], "a stock without a price has no presets: the form asks for a custom cap");
+ assert.deepEqual(capPresets(capEntry(null), "gitlawb"), []);
+ assert.equal(MCAP_USD_TARGETS[MCAP_DEFAULT_INDEX], 25_000);
+});
+
+test("capPick: a pick survives only among the presets it was made from; otherwise the default", () => {
+ assert.equal(capPick(100_000, MCAP_USD_TARGETS), 100_000);
+ assert.equal(capPick(null, MCAP_USD_TARGETS), 25_000);
+ assert.equal(capPick(25_000, [1, 5, 10, 25]), 10, "a $25K pick does not become 25,000 ETH after switching to an unpriced quote");
+ assert.equal(capPick(10, [1, 5, 10, 25]), 10);
+ assert.equal(capPick(10, []), null, "no presets, no pick: the form needs a custom cap");
+});
+
+test("capToQuote feeds the tick maths: $25K at $2,500/ETH is 10 ETH, and the tick round-trips within one spacing step", () => {
+ const usd = capEntry(2500);
+ assert.equal(capToQuote(25_000, usd), 10);
+ assert.equal(capToQuote(10, capEntry(null)), 10, "quote mode is the identity");
+ const tick = startTickForFdv(capToQuote(25_000, usd), 18);
+ const shown = fdvForStartTick(tick, 18) * 2500;
+ assert.ok(Math.abs(shown / 25_000 - 1) < 0.02, `the cap the tick actually produces is shown, within one 2% tick step of the target: $${shown.toFixed(0)}`);
+ const usdgTick = startTickForFdv(capToQuote(10_000, capEntry(1)), 6);
+ const usdgShown = fdvForStartTick(usdgTick, 6);
+ assert.ok(Math.abs(usdgShown / 10_000 - 1) < 0.02);
+});
+
+test("labels: chips in dollars or quote units; quote text per quote kind", () => {
+ assert.equal(capChipLabel(25_000, capEntry(2500), eth), "$25K");
+ assert.equal(capChipLabel(100_000, capEntry(1), usdg), "$100K");
+ assert.equal(capChipLabel(10, capEntry(null), eth), "10 ETH");
+ assert.equal(capQuoteLabel(10.1234, eth), "10.123 ETH");
+ assert.equal(capQuoteLabel(25_000, usdg), "25,000 USDG");
+ assert.equal(capQuoteLabel(0.5, usdg), "0.5 USDG", "fractional USDG keeps its cents (CodeRabbit, PR #27)");
+ assert.equal(capQuoteLabel(99.99, usdg), "99.99 USDG");
+ assert.equal(capQuoteLabel(100.4, usdg), "100 USDG", "from 100 units up, whole units");
+ assert.equal(capQuoteLabel(0.11208, nvda), "0.112 NVDA");
+ assert.equal(capQuoteLabel(1_250_000, gitlawb), "1.25M GITLAWB");
+});
+
+test("capDisplay: dollars lead with the quote figure as detail; without a price the quote figure leads and says so", () => {
+ assert.deepEqual(capDisplay(10, 2500, eth), { main: "$25K", detail: "10 ETH", compact: "$25K", usd: 25_000 });
+ assert.deepEqual(capDisplay(25_000, 1, usdg), { main: "$25K", detail: "25,000 USDG", compact: "$25K", usd: 25_000 });
+ assert.deepEqual(capDisplay(0.112, 223.05, nvda), { main: "$24.98", detail: "0.112 NVDA", compact: "$24.98", usd: 0.112 * 223.05 }, "under $1,000 the figure keeps cents");
+ assert.deepEqual(capDisplay(10, null, eth), { main: "10 ETH", detail: NO_USD_PRICE, compact: "10 ETH · no USD price", usd: null }, "one-string sites still carry the mark");
+ assert.deepEqual(capDisplay(0.5, 0, nvda), { main: "0.500 NVDA", detail: NO_USD_PRICE, compact: "0.500 NVDA · no USD price", usd: null });
+ assert.deepEqual(capDisplay(0.5, 1, usdg), { main: "$0.5", detail: "0.5 USDG", compact: "$0.5", usd: 0.5 });
+});
diff --git a/app/src/lib/launchpad/market-cap.ts b/app/src/lib/launchpad/market-cap.ts
new file mode 100644
index 00000000..433f9e0d
--- /dev/null
+++ b/app/src/lib/launchpad/market-cap.ts
@@ -0,0 +1,68 @@
+/**
+ * Market cap is a dollar figure everywhere; the quote-denominated figure is a detail. Pure, node --test loads it directly.
+ *
+ * The launch form lets the creator enter the starting cap in dollars whenever the quote has a USD price (ETH, USDG,
+ * GITLAWB and stocks with a live price) and converts to quote units for the tick maths; when the quote has no price
+ * the form falls back to quote units, as before. The same four dollar targets apply to every priced quote, so an
+ * ETH launch no longer opens at "10 ETH" while a USDG launch opens at "$10K".
+ */
+
+import { MCAP_PRESETS, type Quote } from "./config";
+import { fmtCompact, fmtQuoteUnits } from "./math";
+import { marketUsd } from "./market-format";
+
+export const MCAP_USD_TARGETS = [5_000, 10_000, 25_000, 100_000];
+export const MCAP_DEFAULT_INDEX = 2; // $25K, about what the old 10 ETH default was
+
+/** How the form takes the starting cap: in dollars (with the quote's USD price) or in quote units (no price). */
+export type CapEntry = { unit: "usd"; quoteUsd: number } | { unit: "quote" };
+
+export function capEntry(quoteUsd: number | null): CapEntry {
+ return quoteUsd !== null && Number.isFinite(quoteUsd) && quoteUsd > 0 ? { unit: "usd", quoteUsd } : { unit: "quote" };
+}
+
+/** Preset caps in the entry unit: the dollar targets, or the quote-unit presets a quote has without a price (none for stocks/GITLAWB). */
+export function capPresets(entry: CapEntry, quoteKey: Quote["key"]): number[] {
+ return entry.unit === "usd" ? MCAP_USD_TARGETS : MCAP_PRESETS[quoteKey];
+}
+
+/** The preset to use: the creator's pick when it belongs to this quote's presets (a pick does not carry across quotes), else the default. */
+export function capPick(pick: number | null, presets: number[]): number | null {
+ if (pick !== null && presets.includes(pick)) return pick;
+ return presets[MCAP_DEFAULT_INDEX] ?? presets[0] ?? null;
+}
+
+/** A cap in the entry unit → quote units, which is what the start tick is computed from. */
+export function capToQuote(v: number, entry: CapEntry): number {
+ return entry.unit === "usd" ? v / entry.quoteUsd : v;
+}
+
+/** Preset chip text: "$25K" in dollar mode, "10 ETH" in quote mode. */
+export function capChipLabel(v: number, entry: CapEntry, quote: Pick): string {
+ return entry.unit === "usd" ? `$${fmtCompact(v, 0)}` : `${fmtQuoteUnits(v, quote.decimals)} ${quote.symbol}`;
+}
+
+/** A quote-denominated cap as quote text: "9.87 ETH", "25,000 USDG", "0.112 NVDA". */
+export function capQuoteLabel(fdvQuote: number, quote: Pick): string {
+ if (quote.decimals <= 6) return `${fdvQuote.toLocaleString("en-US", { maximumFractionDigits: Math.abs(fdvQuote) < 100 ? 2 : 0 })} ${quote.symbol}`; // a 0.5 USDG cap is "0.5 USDG", not "1 USDG"
+ if (quote.key === "stock") return `${fdvQuote.toFixed(3)} ${quote.symbol}`;
+ return `${fmtQuoteUnits(fdvQuote, quote.decimals)} ${quote.symbol}`;
+}
+
+export const NO_USD_PRICE = "no USD price";
+
+/**
+ * How a cap is shown anywhere on the site: the dollar figure leads and the quote figure is the detail; without a USD
+ * price the quote figure leads and the detail says so. Never a bare dash. `compact` is the one-string form for sites
+ * with room for a single figure: the dollar figure when priced, else the quote figure with the mark, so an unpriced
+ * cap is never shown as if it were dollars.
+ */
+export function capDisplay(fdvQuote: number, quoteUsd: number | null, quote: Pick): { main: string; detail: string; compact: string; usd: number | null } {
+ const quoteText = capQuoteLabel(fdvQuote, quote);
+ if (quoteUsd !== null && Number.isFinite(quoteUsd) && quoteUsd > 0) {
+ const usd = fdvQuote * quoteUsd;
+ const main = marketUsd(usd);
+ return { main, detail: quoteText, compact: main, usd };
+ }
+ return { main: quoteText, detail: NO_USD_PRICE, compact: `${quoteText} · ${NO_USD_PRICE}`, usd: null };
+}