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.