Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions app/db/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -318,3 +318,8 @@ CREATE TABLE IF NOT EXISTS bb_quote_tokens (
checked_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (chain_id, address)
);

-- ── token banners (2026-10-05) ─────────────────────────────────────────────
-- An optional wide image the creator uploads beside the logo. Same rules as image_url (https only; uploads are
-- re-encoded server-side, here to a 1500×500 WebP). NULL means no banner: the market cards draw one from the logo.
ALTER TABLE bb_launch_meta ADD COLUMN IF NOT EXISTS banner_url text;
50 changes: 50 additions & 0 deletions app/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"@number-flow/react": "^0.6.2",
"@tanstack/react-query": "^5.91.3",
"clsx": "^2.1.1",
"d3-force": "^3.0.0",
"lightweight-charts": "^5.2.1",
"lucide-react": "^1.42.0",
"motion": "^13.2.0",
Expand All @@ -34,6 +35,7 @@
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/d3-force": "^3.0.10",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
Expand Down
2 changes: 1 addition & 1 deletion app/src/app/(home)/loading.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Sk, SkPost, SkRow } from "@/components/Skeleton";
/** Home skeleton: hero + totals, list header, rows, side column. Same grid as page.tsx so nothing shifts. */
export default function Loading() {
return (
<main className="relative mx-auto max-w-6xl px-4 pb-16 space-y-8" aria-busy="true" aria-label="loading">
<main className="bb-mid bb-page relative pb-16 space-y-8" aria-busy="true" aria-label="loading">
<section className="pt-10 sm:pt-14 pb-2 grid lg:grid-cols-[minmax(0,1.1fr)_minmax(0,1fr)] gap-x-8 gap-y-8 xl:gap-x-12 lg:gap-y-0 lg:grid-rows-[min-content_1fr]">
<div className="min-w-0 lg:col-start-1 lg:row-start-1">
<div className="space-y-2">
Expand Down
52 changes: 27 additions & 25 deletions app/src/app/(home)/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { Metadata } from "next";
import { BRAND_GITHUB } from "@/lib/brand";
import Link from "next/link";
import { cookies } from "next/headers";
import LaunchHero from "@/components/launchpad/LaunchHero";
import LaunchList from "@/components/launchpad/LaunchList";
import LiveRiver from "@/components/launchpad/LiveRiver";
Expand All @@ -16,6 +15,10 @@ import { isFilter } from "@/lib/launchpad/search";
import { nowMs } from "@/lib/launchpad/time";
import { inRiverWindow, riverCoverage } from "@/lib/launchpad/river";
import TrendingStrip from "@/components/launchpad/TrendingStrip";
import WhyFree from "@/components/launchpad/WhyFree";
import HeroBackdrop from "@/components/launchpad/HeroBackdrop";
import { MagicCard } from "@/components/vendor/magic-card";
import { LAYOUT_COOKIE, parseLayout } from "@/lib/launchpad/list-layout";

export const dynamic = "force-dynamic";
// Title and description come from the root layout; ?sort= / ?chain= / ?window= / ?view= are all this one page.
Expand All @@ -31,6 +34,7 @@ export default async function Home({ searchParams }: { searchParams: Promise<{ s
const chain = visibleChainOr(sp.chain);
const filter = isFilter(sp.filter) ? sp.filter : null;
const usd = await ethUsd();
const layout = parseLayout((await cookies()).get(LAYOUT_COOKIE)?.value);
const listOpts = { sort, window, chain, filter, limit: PAGE_SIZE, ethUsd: usd };
const [page, feed, fresh, posts, fetchedTrending] = await Promise.all([
listLaunchesPage(listOpts),
Expand All @@ -43,29 +47,27 @@ export default async function Home({ searchParams }: { searchParams: Promise<{ s
const now = nowMs();

return (
<main className="relative mx-auto max-w-6xl space-y-8 px-4 pb-16">
<LaunchHero configured={LAUNCHPAD_CONFIGURED} />
<LiveRiver initial={feed.filter((item) => inRiverWindow(item, now))} serverNow={now} coveredSince={riverCoverage(feed, RIVER_SEED, now)} lastActivityAt={feed[0] ? new Date(feed[0].at).getTime() : 0} />
<TrendingStrip initial={trending} />
<div className="grid items-start gap-6 xl:grid-cols-[minmax(0,1fr)_17rem]">
<LaunchList initial={page.items} initialHasMore={page.hasMore} initialSort={sort} initialWindow={window} initialChain={chain} initialFilter={filter} initialView={sp.view === "watchlist" ? "watchlist" : "market"} hasDb={dbConfigured()} />
<aside aria-label="New launches and community" className="grid min-w-0 gap-4 md:grid-cols-2 xl:sticky xl:top-24 xl:grid-cols-1">
<JustLaunched initial={fresh.items} serverNow={now} />
<div className="min-w-0 space-y-4">
<PostsFeed initial={posts} compact />
<section aria-labelledby="free-heading" className="rounded-2xl border border-line bg-paper p-4">
<h2 id="free-heading" className="text-sm font-semibold text-ink">Why it&apos;s free</h2>
<p className="mt-2 text-pretty text-xs leading-relaxed text-muted">No fee address in the factory. No platform cut in the locker. On every chain.</p>
<dl className="mt-4 divide-y divide-line border-y border-line text-xs">
<div className="flex items-center justify-between gap-3 py-2.5"><dt className="text-muted">Platform fee</dt><dd className="font-mono font-bold text-up tnum">$0</dd></div>
<div className="flex items-center justify-between gap-3 py-2.5"><dt className="text-muted">Trading fee</dt><dd className="font-mono text-ink tnum">0 / 1 / 3%</dd></div>
</dl>
<p className="mt-3 text-pretty text-[11px] leading-relaxed text-muted">Creators choose the trading fee. It goes in full to their beneficiaries, or is burned.</p>
<a href={`${BRAND_GITHUB}/tree/main/contracts/src`} target="_blank" rel="noopener noreferrer" className="mt-3 inline-flex min-h-8 items-center text-xs font-medium text-body underline decoration-line-strong underline-offset-4 hover:text-ink">Read the contracts ↗</a>
<div className="mt-3 border-t border-line pt-3"><Link href="/agents" className="inline-flex min-h-8 items-center text-xs font-medium text-brand hover:underline underline-offset-4">Agents can launch too →</Link></div>
</section>
</div>
</aside>
<main className="bb-mid relative pb-16">
{/* the hero and the river share one full-bleed band; it clips its own backdrop so nothing widens the page */}
<div className="relative isolate overflow-hidden">
<HeroBackdrop />
<div className="bb-page space-y-8 pb-4">
<LaunchHero configured={LAUNCHPAD_CONFIGURED} />
<LiveRiver initial={feed.filter((item) => inRiverWindow(item, now))} recent={feed} serverNow={now} coveredSince={riverCoverage(feed, RIVER_SEED, now)} lastActivityAt={feed[0] ? new Date(feed[0].at).getTime() : 0} />
</div>
</div>
<div className="bb-page mt-6 space-y-8">
<TrendingStrip initial={trending} />
<div className="grid items-start gap-x-8 gap-y-6 xl:grid-cols-[minmax(0,1fr)_19rem]">
<MagicCard className="min-w-0 rounded-2xl"><LaunchList frame="rounded-[inherit]" initial={page.items} initialHasMore={page.hasMore} initialSort={sort} initialWindow={window} initialChain={chain} initialFilter={filter} initialView={sp.view === "watchlist" ? "watchlist" : "market"} initialLayout={layout} hasDb={dbConfigured()} serverNow={now} /></MagicCard>
<aside aria-label="New launches and community" className="grid min-w-0 gap-4 md:grid-cols-2 xl:sticky xl:top-24 xl:grid-cols-1">
<JustLaunched initial={fresh.items} serverNow={now} />
<div className="min-w-0 space-y-4">
<PostsFeed initial={posts} compact />
<WhyFree />
</div>
</aside>
</div>
</div>
</main>
);
Expand Down
3 changes: 2 additions & 1 deletion app/src/app/about/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Metadata } from "next";
import { pageMetadata } from "@/lib/seo";
import Link from "next/link";
import { ArrowRight, ArrowUpRight } from "lucide-react";
import { ArrowRight, ArrowUpRight, Info } from "lucide-react";
import SectionIntro from "@/components/sections/SectionIntro";
import shell from "@/components/sections/SectionShell.module.css";
import { BRAND, BRAND_DOMAIN, BRAND_GITHUB, BRAND_X, LEGACY_DOMAIN } from "@/lib/brand";
Expand Down Expand Up @@ -34,6 +34,7 @@ export default function AboutPage() {
<main className={shell.page}>
<SectionIntro
eyebrow="About"
icon={<Info />}
title={`What ${BRAND} is`}
description={<>{BRAND} is a free, open-source token launchpad. One transaction deploys a fixed-supply ERC-20, opens a Uniswap v4 pool at the market cap you pick and locks 100% of the supply as liquidity, forever. It runs on Base, Robinhood Chain and Arc.</>}
>
Expand Down
24 changes: 13 additions & 11 deletions app/src/app/agents/page.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
import type { Metadata } from "next";
import { pageMetadata } from "@/lib/seo";
import { ArrowDown, ArrowRight, ArrowUpRight, Braces, FileCode2, Terminal } from "lucide-react";
import { ArrowDown, ArrowLeftRight, ArrowRight, ArrowUpRight, BookOpen, Bot, Braces, FileCode2, Rocket, Terminal } from "lucide-react";
import SectionIntro from "@/components/sections/SectionIntro";
import AgentsCodeBlock from "@/components/sections/AgentsCodeBlock";
import shell from "@/components/sections/SectionShell.module.css";
import styles from "@/components/sections/Agents.module.css";
import { launchpad } from "@/lib/launchpad/config";
import { CHAIN_KEYS, CHAIN_LABELS, CHAINS, SITE_URL, explorerAddress } from "@/lib/chainPublic";
import { BRAND_GITHUB } from "@/lib/brand";
import { ChainLogo } from "@/components/launchpad/ChainLogo";

export const metadata: Metadata = pageMetadata({ path: "/agents", title: "Agents", description: "Launch and trade tokens from an agent: one contract call, plus a JSON API for the list, trades and metadata." });

/** The page's sections, each with the mark its heading carries. */
const sections = [
{ id: "launch", number: "01", label: "Launch a token" },
{ id: "read", number: "02", label: "Read the API" },
{ id: "trade", number: "03", label: "Trade & collect" },
{ id: "launch", icon: Rocket, label: "Launch a token" },
{ id: "read", icon: BookOpen, label: "Read the API" },
{ id: "trade", icon: ArrowLeftRight, label: "Trade & collect" },
] as const;

const endpoints = [
Expand All @@ -34,33 +36,33 @@ export default function AgentsPage() {

return (
<main className={`${shell.page} ${styles.page}`}>
<SectionIntro eyebrow="Developer reference" title="Agents" description={<>No API key, no signup, no platform fee. A launch is one contract call from any wallet with a little ETH for gas. Everything the site shows is also JSON.</>}>
<SectionIntro eyebrow="Developer reference" icon={<Bot />} title="Agents" description={<>No API key, no signup, no platform fee. A launch is one contract call from any wallet with a little ETH for gas. Everything the site shows is also JSON.</>}>
<a href="/llms.txt" className={shell.action}><FileCode2 size={16} aria-hidden="true" />Read llms.txt<ArrowUpRight size={14} aria-hidden="true" /></a>
<a href={BRAND_GITHUB} target="_blank" rel="noreferrer" className={shell.textLink}>Explore the source<ArrowUpRight size={14} aria-hidden="true" /></a>
</SectionIntro>

<div className={styles.entry}>
<div className={styles.entryHeading}><Terminal size={19} aria-hidden="true" /><p>The interface is optional.<span>The same protocol is yours to build with.</span></p></div>
<div className={styles.entryHeading}><span className={styles.entryIcon} aria-hidden="true"><Terminal size={17} /></span><p>The interface is optional.<span>The same protocol is yours to build with.</span></p></div>
<a href="#launch">Start with a launch<ArrowDown size={15} aria-hidden="true" /></a>
</div>

<div className={styles.layout}>
<aside className={styles.sidebar}>
<nav aria-label="Agent documentation sections">
<p className={styles.indexLabel}>On this page</p>
{sections.map(({ id, number, label }) => <a key={id} href={`#${id}`}><span>{number}</span>{label}<ArrowRight size={13} aria-hidden="true" /></a>)}
{sections.map(({ id, icon: Icon, label }) => <a key={id} href={`#${id}`}><span aria-hidden="true"><Icon size={13} /></span>{label}<ArrowRight size={13} aria-hidden="true" /></a>)}
</nav>
<p className={styles.sidebarNote}>Use the JSON API to read.<br />Use your wallet to write on-chain.</p>
</aside>

<div className={styles.content}>
<section id="launch" tabIndex={-1} className={`${shell.anchorSection} ${styles.section}`} aria-labelledby="agents-launch-title">
<div className={styles.sectionHeading}><span className={styles.sectionNumber}>01</span><div><h2 id="agents-launch-title">Launch a token</h2><p>One call, from your own wallet.</p></div></div>
<div className={styles.sectionHeading}><span className={styles.sectionIcon} aria-hidden="true"><Rocket size={17} /></span><div><h2 id="agents-launch-title">Launch a token</h2><p>One call, from your own wallet.</p></div></div>
<div className={styles.networks}>
{CHAIN_KEYS.map((chain) => {
const config = launchpad(chain);
return <div key={chain} className={styles.network}>
<div className={styles.networkHeading}><h3>{CHAIN_LABELS[chain]}</h3><span>Chain <b>{CHAINS[chain].id}</b></span></div>
<div className={styles.networkHeading}><h3><ChainLogo chain={chain} size={16} />{CHAIN_LABELS[chain]}</h3><span>Chain <b>{CHAINS[chain].id}</b></span></div>
<dl>{(["factory", "locker"] as const).map((contract) => <div key={contract}>
<dt>{contract}</dt>
<dd>{config[contract] ? <a href={explorerAddress(chain, config[contract])} target="_blank" rel="noreferrer" aria-label={`${CHAIN_LABELS[chain]} ${contract}: ${config[contract]}, view on explorer`}><code>{config[contract]}</code><ArrowUpRight size={13} aria-hidden="true" /></a> : <span>not deployed yet</span>}</dd>
Expand Down Expand Up @@ -90,7 +92,7 @@ export default function AgentsPage() {
</section>

<section id="read" tabIndex={-1} className={`${shell.anchorSection} ${styles.section}`} aria-labelledby="agents-read-title">
<div className={styles.sectionHeading}><span className={styles.sectionNumber}>02</span><div><h2 id="agents-read-title">Read the API</h2><p>JSON endpoints, plus a plain-text agent reference.</p></div></div>
<div className={styles.sectionHeading}><span className={styles.sectionIcon} aria-hidden="true"><BookOpen size={17} /></span><div><h2 id="agents-read-title">Read the API</h2><p>JSON endpoints, plus a plain-text agent reference.</p></div></div>
<div className={styles.endpoints}>
<div className={styles.endpointHeader}><span>Public endpoints</span><span>No API key</span></div>
{endpoints.map((endpoint) => <div key={endpoint.path} className={styles.endpoint}>
Expand All @@ -106,7 +108,7 @@ export default function AgentsPage() {
</section>

<section id="trade" tabIndex={-1} className={`${shell.anchorSection} ${styles.section}`} aria-labelledby="agents-trade-title">
<div className={styles.sectionHeading}><span className={styles.sectionNumber}>03</span><div><h2 id="agents-trade-title">Trade & collect</h2><p>Standard pools. Direct contract calls.</p></div></div>
<div className={styles.sectionHeading}><span className={styles.sectionIcon} aria-hidden="true"><ArrowLeftRight size={17} /></span><div><h2 id="agents-trade-title">Trade & collect</h2><p>Standard pools. Direct contract calls.</p></div></div>
<div className={styles.tradeReference}>
<div><span className={styles.tradeLabel}>Swap</span><h3>Speak Uniswap v4</h3><p className={styles.prose}>Pools are plain Uniswap v4, with tick spacing <code>200</code> and no hook. The quote can be ETH, TWIG (Base), GITLAWB (Base and Robinhood Chain), USDG (Robinhood Chain), USDC (Arc) or a supported issuer-registry stock token. Get the actual pool key with <code>poolKeyOf(token)</code> on the factory. Swap through the Universal Router with a <code>V4_SWAP</code> command, or any router that speaks v4.</p></div>
<div><span className={styles.tradeLabel}>Collect</span><h3>Pay out accrued fees</h3><p className={styles.prose}>Anyone may call <code>collect(tokenId)</code> on that chain&apos;s locker to pay out accrued fees. Base locker: <code>{locker}</code>. Use the matching chain address above.</p></div>
Expand Down
Loading
Loading