- Arc gas reserves sized to launch-day fees with margin: the base fee ran
~80 gwei plus ~30 gwei tips (measured), four times the 20 gwei floor the
reserves were sized at, so a first buy right after a launch could have
failed for gas. Launch reserve 1 USDC, swap reserve 0.2 USDC; blurb updated.
- the single-token holder skip logs an alert and says what stays off; the
"holds only gas" copy is right where the quote is the gas token
- /api/health flags a configured chain the indexer has never run for; the
secrets script requires the Arc locker, deploy block and RPC once the Arc
factory is set, so Arc cannot go live unindexed
- the sync-lag alert threshold is per chain (block times differ ~20x) with
the global and per-chain env overrides
- LAUNCH_SYNC_CONFIRMATIONS_ARC stays pushable as an override
- sync errors are stored with every URL redacted: the message reaches
/api/health unauthenticated and a keyed provider URL carries its API key
- a chain that finalizes every block keeps 0 confirmations regardless of the
global LAUNCH_SYNC_CONFIRMATIONS, so no Arc-only variable is needed
- holder Transfer fetches bisect blocks once; the token list only splits at a
single over-cap block, and one token still over the cap has that block skipped
- quoteUsdOf prices the ETH quote by key; a bare address(0) is no longer ETH
- contracts/deployments/launchpad-arc.json records Arc's Uniswap v4 addresses
(factory and locker filled at deploy time) and a test pins the app's v4
config to every chain's record
- the hero tolerates a poll from a machine that predates a chain (per-chain
totals merge one level deeper, and a missing bucket counts as 0)
- the dev env example leaves the Arc fork override commented so Arc reads go
through the proxy unless a third anvil is started
- the wallet menu shows the bridge hint on a registry chain the site cannot
launch on yet; ?chain= for such a chain reads as all chains in the list,
the live and list APIs and the home page
- LAUNCH_FACTORY_ABI names NativeQuoteUnsupported with a friendly message
- the bridge reads CHAINS.arc directly; the alias wrappers are gone
- GITLAWB filter chip is offered only on chains that have GITLAWB (filters now
carry a chain list; Arc has none)
- /launch?chain=<unconfigured> mounts on a configured chain instead of a
disabled tile (production, before the Arc contracts are set)
- one shortfall, one message: the native-gas checks skip a quote that already
had the reserve applied (USDC on Arc)
- trade panel keeps a per-chain swap reserve for native and shared-gas buys
(an Arc pool quoted in native USDC no longer reserves an ETH-sized dust)
- indexer: a single block over the node's log cap splits the token list
instead of wedging the cursor; a query timeout is no longer bisected
- one chain-scoped quote-arm helper feeds the GITLAWB, USDG and USDC matches
- metadata validation error, env example, secrets script (Arc RPC optional
until Arc is live) and stale two-chain copy updated
Adds Arc (chain 5042, Circle's L1, public mainnet 2026-09-16) to the registry
behind the existing configured-chain gate: it reads "Coming soon" until the
factory env is set, so this ships before the contracts do.
- chain definition with USDC as the 18-decimal native asset and the public RPC
as the fallback; ARC_RPC_URL (Alchemy) overrides it server-side, the browser
keeps using the same-origin proxy
- Uniswap v4 addresses from Uniswap/contracts 5042.md, Universal Router v2.1.1
with the v2 swap layout (proven on a fork, see the contracts commit)
- a "usdc" quote at a fixed dollar on the Permit2 path, with presets, the list
filter, per-chain totals, and SQL pricing that no longer special-cases USDG
- gas reserve per chain, and the Arc quirk it exists for: the USDC quote IS the
gas token, so a first buy or a trade for the whole balance is refused and the
suggested buy leaves the reserve in the quote's own units
- stock source moved into client-safe config so the Stock quote button is not
offered on a chain without a registry
- indexer: per-chain confirmations (Arc finalizes every block: 0) and log
fetches that halve their range when a node caps results per call (Arc: 2000)
- env plumbing for the Arc factory, locker, deploy block and RPC across the
Dockerfile, fly config, secrets script and env example; site copy names all
three chains
The network breakdown gains one line: GITLAWB sent to 0x…dEaD by GITLAWB-quoted launches, one combined
figure for both chains (one supply bridged over LayerZero), shown as an amount, never in USD. It sums
fees_quote_burned over GITLAWB-quoted launches; GITLAWB is only ever the quote side, so that is the whole burn.
Along the way:
- The live poller merges totals field by field, so a poll that lands on an old machine during a rolling
deploy keeps the last value of a newly added total instead of dropping it.
- The receipt sync path no longer writes a Burned/Paid event for a position whose launch is not indexed
yet: such a row was pinned with token NULL and never rolled into any total (the poller's later pass hit
ON CONFLICT and the rebuild script joins on token). It now leaves the event for the poller, which
indexes the launch first. The rebuild script attaches any historical orphaned rows by position id.
- The dollar totals (volume, fees to recipients, fees burned) fold every quote through its USD price and
silently counted a quote at $0 while its price was unknown (cold start, a feed down >15 min). The totals
now carry usd_partial and the hero shows ≈ with a tooltip while that is the case.
The home list defaulted to launch order, so the first screen was whatever was launched last:
duplicates from two wallets and zero-holder rows. Replace the Trending tab with Live and make it
the default. One rule, in lib/launchpad/ranking.ts and mirrored in SQL so paging stays correct:
A token is live when an outside wallet traded it in the last 24h. Outside = not the launcher and
not a buy inside the sniper window (launch block + 3, the holders panel's definition; one bot
sniped 121 launches within 2s and would otherwise have made every token live). Live tokens rank
by outside wallets this hour, then last trade, then holders. Then launches under 1h old, then
quiet launches under a divider, one row per launcher (the rest are folded into it).
- queries: per-row `traders_1h_ex` / `traders_24h_ex` (outside wallets) in both windows, a `live`
sort built as a CTE with the tier and a per-launcher row_number, `launcher_collapsed` on the kept
row; `trending` stays accepted and orders like `live`. Nothing hidden: New, search and the API
keep every launch. No schema change.
- ranking.ts absorbs trending.ts: tier + chip helpers for the list, strip score now weighs distinct
outside wallets first (trades log-scaled), day window needs an outside wallet.
- list UI: Live tab first and default, rank numbers only for live rows, a chip per row saying why it
sits there, a "Quiet launches" divider; the URL writes sort=new explicitly now.
- docs: llms.txt, agents page, rules FAQ ("Why is my token under quiet launches?"); smoke checks the
live sort and its fields.
Verified against a local production copy: 51 live / 103 quiet rows at a pinned clock, one row per
launcher outside live, no overlap between pages, no self-traded token counted as live.
public/gitlawb-mark.png is now generated from the web repo's planet logo: the mark recoloured
white, the blue satellite kept, cropped and squared on a navy ground with rounded corners
baked into the alpha. Badge pill is the same navy so tile and pill read as one piece.
Four points from the CHANGES_REQUESTED review, plus the test-key concern:
1. Cross-platform test command. `npm test` used inline `TEST_MOCK_DB=1
NODE_OPTIONS=...` which fails on Windows (cmd does not expand inline
env vars). The mock swap is actually done by tsconfig.test.json paths,
so TEST_MOCK_DB was vestigial; the only need is the react-server
condition for `server-only`. Pass `--conditions=react-server` straight
to tsx, which is portable with no env var and no new dependency.
Removed `test:unit`: plain `node --test` cannot run the server test
(needs tsconfig path mocks + react-server), so it was broken, and
`test` now runs the whole suite cross-platform.
2. RPC outage classification. viem 2.55.19's verifyMessage returns false
(rather than throwing) when the RPC transport is unreachable, so an
outage surfaced as 401 "signature does not match" instead of 503.
Added a getChainId liveness probe on the false branch: if the probe
throws, the false came from an outage (503); otherwise it is a genuine
mismatch (401). The nonce is intact either way. The probe only runs on
the failure path, so the happy path keeps its cost.
3. Transport-level outage test through viem itself. The chain mock now
returns a real viem client with an unreachable transport when
transportOutage is set, so the outage path runs through viem's actual
verifyErc6492 path (the same one smart-wallet verification uses) rather
than a hand-rolled stub. Added a unit test asserting 503 + nonce intact.
4. e2e IP-limit test. The IP check used a fresh IP per request, so it
passed even with no limiter. Kept the cross-IP isolation check and
added a single repeated IP past the 30/min limit, asserting it
eventually gets 429.
Test key: replaced the hardcoded TEST_PRIVATE_KEY with a freshly
generated disposable key (never funded, public by design) and labelled
it. The old key is in git history; if it was ever used for real funds
those should be moved, since it is now public.
CodeRabbit review caught two issues:
1. The wallet rate limit added to issueNonce was still abusable. The nonce
endpoint is unauthenticated (no signature), so the launcher check only
confirms the supplied wallet matches the public on-chain launcher
address. An attacker who knows that address passes the launcher check
and spends the wallet bucket. Removed the wallet rate limit from
issueNonce entirely; the IP limit in the route is the only limit on
this endpoint.
2. The db-mock only checked nonce + used_at, not the full predicate set
(chain_id, token, wallet, expires_at). The begin() did not
snapshot/restore on rollback, and the metadata branch could not fail,
so nonce rollback on a failed INSERT was untested. Fixed: the mock now
checks all predicates, snapshots state before the transaction callback
and restores on rejection, and supports a configurable metadata-write
failure.
Updated tests: the nonce DoS test now uses the creator's wallet (the real
attack vector), added predicate-match tests (wrong wallet, wrong chain_id,
expired), and a transaction rollback test.
Hits the real Next.js dev server with a real Postgres DB to verify the
fix end-to-end. Covers: non-creator rejection, creator nonce issuance,
10 non-creator requests not freezing the creator's budget (finding 1),
invalid signature not burning the nonce (finding 2), valid signature
consuming the nonce and writing metadata, nonce reuse rejection, 10
invalid-sig edit requests not freezing the creator's edit budget
(finding 1), non-creator edit rejection, and IP-keyed rate limiting.
RED-GREEN verified: 3 tests fail on the original code for the right
reasons (nonce burned on invalid sig, wallet frozen by attacker, wallet
bucket spent before creator check), all 17 pass after the fix.
- Metadata ownership is per (chain, launcher, meta_key), the thing the public URI is keyed by:
the first registration under a key fixes its content; later rows under the same key must match
exactly (the salt search) or get 409. Serialized with a transaction-scoped advisory lock so two
concurrent first writes cannot both win (tested: 10/10 races → one 200, one 409). Reads prefer the
launched row, then the FIRST registration, never a newer one.
- fly-secrets.sh reads every key independently from the secrets file (no RPC derivation), fails on
missing required keys, supports --dry-run; verified against mocked files.
- rebuild-launch-totals.sql / rebuild-holders.sql run in one transaction holding SHARE locks on the
raw event tables and EXCLUSIVE on the target table, so ingestion waits instead of racing (tested:
an ingestion started mid-rebuild blocked 3s, then applied on top; totals consistent).
- Metadata registration is INSERT-only for unsigned callers: the creator registers before broadcasting
(salt still private); replaying the public launch params returns 409 and cannot overwrite image /
description / website. Later changes go through the wallet-signed edit flow. (review finding 1)
- The on-chain metadataURI is keyed by a client-chosen meta_key instead of the salt, so findSalt's
result stays valid for USDG / stock quotes. Proven on the live Robinhood factory: 17/100 deterministic
USDG launches reverted with the old scheme, 0/100 with the new one. (finding 2)
- Swap and fee ingestion commit the event row and the launch totals in one transaction; a rebuild script
recomputes totals from raw events if ever needed (production checked: no drift). (finding 3)
- Fresh checkout: npm run migrate reads DATABASE_URL from .env.local/.env; fly-secrets.sh is self-contained
(FLY_APP, SECRETS_ENV); README run + deploy steps corrected.
- CI workflow (lint, typecheck, unit tests, production build; forge build + unit tests), CONTRIBUTING.md,
SECURITY.md with private reporting.
openlaunch — launch a token. Free. Open source. On Base or Robinhood Chain.
Contracts (LaunchFactory / LaunchLocker / LaunchToken, Foundry), the site (Next.js),
brand assets. No platform fee, liquidity locked forever.