diff --git a/app/package-lock.json b/app/package-lock.json index 6e352768..54710abd 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -9,10 +9,12 @@ "version": "0.1.0", "dependencies": { "@aws-sdk/client-s3": "^3.1127.0", + "@base-ui/react": "^1.8.0", "@tanstack/react-query": "^5.91.3", "clsx": "^2.1.1", "lightweight-charts": "^5.2.1", "lucide-react": "^1.42.0", + "motion": "^13.2.0", "next": "^16.2.9", "next-themes": "^0.4.6", "postgres": "^3.4.9", @@ -554,6 +556,15 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", @@ -602,6 +613,66 @@ "node": ">=6.9.0" } }, + "node_modules/@base-ui/react": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@base-ui/react/-/react-1.8.0.tgz", + "integrity": "sha512-P0/1sxo6SBVZOklKMIedvTWqw2s2IQzi9x5bIVsXu980cuSOD4NeuRSs+/L7LZQfDkZP/uRZyGPyfFl/B1oH+Q==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "@base-ui/utils": "0.4.0", + "@floating-ui/react-dom": "^2.1.9", + "@floating-ui/utils": "^0.2.12", + "use-sync-external-store": "^1.6.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@date-fns/tz": "^1.2.0", + "@types/react": "^17 || ^18 || ^19", + "date-fns": "^4.0.0", + "react": "^17 || ^18 || ^19", + "react-dom": "^17 || ^18 || ^19" + }, + "peerDependenciesMeta": { + "@date-fns/tz": { + "optional": true + }, + "@types/react": { + "optional": true + }, + "date-fns": { + "optional": true + } + } + }, + "node_modules/@base-ui/utils": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@base-ui/utils/-/utils-0.4.0.tgz", + "integrity": "sha512-bO9fz25kKtPf+aZVyfQrC0PDmJdmVni31W2hCS5/Owb+inwdIL3XU26pCPRPlt4LSxZrBgLwubXQXQlKaFEZzw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "@floating-ui/utils": "^0.2.12", + "reselect": "^5.2.0", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "@types/react": "^17 || ^18 || ^19", + "react": "^17 || ^18 || ^19", + "react-dom": "^17 || ^18 || ^19" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@emnapi/core": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", @@ -803,6 +874,44 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz", + "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz", + "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.8.0", + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.9.tgz", + "integrity": "sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.8.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz", + "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", + "license": "MIT" + }, "node_modules/@humanfs/core": { "version": "0.19.2", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", @@ -4438,6 +4547,29 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/framer-motion": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-13.2.0.tgz", + "integrity": "sha512-9E33ebgMaO33w1nN/jEdW8z3/GO483fMi4rqbMG9rt83XgW9QLKRe4NcmJ8s+fQ3O34++UHrIQwlIWGIWTITjA==", + "license": "MIT", + "dependencies": { + "motion-dom": "^13.2.0", + "motion-utils": "^13.0.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -5840,6 +5972,43 @@ } } }, + "node_modules/motion": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/motion/-/motion-13.2.0.tgz", + "integrity": "sha512-4Hrb5vD6HhjFstLUiCmWvtpsw+WTpP4R+QXfSDYZBz7+uxE/LrRg3aV0ReJxHrTRffHhbIE6svEqnotngXvesQ==", + "license": "MIT", + "dependencies": { + "framer-motion": "^13.2.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/motion-dom": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-13.2.0.tgz", + "integrity": "sha512-N6gdSoWRDk0Rh/fVtlqUtLs+fEN3ELFZI3cn3IQE9Mnf3E+Mh8wjO6MstzCOPFh4Yf0L1as5m2eUyYWj8ylVSQ==", + "license": "MIT", + "dependencies": { + "motion-utils": "^13.0.0" + } + }, + "node_modules/motion-utils": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-13.0.0.tgz", + "integrity": "sha512-7DnN7TmbLcYXcG4RVadXIihWlyuM9afoUww8Y5Agg431kGKiuL2/OMyP4mJ5wLz+pvN3t5ySClLOaVXJ+wekRQ==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -6502,6 +6671,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/reselect": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.3.0.tgz", + "integrity": "sha512-XGoLeRAVzUTcJ1qkxPQhDJyIZ5d6zzZD9nT7AEZOaaU9UbWclhycElmhO+VD5bFeLuzhPBaOV2oXC8uG35ZSpg==", + "license": "MIT" + }, "node_modules/resolve": { "version": "2.0.0-next.7", "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz", diff --git a/app/package.json b/app/package.json index e7ae4806..552a6ec4 100644 --- a/app/package.json +++ b/app/package.json @@ -13,10 +13,12 @@ }, "dependencies": { "@aws-sdk/client-s3": "^3.1127.0", + "@base-ui/react": "^1.8.0", "@tanstack/react-query": "^5.91.3", "clsx": "^2.1.1", "lightweight-charts": "^5.2.1", "lucide-react": "^1.42.0", + "motion": "^13.2.0", "next": "^16.2.9", "next-themes": "^0.4.6", "postgres": "^3.4.9", diff --git a/app/src/app/(home)/loading.tsx b/app/src/app/(home)/loading.tsx index a360c8e6..f71f404b 100644 --- a/app/src/app/(home)/loading.tsx +++ b/app/src/app/(home)/loading.tsx @@ -1,40 +1,49 @@ -import { Sk, SkPost, SkRow, SkStat } from "@/components/Skeleton"; +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 (
-
-
- - - - -
- - +
+
+
+ {[0, 1, 2].map((i) => )}
+
{[0, 1, 2].map((i) => )}
+
+ +
+
+
+
+
+
{[0, 1, 2].map((i) => )}
+
+ +
+
+
+ +
{[0, 1, 2].map((i) =>
)}
+
-
- - - - -
-
-
-
+
+
+
+
- +
-
- - +
+ + + +
-
    +
      {Array.from({ length: 8 }, (_, i) => ( - + ))}
diff --git a/app/src/app/(home)/page.tsx b/app/src/app/(home)/page.tsx index 7f6b95f4..7f57d958 100644 --- a/app/src/app/(home)/page.tsx +++ b/app/src/app/(home)/page.tsx @@ -26,28 +26,28 @@ export default async function Home({ searchParams }: { searchParams: Promise<{ s return ( <> -
-
+
-
- +
diff --git a/app/src/app/agents/page.tsx b/app/src/app/agents/page.tsx index 560a24bb..930470c2 100644 --- a/app/src/app/agents/page.tsx +++ b/app/src/app/agents/page.tsx @@ -1,71 +1,123 @@ import type { Metadata } from "next"; -import { cardPad, codeBlock } from "@/components/ui"; +import { ArrowDown, ArrowRight, ArrowUpRight, Braces, FileCode2, 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 } from "@/lib/chainPublic"; +import { CHAIN_KEYS, CHAIN_LABELS, CHAINS, SITE_URL, explorerAddress } from "@/lib/chainPublic"; +import { BRAND_GITHUB } from "@/lib/brand"; export const metadata: Metadata = { title: "Agents", description: "Launch and trade tokens from an agent: one contract call, plus a JSON API for the list, trades and metadata." }; +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" }, +] as const; + +const endpoints = [ + { path: "/api/launch/list", description: "Browse and sort launches", query: "?chain=base|robinhood&sort=new|trending|mcap|volume|gainers&window=1h|24h|all&limit=50" }, + { path: "/api/launch/feed", description: "Latest launches + trades" }, + { path: "/api/launch/meta/", description: "Image, description and links" }, + { path: "/llms.txt", description: "Machine-readable reference" }, +] as const; + export default function AgentsPage() { const factory = launchpad("base").factory ?? ""; const locker = launchpad("base").locker ?? ""; - return ( -
-
-

Agents

-

No API key, no signup, no fee. A launch is one contract call from any wallet with a little ETH for gas. Everything the site shows is also JSON.

-
- -
-

Launch a token (one call)

-
-
- {CHAIN_KEYS.map((k) => ( -

- {CHAIN_LABELS[k]} (chain {CHAINS[k].id}) · factory {launchpad(k).factory ?? "not deployed yet"} · locker {launchpad(k).locker ?? "—"} -

- ))} -
-

- Call launch(params) on that chain's factory. Quote is ETH (address zero) — or USDG 0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168 on Robinhood Chain, in which case call{" "} - findSalt(...) first so the token address sorts above the quote. Supply 0 means the default 1B. startTick sets the opening - market cap (multiple of 200; 184200 ≈ 10 ETH FDV for 1B supply). lpFee is in pips (0, 10000 = 1%, 30000 = 3%). Empty recipients{" "} - = fees burned; otherwise shares in bps summing to 10000. -

-
{`cast send ${factory} \\
+  const launchExample = `cast send ${factory} \\
   "launch((string,string,string,address,uint256,int24,uint24,bytes32,(address,uint16)[]))" \\
   "(My Token,MYT,,0x0000000000000000000000000000000000000000,0,184200,10000,0x$(openssl rand -hex 32),[(0xYourWallet,10000)])" \\
-  --rpc-url https://mainnet.base.org --private-key $PK`}
-

- Optional metadata (image, description, links): POST {SITE_URL}/api/launch/meta with{" "} - {"{chain, launcher, salt, name, symbol, description?, image_url?, website?, x_handle?}"} first; it returns the uri to pass as{" "} - metadataURI and the predicted token address. -

-
-
+ --rpc-url https://mainnet.base.org --private-key $PK`; -
-

Read

-
-
{`GET ${SITE_URL}/api/launch/list?chain=base|robinhood&sort=new|trending|mcap|volume|gainers&window=1h|24h|all&limit=50
-GET ${SITE_URL}/api/launch/feed              # latest launches + trades
-GET ${SITE_URL}/api/launch/meta/      # image / description / links
-GET ${SITE_URL}/llms.txt`}
-

Amounts are wei strings; prices and FDV are floats in ETH. Poll freely — responses are small and uncached.

-
-
+ return ( +
+ 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.}> + + Explore the source + + +
+
+ Start with a launch +
+ +
+ + +
+
+
01

Launch a token

One call, from your own wallet.

+
+ {CHAIN_KEYS.map((chain) => { + const config = launchpad(chain); + return
+

{CHAIN_LABELS[chain]}

Chain {CHAINS[chain].id}
+
{(["factory", "locker"] as const).map((contract) =>
+
{contract}
+
{config[contract] ? {config[contract]} : not deployed yet}
+
)}
+
; + })} +
+

Call launch(params) on that chain's factory. The example below launches on Base with an ETH quote.

+ +

Example only. Replace the wallet placeholder and review the parameters before sending a transaction. Never share your private key.

+
+
+
+
quote
ETH uses address zero. For USDG on Robinhood Chain, use 0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168 and call findSalt(...) first so the token address sorts above the quote.
+
supply
0 means the default 1B supply.
+
startTick
Sets the opening market cap. A multiple of 200; 184200 ≈ 10 ETH FDV for 1B supply.
+
lpFee
In pips: 0, 10000 = 1%, or 30000 = 3%.
+
recipients
Empty means fees are burned. Otherwise, shares are in bps and sum to 10000.
+
+
+
+
POST

Add metadata first

Optional
+

For an image, description or links, send metadata first. The response contains the uri to pass as metadataURI and the predicted token address.

+ +
+
+ +
+
02

Read the API

JSON endpoints, plus a plain-text agent reference.

+
+
Public endpointsNo API key
+ {endpoints.map((endpoint) =>
+ GET +
{endpoint.path}

{endpoint.description}

{"query" in endpoint && {endpoint.query}}
+
)} +
+ # image / description / links\nGET ${SITE_URL}/llms.txt`} /> +
+

Raw token and quote amounts are decimal integer strings. Use quote_decimals for quote amounts, not a blanket 18 decimals. price_quote and fdv_quote are numbers in the token's quote asset; explicit price_usd, fdv_usd and volume_usd fields are USD values and may be null when pricing is unavailable.

+

Poll modestly and back off on errors. Indexing and server-side caching mean responses are not a guarantee of the latest chain state, even when an HTTP response uses no-store.

+
+
-
-

Trade

-
-

- Pools are plain Uniswap v4 (ETH / token, tick spacing 200, no hook). Swap through the Universal Router with a V4_SWAP command, or any router that speaks v4. Get the pool - key with poolKeyOf(token) on the factory. Anyone may call collect(tokenId) on the locker ({locker}) to pay out accrued fees. -

-

- After a transaction you sent, POST {SITE_URL}/api/launch/sync?chain=base|robinhood&tx=0x… indexes it immediately; otherwise the poller picks it up within ~15s. -

+
+
03

Trade & collect

Standard pools. Direct contract calls.

+
+
Swap

Speak Uniswap v4

Pools are plain Uniswap v4, with tick spacing 200 and no hook. The quote can be ETH, USDG or a supported issuer-registry stock token. Get the actual pool key with poolKeyOf(token) on the factory. Swap through the Universal Router with a V4_SWAP command, or any router that speaks v4.

+
Collect

Pay out accrued fees

Anyone may call collect(tokenId) on that chain's locker to pay out accrued fees. Base locker: {locker}. Use the matching chain address above.

+
+

Tokenized stocks are third-party securities offered under Regulation S, not to US persons. The site recognizes them from issuer registries, not on-chain names.

+
+
POST

Sync your transaction

+

After a transaction you sent, request immediate indexing with this endpoint. Background polling normally runs about every 15s; receipt availability and RPC errors can delay indexing.

+ +
+
-
+
); } diff --git a/app/src/app/api/launch/candles/route.ts b/app/src/app/api/launch/candles/route.ts index ff9449ac..905fb1fd 100644 --- a/app/src/app/api/launch/candles/route.ts +++ b/app/src/app/api/launch/candles/route.ts @@ -1,9 +1,8 @@ import { NextResponse } from "next/server"; import { isAddress } from "viem"; import { isChainKey } from "@/lib/chainPublic"; -import { getCandles, getLaunch, getWalletSwaps } from "@/lib/launchpad/queries"; -import { INTERVALS, isInterval, lookbackFor } from "@/lib/launchpad/candles"; -import { quoteInfo } from "@/lib/launchpad/config"; +import { getCandleBaseline, getCandles, getLaunch, getWalletSwaps } from "@/lib/launchpad/queries"; +import { INTERVALS, boundedCandleFrom, isInterval } from "@/lib/launchpad/candles"; import { ethUsd } from "@/lib/launchpad/ethPrice"; import { memo } from "@/lib/launchpad/memo"; @@ -11,8 +10,9 @@ export const dynamic = "force-dynamic"; /** * GET /api/launch/candles?chain=&token=&interval=1m|5m|15m|1h|4h|1d[&from=unix][&wallet=0x…] - * → { interval, from, launch: {t, price}, quote: {symbol, decimals, usd}, supply, candles: RawCandle[], mine?: [...] } - * Sparse buckets (quote per token); the client fills gaps. 3s memo per key. + * → { interval, from, asOf, baseline: {price, hasPriorTrades}, launch: {t, price}, quote: {symbol, decimals, usd}, supply, candles: RawCandle[], mine?: [...] } + * Sparse whole buckets (quote per token), at most 2,000 including the current + * bucket. The client fills gaps from baseline.price. 3s snapshot memo per key. */ export async function GET(req: Request) { const u = new URL(req.url); @@ -23,20 +23,31 @@ export async function GET(req: Request) { const usd = await ethUsd(); const l = await memo(`launch:${chain}:${token}`, 3_000, () => getLaunch(chain, token, usd)); if (!l) return NextResponse.json({ error: "not found" }, { status: 404 }); - const q = quoteInfo(chain, l.quote); + // getLaunch resolves issuer-registry stock quotes too; the static quote list + // would relabel them "?" and could aggregate with the wrong decimals. + const q = { symbol: l.quote_symbol, decimals: l.quote_decimals }; const intervalS = INTERVALS[interval]; const launchT = Math.floor(new Date(l.block_time).getTime() / 1000); + const requestedAt = Math.floor(Date.now() / 1000); const fromRaw = Number(u.searchParams.get("from")); - const from = Number.isFinite(fromRaw) && fromRaw > 0 ? Math.max(launchT, fromRaw) : Math.max(launchT, Math.floor(Date.now() / 1000) - lookbackFor(intervalS)); + const from = boundedCandleFrom(fromRaw, launchT, requestedAt, intervalS); const wallet = u.searchParams.get("wallet"); - const [candles, mine] = await Promise.all([ - memo(`candles:${chain}:${token}:${interval}:${from}`, 3_000, () => getCandles(chain, token, intervalS, from, q.decimals)), - wallet && isAddress(wallet) ? getWalletSwaps(chain, token, wallet) : Promise.resolve(null), - ]); + const snapshot = await memo(`candles:${chain}:${token}:${interval}:${from}`, 3_000, async () => { + const asOf = requestedAt; + const [candles, priorPrice] = await Promise.all([ + getCandles(chain, token, intervalS, from, q.decimals, asOf), + getCandleBaseline(chain, token, from, q.decimals), + ]); + return { candles, priorPrice, asOf }; + }); + // A cache hit can predate this request. Markers must share its cutoff so a + // new wallet trade cannot appear ahead of the corresponding OHLCV update. + const mine = wallet && isAddress(wallet) ? await getWalletSwaps(chain, token, wallet, snapshot.asOf) : null; // launch price in quote per token, from the start tick const launchPrice = 1 / (Math.pow(1.0001, l.start_tick) * Math.pow(10, q.decimals - 18)); + const baseline = { price: snapshot.priorPrice ?? launchPrice, hasPriorTrades: snapshot.priorPrice !== null }; return NextResponse.json( - { interval, from, launch: { t: launchT, price: launchPrice }, quote: { symbol: q.symbol, decimals: q.decimals, usd: l.quote_usd }, supply: Number(BigInt(l.supply)) / 1e18, candles, mine }, + { interval, from, asOf: snapshot.asOf, baseline, launch: { t: launchT, price: launchPrice }, quote: { symbol: q.symbol, decimals: q.decimals, usd: l.quote_usd }, supply: Number(BigInt(l.supply)) / 1e18, candles: snapshot.candles, mine }, { headers: { "cache-control": "no-store" } }, ); } diff --git a/app/src/app/feed/page.tsx b/app/src/app/feed/page.tsx index a062a4c7..7bcd0d8c 100644 --- a/app/src/app/feed/page.tsx +++ b/app/src/app/feed/page.tsx @@ -1,19 +1,22 @@ import type { Metadata } from "next"; -import { PostsFeed } from "@/components/launchpad/Posts"; +import Link from "next/link"; +import { ArrowUpRight } from "lucide-react"; +import CommunityFeed from "@/components/sections/CommunityFeed"; +import SectionIntro from "@/components/sections/SectionIntro"; +import styles from "@/components/sections/SectionShell.module.css"; import { listFeed } from "@/lib/launchpad/postsServer"; export const metadata: Metadata = { title: "Posts", description: "What people are saying about tokens launched on openlaunch.lol." }; export const dynamic = "force-dynamic"; export default async function FeedPage() { - const posts = await listFeed(100); + const result = await listFeed(100).then((posts) => ({ posts, failed: false })).catch(() => ({ posts: [], failed: true })); return ( -
-
-

Posts

-

Comments from holders, traders and creators, across every token. Posting happens on the token pages.

-
- +
+ + Explore tokens +
); } diff --git a/app/src/app/globals.css b/app/src/app/globals.css index 82b6f585..a2fd4718 100644 --- a/app/src/app/globals.css +++ b/app/src/app/globals.css @@ -8,7 +8,8 @@ never add a `.light` rule. The OS preference is not consulted. */ @custom-variant dark (&:is(.dark *)); -@theme { +/* Chart code reads tokens at runtime; retain even tokens with no utility class. */ +@theme static { --font-sans: var(--font-inter), ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; --font-mono: var(--font-space-mono), ui-monospace, SFMono-Regular, Menlo, monospace; --font-display: var(--font-unbounded), var(--font-inter), ui-sans-serif, system-ui, sans-serif; @@ -25,6 +26,8 @@ --color-muted: #64748b; /* AA on paper/card at body sizes */ --color-faint: #94a3b8; /* decorative / placeholders only */ --color-inverse: #ffffff; /* text on ink / brand / up / down fills */ + --color-skeleton: #ecebe8; + --color-skeleton-highlight: #f5f4f1; /* accent */ --color-brand: #0052ff; @@ -40,13 +43,14 @@ --color-warm: #d97706; --color-warm-ink: #b45309; /* text on warm-soft / paper */ --color-warm-soft: #fffbeb; - - /* values that used to be hardcoded in components — same light values, now themeable */ --color-holder-good-line: #bbf7d0; --color-holder-good-bg: #ecfdf3; - --color-skeleton: #ecebe8; - --color-skeleton-highlight: #f5f4f1; - --color-scrim: #0f172a; /* modal backdrop (rendered at 40%); stays dark in both themes */ + + /* modal backdrop (rendered at 40%); stays dark in both themes */ + --color-scrim: #0f172a; + /* chart-only tokens (read at runtime via getComputedStyle; @theme static keeps them) */ + --color-chart-grid: #f1f0ee; + --color-chart-vol: #cbd5e1; --shadow-card: 0 1px 2px rgba(15, 23, 42, 0.04), 0 4px 16px rgba(15, 23, 42, 0.04); --shadow-card-hover: 0 2px 4px rgba(15, 23, 42, 0.06), 0 10px 28px rgba(15, 23, 42, 0.09); @@ -56,10 +60,6 @@ :root { color-scheme: light; - /* Chart-only tokens live here, not in @theme: Tailwind drops @theme variables - that no class references, and the chart reads these with getComputedStyle. */ - --color-chart-grid: #f1f0ee; - --color-chart-vol: #cbd5e1; } /* ── Dark: black ground, alpha ramp of white, near-zero chroma greys. The blue @@ -75,6 +75,8 @@ --color-muted: #8c8c8c; /* white .55 */ --color-faint: #666666; /* white .40 */ --color-inverse: #000000; + --color-skeleton: #141414; + --color-skeleton-highlight: #1f1f1f; --color-brand: #4d8bff; --color-brand-strong: #7aa6ff; --color-brand-soft: #0d1b33; @@ -88,11 +90,9 @@ --color-warm-soft: #2a2007; --color-holder-good-line: #0d5233; --color-holder-good-bg: #06241a; - --color-skeleton: #141414; - --color-skeleton-highlight: #1f1f1f; - --color-scrim: #000000; --color-chart-grid: #1a1a1a; --color-chart-vol: #333333; + --color-scrim: #000000; --shadow-card: 0 1px 2px #00000026, 0 4px 16px #00000026; --shadow-card-hover: 0 2px 4px #00000033, 0 10px 28px #00000040; --shadow-sheet: 0 -8px 40px #00000066; @@ -132,15 +132,6 @@ body { font-feature-settings: "cv11", "ss01"; } -/* faint blue wash behind the hero only */ -.bb-sky { - position: absolute; - inset: 0 0 auto 0; - height: 520px; - pointer-events: none; - background: radial-gradient(1200px 400px at 50% -100px, var(--color-brand-soft), transparent); -} - /* tabular numbers everywhere money shows */ .tnum { font-variant-numeric: tabular-nums; @@ -176,7 +167,7 @@ body { } /* the sticky mobile action bar covers the bottom of the page: pad the footer */ -@media (max-width: 767px) { +@media (max-width: 1023px) { body:has(.bb-action-bar) .bb-footer { padding-bottom: calc(5.5rem + env(safe-area-inset-bottom)); } } @@ -222,7 +213,6 @@ input[type="number"] { -moz-appearance: textfield; appearance: textfield; } /* price / number flash when a live value changes */ @keyframes bb-pop { 0% { transform: scale(1); } 40% { transform: scale(1.06); } 100% { transform: scale(1); } } .bb-pop { animation: bb-pop 320ms ease-out 1; } - @media (prefers-reduced-motion: reduce) { .bb-toast-in, .bb-toast-out, .bb-confetti, .bb-pop { animation: none; } .bb-confetti { display: none; } @@ -230,11 +220,10 @@ input[type="number"] { -moz-appearance: textfield; appearance: textfield; } /* live list highlights */ @keyframes bb-row-new { - 0% { box-shadow: 0 0 0 0 rgba(0, 82, 255, 0.45), var(--shadow-card); border-color: var(--color-brand); background-color: var(--color-brand-soft); } - 60% { box-shadow: 0 0 0 10px rgba(0, 82, 255, 0), var(--shadow-card); } - 100% { box-shadow: var(--shadow-card); border-color: var(--color-brand); background-color: var(--color-card); } + 0% { background-color: var(--color-brand-soft); } + 100% { background-color: var(--color-paper); } } -.bb-row-new { animation: bb-row-new 1.6s ease-out 1; border-color: var(--color-brand) !important; } +.bb-row-new { animation: bb-row-new 1.6s ease-out 1; } @keyframes bb-row-buy { 0% { background-color: var(--color-up-soft); border-color: var(--color-up); } 100% { background-color: var(--color-card); border-color: var(--color-line); } } @keyframes bb-row-sell { 0% { background-color: var(--color-down-soft); border-color: var(--color-down); } 100% { background-color: var(--color-card); border-color: var(--color-line); } } .bb-row-buy { animation: bb-row-buy 2.2s ease-out 1; } @@ -242,16 +231,18 @@ input[type="number"] { -moz-appearance: textfield; appearance: textfield; } @media (prefers-reduced-motion: reduce) { .bb-row-new, .bb-row-buy, .bb-row-sell { animation: none; } } /* ── loading system: route progress bar, skeleton shimmer, spinner ─────────── */ -.bb-progress { position: fixed; top: 0; left: 0; height: 3px; width: 0; background: var(--color-brand); z-index: 100; opacity: 0; pointer-events: none; transition: width 0.25s ease, opacity 0.3s ease; box-shadow: 0 0 8px rgba(0, 82, 255, 0.45); } +/* scaleX rather than width: the bar animates on the compositor instead of + re-running layout on every frame. transform-origin pins it to the left edge. */ +.bb-progress { position: fixed; top: 0; left: 0; height: 3px; width: 100%; transform: scaleX(0); transform-origin: left; background: var(--color-brand); z-index: 100; opacity: 0; pointer-events: none; transition: transform 0.25s ease, opacity 0.3s ease; box-shadow: 0 0 8px color-mix(in srgb, var(--color-brand) 45%, transparent); } .bb-progress-run { opacity: 1; animation: bb-progress-creep 12s cubic-bezier(0.1, 0.6, 0.2, 1) forwards; } -.bb-progress-done { opacity: 0; width: 100%; transition: width 0.2s ease, opacity 0.35s ease 0.15s; } -@keyframes bb-progress-creep { 0% { width: 0; } 10% { width: 35%; } 40% { width: 70%; } 100% { width: 92%; } } +.bb-progress-done { opacity: 0; transform: scaleX(1); transition: transform 0.2s ease, opacity 0.35s ease 0.15s; } +@keyframes bb-progress-creep { 0% { transform: scaleX(0); } 10% { transform: scaleX(0.35); } 40% { transform: scaleX(0.7); } 100% { transform: scaleX(0.92); } } .bb-skel { background: linear-gradient(90deg, var(--color-skeleton) 0%, var(--color-skeleton-highlight) 40%, var(--color-skeleton) 80%); background-size: 200% 100%; animation: bb-shimmer 1.6s ease-in-out infinite; } @keyframes bb-shimmer { 0% { background-position: 120% 0; } 100% { background-position: -80% 0; } } .bb-spin { animation: bb-rotate 0.8s linear infinite; } @keyframes bb-rotate { to { transform: rotate(360deg); } } @media (prefers-reduced-motion: reduce) { - .bb-progress-run { animation: none; width: 60%; } + .bb-progress-run { animation: none; transform: scaleX(0.6); } .bb-skel { animation: none; background: var(--color-skeleton); } .bb-spin { animation-duration: 1.6s; } } @@ -261,10 +252,10 @@ input[type="number"] { -moz-appearance: textfield; appearance: textfield; } @keyframes bb-beacon { 0% { transform: scale(1); opacity: 0.55; } 70% { transform: scale(2.8); opacity: 0; } 100% { transform: scale(2.8); opacity: 0; } } @media (prefers-reduced-motion: reduce) { .bb-beacon::after { animation: none; opacity: 0; } } -/* "Hot right now" card: slow warm glow around the border */ -.bb-hot { box-shadow: 0 0 0 0 rgba(217, 119, 6, 0.0); animation: bb-hot 2.8s ease-in-out infinite; } -@keyframes bb-hot { 0%, 100% { box-shadow: 0 0 0 0 rgba(217, 119, 6, 0.0); } 50% { box-shadow: 0 0 0 4px rgba(217, 119, 6, 0.14); } } -@media (prefers-reduced-motion: reduce) { .bb-hot { animation: none; } } +/* One finite entry reaction for a real activity event. */ +.bb-tape-enter { animation: bb-tape-enter 240ms ease-out both; } +@keyframes bb-tape-enter { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: translateY(0); } } +@media (prefers-reduced-motion: reduce) { .bb-tape-enter { animation: none; } } /* ── animated theme toggle (View Transitions) ─────────────────────────────── */ /* The toggler drives the reveal itself with a clip-path animation on ::view-transition-new(root), so the browser's default cross-fade has to be @@ -295,4 +286,4 @@ input[type="number"] { -moz-appearance: textfield; appearance: textfield; } ::view-transition-new(root) { animation: none !important; } -} \ No newline at end of file +} diff --git a/app/src/app/launch/page.tsx b/app/src/app/launch/page.tsx index e577155c..62db1bfc 100644 --- a/app/src/app/launch/page.tsx +++ b/app/src/app/launch/page.tsx @@ -4,7 +4,7 @@ import { ethUsd } from "@/lib/launchpad/ethPrice"; import { isChainKey } from "@/lib/chainPublic"; export const metadata: Metadata = { - title: "Launch a token — free", + title: "Launch a token for free", description: "Deploy a token on Base or Robinhood Chain with 100% of supply locked as Uniswap v4 liquidity. No platform fee. Gas only.", }; @@ -15,11 +15,10 @@ export default async function LaunchPage({ searchParams }: { searchParams: Promi const usd = await ethUsd(); return ( <> -

Launch a token

-

Pick a chain, fill this in, sign once, done. No platform fee — the only thing you pay is gas.

+

Pick a chain, fill this in, sign once, done. No platform fee. You only pay gas.

diff --git a/app/src/app/layout.tsx b/app/src/app/layout.tsx index 14c3ab0a..920241b0 100644 --- a/app/src/app/layout.tsx +++ b/app/src/app/layout.tsx @@ -7,10 +7,9 @@ import TxToasts from "@/components/launchpad/TxToasts"; import LiveProvider from "@/components/launchpad/LiveProvider"; import { getLaunchFeed, getLaunchTotals } from "@/lib/launchpad/queries"; import { ethUsd } from "@/lib/launchpad/ethPrice"; -import { SITE_URL, explorerAddress } from "@/lib/chainPublic"; -import { launchpad } from "@/lib/launchpad/config"; -import { BRAND, BRAND_DOMAIN, BRAND_GITHUB, BRAND_TLD, BRAND_X, SITE_DESCRIPTION, SITE_TITLE, SOCIAL_DESCRIPTION } from "@/lib/brand"; -import Mark from "@/components/launchpad/Mark"; +import { SITE_URL } from "@/lib/chainPublic"; +import { BRAND, BRAND_DOMAIN, BRAND_X, SITE_DESCRIPTION, SITE_TITLE, SOCIAL_DESCRIPTION } from "@/lib/brand"; +import Footer from "@/components/Footer"; import RouteProgress from "@/components/RouteProgress"; import ThemeProvider from "@/components/ThemeProvider"; import { Suspense } from "react"; @@ -33,17 +32,15 @@ export const metadata: Metadata = { }; // Light is the default; dark is a class the header toggle adds, never an OS preference, so the -// pre-CSS hints stay light (as on main). ThemeProvider keeps theme-color in step after a toggle. +// pre-CSS hints stay light. ThemeProvider keeps theme-color in step after a toggle. export const viewport: Viewport = { themeColor: "#FAFAF8", colorScheme: "light", width: "device-width", initialScale: 1, viewportFit: "cover" }; export default async function RootLayout({ children }: { children: React.ReactNode }) { const usd = await ethUsd(); const [feed, totals] = await Promise.all([getLaunchFeed(24, usd).catch(() => []), getLaunchTotals(usd)]); - const b = launchpad("base"); - const r = launchpad("robinhood"); return ( - + @@ -53,36 +50,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo
{children}
- +
diff --git a/app/src/app/llms.txt/route.ts b/app/src/app/llms.txt/route.ts index 82b60289..1be3ef9e 100644 --- a/app/src/app/llms.txt/route.ts +++ b/app/src/app/llms.txt/route.ts @@ -19,7 +19,7 @@ export function GET() { const r = launchpad("robinhood"); const body = `# ${BRAND_DOMAIN} -> Open-source (MIT — https://github.com/Gitlawb/openlaunch), zero-fee token launchpad on Base (8453) and Robinhood Chain (4663). One transaction deploys a token and locks 100% of its supply as +> Open-source (MIT: https://github.com/Gitlawb/openlaunch), zero-fee token launchpad on Base (8453) and Robinhood Chain (4663). One transaction deploys a token and locks 100% of its supply as > Uniswap v4 liquidity, forever. No platform fee: the factory and locker have no fee address at all. ## Source verification diff --git a/app/src/app/me/page.tsx b/app/src/app/me/page.tsx index 49857732..7b3d0a7c 100644 --- a/app/src/app/me/page.tsx +++ b/app/src/app/me/page.tsx @@ -1,16 +1,15 @@ import type { Metadata } from "next"; import MeDashboard from "@/components/launchpad/MeDashboard"; +import SectionIntro from "@/components/sections/SectionIntro"; +import shell from "@/components/sections/SectionShell.module.css"; export const metadata: Metadata = { title: "Me", description: "Your launches, fees, holdings and trades on Base and Robinhood Chain." }; export const dynamic = "force-dynamic"; export default function MePage() { return ( -
-
-

Me

-

Your launches and the fees they earn, your holdings, your trades. Collect fees, edit token details.

-
+
+
); diff --git a/app/src/app/not-found.tsx b/app/src/app/not-found.tsx index 62f2b56e..61e2518a 100644 --- a/app/src/app/not-found.tsx +++ b/app/src/app/not-found.tsx @@ -5,7 +5,7 @@ export default function NotFound() { return (
404
-

Nothing here. That token was not launched here — or not yet.

+

Nothing here. That token has not been launched here.

Back to the launchpad diff --git a/app/src/app/opengraph-image.tsx b/app/src/app/opengraph-image.tsx index ccfa716e..4008451e 100644 --- a/app/src/app/opengraph-image.tsx +++ b/app/src/app/opengraph-image.tsx @@ -66,7 +66,7 @@ export default async function OgImage() { Robinhood Chain
- One transaction deploys your token and locks 100% of supply as Uniswap v4 liquidity — forever. Nobody can pull it. We take nothing. + One transaction deploys your token and locks 100% of supply as Uniswap v4 liquidity forever. Nobody can pull it. We take nothing.
diff --git a/app/src/app/rules/page.tsx b/app/src/app/rules/page.tsx index 5ac46962..a9b4df32 100644 --- a/app/src/app/rules/page.tsx +++ b/app/src/app/rules/page.tsx @@ -1,27 +1,34 @@ import type { Metadata } from "next"; import Link from "next/link"; -import { btn, cardPad } from "@/components/ui"; +import { ArrowDown, ArrowRight, ArrowUpRight, ChevronDown, FileCode2, LockKeyhole } from "lucide-react"; +import SectionIntro from "@/components/sections/SectionIntro"; +import shell from "@/components/sections/SectionShell.module.css"; +import styles from "@/components/sections/RulesGuide.module.css"; import { launchpad } from "@/lib/launchpad/config"; -import { CHAIN_KEYS, CHAIN_LABELS, explorerAddress } from "@/lib/chainPublic"; +import { CHAINS, CHAIN_KEYS, CHAIN_LABELS, explorerAddress } from "@/lib/chainPublic"; import { BRAND_GITHUB } from "@/lib/brand"; export const metadata: Metadata = { title: "How it works", description: "What a launch does on-chain, what it costs (gas), and what can never happen to your liquidity." }; const STEPS = [ - ["Deploys your token", "A plain ERC-20 with EIP-2612 permit. Fixed supply of 1,000,000,000. No mint, no pause, no blacklist, no transfer tax, no owner."], - ["Opens a Uniswap v4 pool", "ETH / your token — or a tokenized stock / your token (Coinbase stocks on Base, Robinhood Stock Tokens on Robinhood Chain), or USDG on Robinhood Chain — no hook. The pool starts at the market cap you pick."], - ["Locks 100% of supply as liquidity", "The whole supply goes into one single-sided position. Its NFT is minted to an ownerless locker that has no function to withdraw, transfer or shrink it. Ever."], - ["Registers the fee routing", "The trading fee you chose (0%, 1% or 3%) goes 100% to the beneficiaries you named, or is burned if you named none. Fixed at launch, unchangeable."], + { title: "Your token is deployed", description: "A plain ERC-20 with EIP-2612 permit. Fixed supply, 1 billion by default. No mint, no pause, no blacklist, no transfer tax, no owner.", value: "1B", label: "default supply" }, + { title: "A market opens", description: "A Uniswap v4 pool pairs your token with ETH, a tokenized stock (Coinbase stocks on Base, Robinhood Stock Tokens on Robinhood Chain), or USDG on Robinhood Chain. No hook. The pool starts at the market cap you pick.", value: "v4", label: "Uniswap pool" }, + { title: "The liquidity position is locked", description: "100% of the supply goes into one single-sided position at launch. Its NFT is minted to an ownerless locker that has no function to withdraw, transfer or shrink it. Ever.", value: "100%", label: "deposited at launch" }, + { title: "Fee routing is written in", description: "The trading fee you choose (0%, 1% or 3%) goes 100% to the beneficiaries you name, or is burned if you name none. Fixed at launch, unchangeable.", value: "0 / 1 / 3%", label: "your trading fee" }, ] as const; -const NEVER = [ - ["No platform fee", "There is no fee address, fee variable or treasury anywhere in the factory or the locker. Nothing to switch on later — it cannot be added to immutable code."], - ["No rug", "Nobody can remove liquidity: not the creator, not us. Collecting fees removes zero liquidity."], - ["No pre-mine", "Every token starts inside the pool. The only way to hold any is to buy."], +const FIXED = [ + ["No platform fee", "There is no fee address, fee variable or treasury anywhere in the factory or the locker. Nothing to switch on later. It cannot be added to immutable code."], + ["No liquidity withdrawal", "Nobody can remove liquidity: not the creator, not us. Collecting fees removes zero liquidity."], + ["No pre-mine", "Every token starts inside the pool. There is no creator-reserved allocation."], ["No admin", "No owner, no pause, no upgrade, no allowlist. The contracts are the same for everyone, forever."], ] as const; -/** Where each chain's contract source is verified (links go to the explorer's contract record). */ +const CONTENTS = [ + ["launchpad", "The launch"], ["fees", "Fees & routing"], ["immutable", "What stays fixed"], ["know", "Before you begin"], ["contracts", "Contracts"], +] as const; + +/** Verification records stay chain-specific and point to each deployed contract. */ const VERIFIERS: Record<(typeof CHAIN_KEYS)[number], { name: string; url: (addr: string) => string }[]> = { base: [ { name: "Basescan", url: (a) => `https://basescan.org/address/${a}#code` }, @@ -36,117 +43,96 @@ const VERIFIERS: Record<(typeof CHAIN_KEYS)[number], { name: string; url: (addr: export default function RulesPage() { return ( -
-
-

How it works

-

One transaction on Base or Robinhood Chain. Gas is the only cost — usually a few cents.

-
+
+ + Launch a token + +
+ + +
+
+
+

01 / The launch

One signature.
Four things happen.

+

All in the same transaction.
No separate setup. No platform fee.

+
+
    + {STEPS.map(({ title, description, value, label }, i) => ( +
  1. + +

    {title}

    {description}

    +
    {value}{label}
    +
  2. + ))} +
+

+
-
-

What a launch does

-
    - {STEPS.map(([t, d], i) => ( -
  1. - {i + 1} -
    -
    {t}
    -

    {d}

    +
    +

    02 / Fees & routing

    Know where it goes.

    +
    +
    Platform fee0%

    We take nothing.
    Gas is paid to the network.

    Check the code
    +
    +

    The creator chooses the trading fee

    +
    +
    0%
    No pool trading fee. Network gas still applies. Nobody earns fees from volume, including the creator.
    +
    1% or 3%
    Uniswap charges it on every trade and it accrues to the locked position. The platform takes no share.
    +
    +

    Beneficiaries and shares are set at launch and can never be changed. A promise like “half the fees go to this address” is enforced by the contract, not by us.

    -
  2. - ))} -
-
+
+
+

Anyone can collect. The routing decides who receives.

Press Collect on the token page. Accrued fees leave the pool and are paid straight to the beneficiaries in the same transaction, or burned to 0x…dEaD if the launch has no beneficiary.

+

If a beneficiary cannot receive the payment, their share is credited to them and can be claimed any time. It is never lost and never blocks the others.

+
+
-
-

Trading fees, in plain words

-
-

- 0% — a feeless pool. Trades cost only Uniswap gas. Nobody earns from volume, including the creator. -

-

- 1% or 3% — Uniswap charges it on every trade and it accrues to the locked position. Anyone can press Collect on the token page: the accrued fees leave - the pool and are paid straight to the beneficiaries in that same transaction (or burned to 0x…dEaD if the launch has no beneficiary). If a beneficiary cannot - receive the payment, their share is credited to them and can be claimed any time; it is never lost and never blocks the others. -

-

Beneficiaries and shares are set at launch and can never be changed — a promise like “half the fees go to this address” is enforced by the contract, not by us.

-
-
+
+

03 / What stays fixed

Code, not a promise.

Read the contracts
+
    {FIXED.map(([title, description]) =>
  • {title}

    {description}

  • )}
+
-
-

What can never happen

-
    - {NEVER.map(([t, d]) => ( -
  • -
    {t}
    -

    {d}

    -
  • - ))} -
-
+
+

04 / Before you begin

Locked liquidity.
Not a guarantee of value.

+

Tokens launched here are created by their launchers, not by openlaunch. Do your own research; a locked pool does not make a token valuable. Nothing is refundable.

+
+
How does the price move?

Price follows a single-sided Uniswap v4 curve: the first buyers get the most tokens per unit of the quote asset, and every buy moves the price up. Sells move it down.

There is no anti-snipe mechanism. Bots can buy in the first block like anyone else.

+
What can change after launch?

Nothing on-chain can be edited after launch: not the name, not the fee, not the beneficiaries. Metadata (image, description, links) is stored by this site and can change; the token itself cannot.

+
What should I know about stock-quoted pools?

Stock-quoted pools use tokenized stocks as the quote asset: Coinbase tokenized stocks (B20) on Base, recognised here only from Base's official list and priced from Chainlink's on-chain feeds; Robinhood Stock Tokens on Robinhood Chain, recognised only from Robinhood's own registry.

Both are securities issued by third parties under Regulation S and are not offered to US persons. Coinbase's also exclude the UK, Canada, Australia, Singapore and Switzerland. Their issuers can pause transfers or freeze wallets in restricted jurisdictions. Holding or trading them is subject to the issuer's terms, not ours.

+
+
-
-

What you should know

-
-

Price follows a single-sided Uniswap v4 curve: the first buyers get the most tokens per ETH, and every buy moves the price up. Sells move it down. There is no anti-snipe mechanism; bots can buy in the first block like anyone else.

-

Nothing is refundable and nothing can be edited after launch — not the name, not the fee, not the beneficiaries. Metadata (image, description, links) is stored by this site and can change; the token itself cannot.

-

Tokens launched here are created by their launchers, not by openlaunch. Do your own research; a locked pool does not make a token valuable.

-

Stock-quoted pools use tokenized stocks as the quote asset: Coinbase tokenized stocks (B20) on Base, recognised here only from Base's official list and priced from Chainlink's on-chain feeds; Robinhood Stock Tokens on Robinhood Chain, recognised only from Robinhood's own registry. Both are securities issued by third parties under Regulation S and are not offered to US persons (Coinbase's also exclude the UK, Canada, Australia, Singapore and Switzerland); their issuers can pause transfers or freeze wallets in restricted jurisdictions. Holding or trading them is subject to the issuer's terms, not ours.

-
-
+
+

05 / The source of truth

Don't take our word for it.

+

Contracts, addresses and source verification. The same contract design on both chains, with chain-specific Uniswap deployments.

+
+ {CHAIN_KEYS.map((chain) => { + const config = launchpad(chain); + return
+

{CHAIN_LABELS[chain]}

Chain ID {CHAINS[chain].id}
+
{(["factory", "locker"] as const).map((kind) => { + const address = config[kind]; + return
{kind === "factory" ? "Factory" : "Locker"}
{address ? {address} : Not deployed yet}
; + })}
+ {config.factory && config.locker ?

Source verified

{VERIFIERS[chain].map((verifier) =>
{verifier.name}Factory Locker
)}
: null} +
; + })} +
+

Uniswap v4's PoolManager, PositionManager, Permit2 and Universal Router are Uniswap's canonical deployments. MIT-licensed source on GitHub ↗; the verification records for each chain are linked above.

+
-
-

Contracts

-
- {CHAIN_KEYS.map((k) => { - const c = launchpad(k); - return ( -
-

{CHAIN_LABELS[k]}

-

- Factory:{" "} - {c.factory ? ( - - {c.factory} - - ) : ( - not deployed yet - )} -

-

- Locker:{" "} - {c.locker ? ( - - {c.locker} - - ) : ( - not deployed yet - )} -

- {c.factory && c.locker ? ( -

- Source verified:{" "} - {VERIFIERS[k].map((v, i) => ( - - {i > 0 ? " · " : ""} - {v.name} (factory,{" "} - locker) - - ))} -

- ) : null} -
- ); - })} -

Identical bytecode on both chains. Uniswap v4's PoolManager, PositionManager, Permit2 and Universal Router are Uniswap's canonical deployments. MIT-licensed source on{" "} - GitHub ↗; the verification records for each chain are linked above. -

+

Your next move

Make something of your own.

Launch a token
-
- -
- - Launch a token -
); diff --git a/app/src/app/t/[chain]/[token]/layout.tsx b/app/src/app/t/[chain]/[token]/layout.tsx new file mode 100644 index 00000000..8e186017 --- /dev/null +++ b/app/src/app/t/[chain]/[token]/layout.tsx @@ -0,0 +1,20 @@ +import { notFound } from "next/navigation"; +import { isAddress } from "viem"; +import { isChainKey } from "@/lib/chainPublic"; +import { getLaunch } from "@/lib/launchpad/queries"; +import { memo } from "@/lib/launchpad/memo"; + +export const dynamic = "force-dynamic"; + +/** + * Existence gate for the token route. The page sits under a loading boundary, so by the time it + * calls notFound() the streamed shell has already gone out with a 200. A layout renders before that + * boundary, so a miss here is a real HTTP 404 (search engines drop the URL; the site 404 page shows). + */ +export default async function TokenLayout({ params, children }: { params: Promise<{ chain: string; token: string }>; children: React.ReactNode }) { + const { chain, token } = await params; + if (!isChainKey(chain) || !isAddress(token)) notFound(); + const exists = await memo(`launch-exists:${chain}:${token.toLowerCase()}`, 3_000, async () => Boolean(await getLaunch(chain, token))); + if (!exists) notFound(); + return children; +} diff --git a/app/src/app/t/[chain]/[token]/loading.tsx b/app/src/app/t/[chain]/[token]/loading.tsx index 8a1ee861..387209fb 100644 --- a/app/src/app/t/[chain]/[token]/loading.tsx +++ b/app/src/app/t/[chain]/[token]/loading.tsx @@ -1,75 +1,18 @@ -import { Sk, SkStat } from "@/components/Skeleton"; +import { Sk } from "@/components/Skeleton"; -/** Token page skeleton: header, chart, stats, trades on the left; trade panel on the right. */ export default function Loading() { - return ( -
-
- - -
-
- - -
- -
- - - -
-
-
- - -
-
-
-
-
-
- - -
- -
-
- {Array.from({ length: 6 }, (_, i) => ( - - ))} -
-
-
- -
- {Array.from({ length: 6 }, (_, i) => ( -
- - - - -
- ))} -
-
-
-
- - -
- {Array.from({ length: 4 }, (_, i) => ( - - ))} -
- -
-
- - - -
-
+ return
+ +
+
+
+
+
{Array.from({ length: 4 }, (_, i) =>
)}
-
- ); + +
+
; } diff --git a/app/src/app/t/[chain]/[token]/opengraph-image.tsx b/app/src/app/t/[chain]/[token]/opengraph-image.tsx index 9e65600e..25a97610 100644 --- a/app/src/app/t/[chain]/[token]/opengraph-image.tsx +++ b/app/src/app/t/[chain]/[token]/opengraph-image.tsx @@ -124,7 +124,7 @@ export default async function TokenOg({ params }: { params: Promise<{ chain: str
Token not found
)}
- Free. Open source. No platform fee — ever. + Free. Open source. No platform fee. Ever. {/* call to action: the one thing a reader can do from this card */} {card ? `Trade ${card.symbol} on ${BRAND}${BRAND_TLD}` : `Launch a token on ${BRAND}${BRAND_TLD}`} → diff --git a/app/src/app/t/[chain]/[token]/page.tsx b/app/src/app/t/[chain]/[token]/page.tsx index d09da700..e5b0591c 100644 --- a/app/src/app/t/[chain]/[token]/page.tsx +++ b/app/src/app/t/[chain]/[token]/page.tsx @@ -2,8 +2,9 @@ import type { Metadata } from "next"; import Link from "next/link"; import { notFound } from "next/navigation"; import { isAddress, type Address } from "viem"; +import { ArrowLeft, ArrowUpRight, Globe, Share2 } from "lucide-react"; import TokenAvatar from "@/components/launchpad/TokenAvatar"; -import FeeChip, { feeModeOf } from "@/components/launchpad/FeeChip"; +import { feeModeOf } from "@/components/launchpad/FeeChip"; import TradePanel from "@/components/launchpad/TradePanel"; import CollectPanel from "@/components/launchpad/CollectPanel"; import CopyChip from "@/components/launchpad/CopyChip"; @@ -12,14 +13,18 @@ import PriceChart from "@/components/launchpad/PriceChart"; import TokenComments from "@/components/launchpad/Posts"; import ChangeChip from "@/components/launchpad/ChangeChip"; import HoldersPanel from "@/components/launchpad/HoldersPanel"; +import TokenDetails from "@/components/launchpad/TokenDetails"; +import TokenTrades from "@/components/launchpad/TokenTrades"; +import LaunchReceipt from "@/components/launchpad/LaunchReceipt"; import { getHolderPanel } from "@/lib/launchpad/holdersServer"; import { memo } from "@/lib/launchpad/memo"; import { ago, nowMs } from "@/lib/launchpad/time"; -import ChainBadge from "@/components/launchpad/ChainBadge"; import { getLaunch, getSwaps } from "@/lib/launchpad/queries"; import { ethUsd } from "@/lib/launchpad/ethPrice"; import { NATIVE, TICK_SPACING, uniswapSwapUrl, type Quote } from "@/lib/launchpad/config"; -import { fmtCompact, fmtPrice, fmtQuote, fmtUsd } from "@/lib/launchpad/math"; +import { fmtCompact, fmtPrice, fmtQuote, fmtUsd, pipsToPct } from "@/lib/launchpad/math"; +import { marketCount as count } from "@/lib/launchpad/token-market"; +import { marketUsd } from "@/lib/launchpad/market-format"; import { CHAIN_LABELS, SITE_URL, explorerAddress, explorerName, explorerTx, isChainKey, shortAddr } from "@/lib/chainPublic"; import { stockByAddress } from "@/lib/launchpad/stocksServer"; import { BRAND_DOMAIN, BRAND_X } from "@/lib/brand"; @@ -32,7 +37,7 @@ export async function generateMetadata({ params }: { params: Promise<{ chain: st const l = isChainKey(chain) && isAddress(token) ? await getLaunch(chain, token) : null; if (!l) return { title: "Token not found" }; const title = `${l.name} (${l.symbol})`; - const description = clampSocial(l.description ?? `${l.name} launched on openlaunch.lol — 100% of supply locked as Uniswap v4 liquidity on ${CHAIN_LABELS[l.chain]}, ${l.lp_fee === 0 ? "0% trading fee" : "no platform fee"}.`, 155); + const description = clampSocial(l.description ?? `${l.name} launched on openlaunch.lol. 100% of supply locked as Uniswap v4 liquidity on ${CHAIN_LABELS[l.chain]}, ${l.lp_fee === 0 ? "0% trading fee" : "no platform fee"}.`, 155); // Next replaces nested metadata objects rather than merging them, so repeat the site-level fields here: // og:site_name (Discord shows it above the title) and twitter summary_large_image (full-width card). return { @@ -65,213 +70,92 @@ export default async function TokenPage({ params }: { params: Promise<{ chain: s const fdvUsd = l.fdv_usd; const chainLabel = CHAIN_LABELS[chain]; const fdvQuoteLabel = fmtQuote(BigInt(Math.round(l.fdv_quote * 10 ** quote.decimals)), quote.decimals, quote.symbol); - const shareText = `${l.name} ($${l.symbol}) on ${chainLabel} — ${l.lp_fee === 0 ? "0% fee" : mode === "burn" ? "fees burned" : "no platform fee"}, liquidity locked forever`; + const shareText = `${l.name} ($${l.symbol}) on ${chainLabel}. ${l.lp_fee === 0 ? "0% fee" : mode === "burn" ? "fees burned" : "no platform fee"}, liquidity locked forever`; + + const supplyLabel = fmtCompact(Number(BigInt(l.supply)) / 1e18, 0); + const feeRoute = mode === "free" ? "No trading fee" : `${pipsToPct(l.lp_fee)} trading fee → ${mode === "burn" ? "burned" : mode === "split" ? "beneficiaries" : "beneficiary"}`; + const utility = "inline-flex min-h-9 items-center gap-1.5 rounded-lg border border-line px-2.5 text-xs text-muted hover:border-line-strong hover:text-ink"; return ( <> -
-
-