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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ Creators choose a 0 / 1 / 3 % trading fee that is pushed **in full** to benefici
- **Rug-proof liquidity.** The position NFT lives in a locker with no owner, no pause, no upgrade path and
no transfer function. Proven with an adversarial fork suite against the live deployment.
- **Two chains, one set of addresses.** Base and Robinhood Chain, same contracts, same rules.
- **Any quote asset.** ETH, USDG, or a **tokenized stock**: Coinbase's B20 stocks on Base
- **Any quote asset.** ETH, USDG, **GITLAWB** ([Gitlawb](https://gitlawb.com)'s token, on Base and bridged 1:1 to
Robinhood Chain; priced from its Base Uniswap v4 WETH pool; fees paid or burned in GITLAWB), or a **tokenized stock**: Coinbase's B20 stocks on Base
(NVDAc, AAPLc, TSLAc, …, priced from Chainlink on-chain feeds) and Robinhood Stock Tokens on Robinhood Chain.
- **Trade in-page.** Buys and sells go straight to the Uniswap v4 pool through the Universal Router; the site
never touches funds.
Expand Down
Binary file added app/public/gitlawb-mark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion app/scripts/smoke.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ set -u
B="${1:-https://openlaunch.lol}"
fail=0
check() { code=$(curl -s -o /dev/null -w '%{http_code}' "$B$1"); ok=0; case "$1" in /api/launch/meta/*) { [ "$code" = 200 ] || [ "$code" = 404 ]; } && ok=1;; /t/0x*) { [ "$code" = 307 ] || [ "$code" = 308 ]; } && ok=1;; *) [ "$code" = 200 ] && ok=1;; esac; [ $ok = 1 ] && echo " ok $code $1" || { echo " FAIL $code $1"; fail=1; }; }
for p in / /me /feed "/api/posts?feed=1" "/api/quotes?chain=robinhood&q=AAPL" "/?chain=robinhood" /launch "/launch?chain=robinhood" /rules /agents /llms.txt /api/health "/api/launch/live?sort=trending&window=1h" "/api/launch/live?sort=new&chain=robinhood" /api/launch/feed /api/presence; do check "$p"; done
for p in / /me /feed "/api/posts?feed=1" "/api/quotes?chain=robinhood&q=AAPL" "/api/quotes?chain=base" "/?filter=gitlawb" /gitlawb-mark.png "/?chain=robinhood" /launch "/launch?chain=robinhood" /rules /agents /llms.txt /api/health "/api/launch/live?sort=trending&window=1h" "/api/launch/live?sort=new&chain=robinhood" /api/launch/feed /api/presence; do check "$p"; done
for c in base robinhood; do
T=$(curl -s "$B/api/launch/list?chain=$c&limit=1" | python3 -c 'import json,sys; l=json.load(sys.stdin)["launches"]; print(l[0]["token"] if l else "")')
if [ -n "$T" ]; then check "/t/$c/$T"; check "/t/$c/$T/opengraph-image"; check "/api/launch/meta/$T?chain=$c"; check "/t/$T"; else echo " skip no launches on $c"; fi
Expand Down
4 changes: 2 additions & 2 deletions app/src/app/agents/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export default function AgentsPage() {
<div className={styles.reference}>
<div className={styles.referenceHeading}><Braces size={16} aria-hidden="true" /><h3>Parameters, without the guesswork</h3></div>
<dl className={styles.parameters}>
<div><dt><code>quote</code></dt><dd>ETH uses address zero. For USDG on Robinhood Chain, use <code>0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168</code> and call <code>findSalt(...)</code> first so the token address sorts above the quote.</dd></div>
<div><dt><code>quote</code></dt><dd>ETH uses address zero. For GITLAWB use <code>0x5F980Dcfc4c0fa3911554cf5ab288ed0eb13DBa3</code> on Base or <code>0xd1b0d44E4f6ed940fcC7A9F59Bf30Daf62cCFe3D</code> on Robinhood Chain; for USDG on Robinhood Chain, <code>0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168</code>. For any ERC-20 quote call <code>findSalt(...)</code> first so the token address sorts above the quote.</dd></div>
<div><dt><code>supply</code></dt><dd><code>0</code> means the default <code>1B</code> supply.</dd></div>
<div><dt><code>startTick</code></dt><dd>Sets the opening market cap. A multiple of <code>200</code>; <code>184200 ≈ 10 ETH</code> FDV for <code>1B</code> supply.</dd></div>
<div><dt><code>lpFee</code></dt><dd>In pips: <code>0</code>, <code>10000 = 1%</code>, or <code>30000 = 3%</code>.</dd></div>
Expand Down Expand Up @@ -106,7 +106,7 @@ export default function AgentsPage() {
<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.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, USDG 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}>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, GITLAWB (both chains), USDG (Robinhood Chain) 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>
</div>
<p className={styles.exampleNote}>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.</p>
Expand Down
6 changes: 4 additions & 2 deletions app/src/app/api/quotes/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ import { ensureRegistry, stockList, stockPrices } from "@/lib/launchpad/stocksSe
import { searchStocks } from "@/lib/launchpad/stocks";
import { searchBaseStocks, stockTileSvg } from "@/lib/launchpad/baseStocks";
import { memo } from "@/lib/launchpad/memo";
import { gitlawbUsd } from "@/lib/launchpad/gitlawbServer";

export const dynamic = "force-dynamic";

/**
* GET /api/quotes?chain=<base|robinhood>[&q=AAPL] → quote assets the launch form may offer on that chain:
* the fixed ones (ETH, USDG) plus tokenized stocks from that chain's registry (with live USD):
* the fixed ones (ETH, USDG, GITLAWB — with GITLAWB's live USD) plus tokenized stocks from that chain's registry (with live USD):
* Coinbase tokenized stocks on Base, Robinhood Stock Tokens on Robinhood Chain.
* Only registry addresses are ever labelled as stocks.
*/
Expand All @@ -19,7 +20,8 @@ export async function GET(req: Request) {
const chain = u.searchParams.get("chain");
if (!isChainKey(chain)) return NextResponse.json({ error: "bad chain" }, { status: 400 });
const q = (u.searchParams.get("q") ?? "").slice(0, 12);
const fixed = launchpad(chain).quotes.map((x) => ({ ...x, usd: x.usd }));
const gl = await gitlawbUsd(); // one price for both chains: the Robinhood token is the Base token bridged 1:1
const fixed = launchpad(chain).quotes.map((x) => ({ ...x, usd: x.key === "gitlawb" ? gl : x.usd }));
const stocks = await memo(`quotes:stocks:${chain}:${q.toUpperCase()}`, 30_000, async () => {
let hits: { address: string; symbol: string; name: string; decimals: number; logo: string | null }[];
if (chain === "base") hits = searchBaseStocks(q, 13).map((s) => ({ ...s, logo: stockTileSvg(s.symbol) }));
Expand Down
5 changes: 3 additions & 2 deletions app/src/app/launch/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Metadata } from "next";
import LaunchForm from "@/components/launchpad/LaunchForm";
import { ethUsd } from "@/lib/launchpad/ethPrice";
import { gitlawbUsd } from "@/lib/launchpad/gitlawbServer";
import { isChainKey } from "@/lib/chainPublic";

export const metadata: Metadata = {
Expand All @@ -12,15 +13,15 @@ export const dynamic = "force-dynamic";

export default async function LaunchPage({ searchParams }: { searchParams: Promise<{ chain?: string }> }) {
const sp = await searchParams;
const usd = await ethUsd();
const [usd, gitlawb] = await Promise.all([ethUsd(), gitlawbUsd()]);
return (
<>
<main className="relative mx-auto max-w-6xl px-4 pt-6 sm:pt-10 pb-16 space-y-5 sm:space-y-6">
<header className="max-w-2xl">
<h1 className="font-display font-bold tracking-[-0.02em] text-ink text-3xl sm:text-4xl">Launch a token</h1>
<p className="mt-2 text-base text-body">Pick a chain, fill this in, sign once, done. No platform fee. You only pay gas.</p>
</header>
<LaunchForm ethUsd={usd} initialChain={isChainKey(sp.chain) ? sp.chain : "base"} />
<LaunchForm ethUsd={usd} gitlawbUsd={gitlawb} initialChain={isChainKey(sp.chain) ? sp.chain : "base"} />
</main>
</>
);
Expand Down
4 changes: 3 additions & 1 deletion app/src/app/llms.txt/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ ${verificationLine("Robinhood Chain (4663)", r, [

## What a launch does
1. deploys a fixed-supply ERC-20 (1,000,000,000; no mint/pause/blacklist/tax/owner)
2. initializes a Uniswap v4 pool quote/token (quote = ETH, or USDG on Robinhood Chain; tick spacing 200, no hook) at the chosen start tick
2. initializes a Uniswap v4 pool quote/token (quote = ETH, GITLAWB, USDG on Robinhood Chain, or a registry stock token; tick spacing 200, no hook) at the chosen start tick
3. mints one single-sided position holding 100% of supply to an ownerless locker (no withdraw path exists)
4. registers fee routing: lpFee 0 | 10000 (1%) | 30000 (3%) pips; recipients [] = fees burned,
else {payout,bps}[] summing to 10000. Fixed forever.
Expand All @@ -47,6 +47,8 @@ Robinhood Chain (4663): LaunchFactory ${r.factory ?? "(not deployed yet)"} · La
Uniswap v4: Base PoolManager 0x498581fF718922c3f8e6A244956aF099B2652b2b, Universal Router 0x6fF5693b99212Da76ad316178A184AB56D299b43;
Robinhood PoolManager 0x8366a39CC670B4001A1121B8F6A443A643e40951, Universal Router 0x8876789976decbfcbbbe364623c63652db8c0904
USDG (Robinhood, 6 dec): 0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168
GITLAWB (Base, 18 dec): 0x5F980Dcfc4c0fa3911554cf5ab288ed0eb13DBa3 (Gitlawb's token; fees paid in GITLAWB, burned when recipients = [])
GITLAWB (Robinhood, 18 dec): 0xd1b0d44E4f6ed940fcC7A9F59Bf30Daf62cCFe3D (LayerZero OFT of the Base token, 1:1)

## Launch (contract call)
factory.launch((name, symbol, metadataURI, quote, supply=0, startTick, lpFee, salt, recipients))
Expand Down
3 changes: 2 additions & 1 deletion app/src/app/rules/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export const metadata: Metadata = { title: "How it works", description: "What a

const STEPS = [
{ 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: "A market opens", description: "A Uniswap v4 pool pairs your token with ETH, GITLAWB, 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;
Expand Down Expand Up @@ -108,6 +108,7 @@ export default function RulesPage() {
<div className={styles.questions}>
<details open><summary>How does the price move?<ChevronDown size={17} aria-hidden="true" /></summary><div><p>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.</p><p>There is no anti-snipe mechanism. Bots can buy in the first block like anyone else.</p></div></details>
<details open><summary>What can change after launch?<ChevronDown size={17} aria-hidden="true" /></summary><div><p>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.</p></div></details>
<details open><summary>What is a GITLAWB-quoted pool?<ChevronDown size={17} aria-hidden="true" /></summary><div><p>GITLAWB is Gitlawb&apos;s token on Base, bridged 1:1 to Robinhood Chain over LayerZero (one supply, two chains): an ordinary ERC-20 with no transfer restrictions and no issuer switch. Pick it as the quote and buyers pay in GITLAWB; any trading fee is paid in GITLAWB to the beneficiaries named at launch (by their shares), and a launch that names none burns GITLAWB on every trade.</p><p>Dollar figures for these pools come from the Uniswap v4 WETH/GITLAWB pool price on Base, cross-checked against a 30-minute on-chain average, and ETH/USD, on both chains. They are for display and sorting only; nothing on-chain depends on them.</p></div></details>
<details open><summary>What should I know about stock-quoted pools?<ChevronDown size={17} aria-hidden="true" /></summary><div><p>Stock-quoted pools use tokenized stocks as the quote asset: Coinbase tokenized stocks (B20) on Base, recognised here only from Base&apos;s official list and priced from Chainlink&apos;s on-chain feeds; Robinhood Stock Tokens on Robinhood Chain, recognised only from Robinhood&apos;s own registry.</p><p>Both are securities issued by third parties under Regulation S and are not offered to US persons. Coinbase&apos;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&apos;s terms, not ours.</p></div></details>
</div>
</section>
Expand Down
24 changes: 21 additions & 3 deletions app/src/app/t/[chain]/[token]/opengraph-image.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,17 @@ import { BRAND, BRAND_TLD } from "@/lib/brand";
import { isOwnImageUrl } from "@/lib/launchpad/images";
import { imagePublicBase, readImage } from "@/lib/launchpad/imageStore";
import sharp from "sharp";
import { readFile } from "node:fs/promises";
import path from "node:path";
import { GITLAWB_LOGO_BG, GITLAWB_LOGO_PATH } from "@/lib/launchpad/gitlawb";

/** Gitlawb's logo from public/, inlined once per process (satori needs a data URL or absolute URL). */
let gitlawbLogo: Promise<string | null> | null = null;
function gitlawbLogoDataUrl(): Promise<string | null> {
return (gitlawbLogo ??= readFile(path.join(process.cwd(), "public", GITLAWB_LOGO_PATH))
.then((b) => `data:image/png;base64,${b.toString("base64")}`)
.catch(() => null));
}

export const alt = "token on openlaunch.lol";
export const size = { width: 1200, height: 630 };
Expand Down Expand Up @@ -58,6 +69,7 @@ export default async function TokenOg({ params }: { params: Promise<{ chain: str
const [usd, fonts] = await Promise.all([ethUsd(), loadOgFonts()]);
const l = isChainKey(chain) && isAddress(token) ? await memo(`og:${chain}:${token.toLowerCase()}`, 30_000, () => getLaunch(chain, token, usd)) : null;
const card = l ? shapeCard(l, nowMs()) : null;
const glLogo = card?.quote?.kind === "gitlawb" ? await gitlawbLogoDataUrl() : null;
const logo = l ? await memo(`og-logo:${chain}:${token.toLowerCase()}`, 60_000, () => ownLogo(l.image_url)) : null;
const h = hueOf(token);
return new ImageResponse(
Expand Down Expand Up @@ -106,9 +118,15 @@ export default async function TokenOg({ params }: { params: Promise<{ chain: str
<div style={{ display: "flex", flexWrap: "wrap", gap: 12, marginTop: 26 }}>
{card.quote ? (
<span style={{ display: "flex", alignItems: "center", gap: 10, height: 40, padding: "0 16px 0 7px", borderRadius: 999, border: `1px solid ${LINE}`, background: "#fff", color: BODY, fontSize: 19, fontWeight: 600, whiteSpace: "nowrap" }}>
<span style={{ display: "flex", alignItems: "center", justifyContent: "center", width: 30, height: 30, borderRadius: 8, background: BLUE, color: "#fff", fontSize: card.quote.ticker.length > 4 ? 7 : card.quote.ticker.length > 3 ? 8 : 10, fontWeight: 800, letterSpacing: -0.5, whiteSpace: "nowrap", overflow: "hidden" }}>
{card.quote.ticker}
</span>
{card.quote.kind === "gitlawb" ? (
<span style={{ display: "flex", alignItems: "center", justifyContent: "center", width: 30, height: 30, borderRadius: 8, background: GITLAWB_LOGO_BG, overflow: "hidden" }}>
{glLogo ? <img src={glLogo} width={30} height={30} alt="" /> : null}
</span>
) : (
<span style={{ display: "flex", alignItems: "center", justifyContent: "center", width: 30, height: 30, borderRadius: 8, background: BLUE, color: "#fff", fontSize: card.quote.ticker.length > 4 ? 7 : card.quote.ticker.length > 3 ? 8 : 10, fontWeight: 800, letterSpacing: -0.5, whiteSpace: "nowrap", overflow: "hidden" }}>
{card.quote.ticker}
</span>
)}
priced in {card.quote.symbol}
</span>
) : null}
Expand Down
Loading
Loading