AlphaCOLLAB runs on GenesisL1 mainnet and has had no external audit. Use small amounts.Security notes

COLLAB

The web app

COLLAB's interface is a static site: HTML, CSS and ES modules served as files. There is no bundler, no framework, no compile step and no Node.js anywhere in the toolchain; the release build only versions asset URLs and precompresses files. The browser talks only to the GenesisL1 RPC endpoint the visitor uses, to the visitor's wallet, to the site's own files (/config.js, /data/, /og/) and, when the visitor chooses a display currency, to the price API. This page is for developers who change the app: it is the contract every page and view follows.

Part
Developers
Version
COLLAB 2.2.0
Updated
Reading time
50 min

Layout

web/
  index.html daos.html dao.html votes.html tokens.html create.html descifi.html docs.html admin.html
  settings.html privacy.html 404.html   entries, one page module each (see "URLs"); 404.html runs
                            js/fallback.js. Every entry carries the portal shell's static markup
                            between <!-- shell:top --> and <!-- shell:foot --> markers, written by
                            tools/shell_html.py (see "The portal shell")
  pools/index.html locks/index.html   copies of descifi.html with their own head
                            (python3 tools/prerender.py --static-pages)
  docs.html docs/           the documentation, generated from docs/*.md by tools/build_docs.py: the
                            home, docs/<slug>/index.html, docs/<slug>.md, index.json, search.json;
                            llms.txt beside them (see "The docs pages")
  config.js                 generated per deployment (tools/write_web_config.py); config.local.js for dev
  BUILD.json                in a release tree only: the release and its ?v= (tools/version_assets.py)
  _headers _redirects       headers and rewrites for Netlify and Cloudflare Pages (_redirects also for IPFS
                            gateways; deploy/ has nginx and Caddy)
  robots.txt sitemap.xml manifest.webmanifest favicon.svg
  assets/css/app.css        the design system, shared components and every area's styles (see "Design system")
  assets/css/deploy.css     the deploy flow (the admin page's Deploy and Check tabs)
  assets/img/               icons.svg (the sprite, see "Icons"), logo.svg, og.png and the app icons
  assets/fonts/             Inter, JetBrains Mono, Source Serif 4 (WOFF2) with their OFL texts
  assets/deploy/collab-protocol.json   the deploy bundle (tools/export_deploy_artifacts.py)
  assets/docs/              the documentation's figures, WebP (tools/docs_figures.py)
  js/theme.js               classic script in <head>: theme before first paint, settings of 1.0 migrated
  js/consent.js             cookie notice, and Google Analytics only after consent where configured
  js/fallback.js            on 404.html: sends a clean path to its entry on hosts without rewrites
  js/vendor/                ethers 6.17.0 (ESM), libsodium.js 0.8.2 (sodium-0.8.2.js), ML-KEM-768 (mlkem-0.0.7.js),
                            markdown-it 14; each file carries its version in its name, so a new version gets a new name
  js/core/                  shared modules (below). Owned by the core team; do not fork them.
  js/pages/<page>.js        page entry points
  js/views/dao-<view>.js    DAO sections, lazily loaded by js/pages/dao.js
  js/views/new/<kind>.js    proposal builders, lazily loaded by js/views/dao-new.js
  js/views/profile-editors.js  the logo and description editors (wizard, profile builders, representatives)
  js/create/                the creation wizard: its steps, draft, presets and review
  js/descifi/               DeSciFi and the tokens pages: chain data, the market, swap, pools, locks, token list and page
  js/sealed/                sealed data: browser cryptography compatible with CIPNFT, keys, the vault's operations
  js/admin/                 /admin: the Admin API client and session (api.js, session.js), sign-in (signin.js), the
                            frame and tabs (app.js, parts.js, status.js, publish.js, site.js with sitecheck.js and lookup.js)
  js/deploy/                the deploy flow (flow.js) and Check (check.js), loaded by the admin page's Deploy and Check tabs

The admin page's API is documented in Admin API reference, and the formats of config.js, site.json and the /data/ files in Data formats.

Non-negotiable rules

Security

  • Never use innerHTML, outerHTML, insertAdjacentHTML, document.write or string HTML. Build DOM with h() / s() from core/dom.js. Every string from the chain (titles, descriptions, token names and symbols, constitutions, links, model metadata) is attacker-controlled. Clean names and symbols with cleanText() from core/tokens.js (it counts characters as code points, like clean_text() in tools/seo/text.py, and removes the same invisible, blank and bidi characters, astral ones included: change both together). Show a DAO by dao.name, never dao.title (see daodata.js). An element that shows a name or title from the chain gets dir: "auto"; inside a sentence, wrap the name in h("bdi", {}, name), so a right-to-left name never reorders the text around it.
  • Chain strings need not be valid UTF-8 (older contracts, other tokens and NFTs). Every interface from core/contracts.js (iface(), contract(), read.*) decodes them with U+FFFD for the bad bytes instead of failing the whole call; an ABI you write yourself goes through lenientInterface([...]), not new ethers.Interface.
  • Markdown goes through renderMarkdown() (core/md.js: markdown-it tokens rendered to DOM nodes, links filtered), inside a .doc container.
  • URLs go through safeUrl(); external links get target="_blank" rel="noopener noreferrer". User-supplied links additionally get nofollow.
  • Images from the chain are shown only as data: URLs produced by readImageUrl() (hash-verified, MIME allowlist) inside <img>; SVG never goes into the DOM as markup.
  • The Content-Security-Policy forbids inline scripts and inline style attributes. Set styles with the style: {…} attribute of h() (CSSOM, allowed) or with classes. No eval, no new Function.
  • No new third-party code. If something is missing, write it. No CDN, no remote fonts. Analytics exist only as js/consent.js loads them: in deployments that configure an ID, after Accept.
  • Amounts are BigInt in base units end to end. Parse input with parseUnits() / the form fields in core/forms.js; format with units() / l1() / money(). Never use floating point for amounts sent on-chain (fiat values from core/price.js are for display only). Read decimals from token metadata; never assume 18 for ERC-20 tokens.
  • Addresses are validated and checksummed with ethers.getAddress() (addressField does it).
  • Every transaction goes through send() / sendValue() from core/wallet.js: they preflight with estimateGas (reverts are explained before the wallet opens), show progress toasts and invalidate caches. Never call signer.sendTransaction directly.
  • Irreversible or dangerous user actions (giving up token powers, deleting a model, revealing data, declassifying, publishing keys) require confirmDialog() with plain consequences.
  • Browser storage only for conveniences, under collab-* keys, always wrapped in try/catch (use readRaw / writeRaw / readJSON / writeJSON from core/settings.js), never trusted without validation. Add every new key to STORAGE_KEYS there: /privacy lists them. Secrets (derived encryption keys) live in memory only and are dropped on account change or page unload.

Links and paths

  • Build every link with url.* from core/router.js (and ctx.href inside a DAO). tools/lint_web.py refuses hand-written *.html page links and #/ hash routes in modules.
  • Every path in HTML is root-absolute (/assets/…, /js/…, /config.js) or a clean URL: pages are served at any depth (/dao/0x…/proposals/3). The site must be hosted at a domain root.

Licence

  • Every source file we own starts with the licence line: JS and CSS /* MIT License. Copyright (c) 2026 Decentralized Science Labs LLC. */ as the first line, then the description comment; Python the same text as a # comment after the shebang; HTML <!-- MIT License. Copyright (c) 2026 Decentralized Science Labs LLC. --> right after <!doctype html>; Solidity // SPDX-License-Identifier: MIT then // Copyright (c) 2026 Decentralized Science Labs LLC. Vendored files keep their own headers. tools/lint_web.py checks web/ and tools/.

Design and copy

  • Use the design system (see "Design system") and its classes. The look follows genesisl1.com/gov.html: white windows, hairline borders, GenesisL1 blue, status pills.
  • Feature styles live in their area's section of app.css, each marked /* ---- <area> (merged from parts/<area>.css) ---- */ (portal, dex, dao, governance, models, sealed, create, treasury, tokens, admin, core, landing, settings), before the design system's shared rules; the docs pages have a section of their own. Prefix every new class with your area (tr-, tk-, cr-, sl-, md-, dx-, gv-, dc-, dao-, ld-, st-, ad-) and never restyle shared classes there; add a modifier class instead. Theme colours come from the tokens; check dark mode.
  • To style areas in parallel, split one out again as a part, assets/css/parts/<area>.css, imported at the top of app.css with @import url("/assets/css/parts/<area>.css");, and edit only your own part. Before a release python3 tools/merge_css.py folds every part into app.css at its import line (the order stays exactly as tested; a part with @import or a relative url() must be merged by hand) and deletes the parts; tools/build_release.py refuses to pack while imports remain.
  • Cascade order: the area sections (and imported parts) come first, so a shared rule in app.css wins over an area rule of equal specificity. To adjust a shared class inside your area, combine it with your own class (.kv.tr-kv, .item.tr-tok) rather than repeating the shared selector.
  • Copy: plain verbs, sentence case, specific. Buttons say what happens ("Submit proposal", "Reclaim deposit"), and the toast and the history use the same words. Errors say what happened and how to fix it. Empty states invite the next action. Do not use: ALL-CAPS labels, eyebrow labels above headings, meta strings joined with middle dots ("a · b"), spaced em-dash fragments ("Label — fragment"), arrows appended to link text.
  • Responsive down to 360 px wide with no sideways scroll; visible keyboard focus; prefers-reduced-motion respected; tables wrapped in .table-wrap. Numbers use .num (tabular figures).

URLs

Clean paths, each served by one HTML entry (production servers rewrite them; see docs/DEPLOY.md):

pathentrynotes
/index.htmlthe portal's home
/daosdaos.htmldirectory; ?q=&sort=members|treasury|newest&mine=1
/dao/<slug>[/<section>/…]dao.htmlsections below; /dao/<address> also works and leads to the slug URL; prerendered copies serve /dao/<slug>
/votesvotes.htmlopen proposals across DAOs; ?state=voting|deposit|waiting&dao=<slug>
/tokens[/<address>]tokens.htmlevery token on DeSciFi, one token (W-DEX)
/admin[/<tab>]admin.htmlthe operator's admin: deploy, publish, check, site settings, status (W-ADMIN)
/createcreate.htmlwizard; the step as ?step=identity|constitution|members|governance|people|review
/swap[/<in>/<out>]descifi.htmlthe zero address is native L1
/pools, /pools/<pair>, /pools/add[/<a>/<b>/<feeBps>]descifi.htmlpool list, one pool, add liquidity
/locks[/<owner>]descifi.htmllock boxes
/docs, /docs/<slug>, /docs/<slug>.mddocs.html, docs/<slug>/index.htmlthe docs home, one page, its Markdown; generated by tools/build_docs.py
/deploy(301)moved to /admin/deploy in 2.2
/settingssettings.html#network #explorer #currency #appearance #privacy
/privacyprivacy.htmlcookie and privacy policy, #storage lists every key

DAO sections: (overview) proposals, proposals/<id>, propose, propose/<kind>, propose/<kind>/<builder> with ?preset=<base64url JSON>, treasury, members, tokens, liquidity, sealed, sealed/<tokenId>, models, models/<id>, constitution, constitution/v<n>, constitution/compare/<a>/<b>, settings. Addresses in paths are lowercase; the router accepts any case and fixes it with history.replaceState.

core/router.js

  • url builders, the only way to write a link: url.home(), url.daos({q, sort, mine}), url.dao(dao, ...parts) where dao is a DAO object (its slug wins), a slug or a treasury address (url.dao(d, "proposals", 12) or url.dao(d, "proposals/12"); pass the object so the link is the canonical slug URL), daoKey(dao) (the path segment: slug, else lowercase address), url.votes({state, dao}), url.tokens(address?), url.admin(tab?), url.propose(treasury, kind, builder, preset), url.create(step), url.swap(a, b), url.pool(pair) (no pair: the list), url.pools(), url.addLiquidity(a, b, feeBps), url.locks(owner), url.docs(ref) ("" the docs home, "proposals" a page, "proposals#delegation" a section; an anchor of 2.2.0's single docs page, such as "holding-period", leads to where that section is now through DOC_MOVED, which tools/build_docs.py --check verifies), url.settings(anchor), url.deploy() (= /admin/deploy), url.privacy(anchor).
  • route() → {entry, parts, query (URLSearchParams), hash, path} for the current location; parts are the path segments inside the entry (/dao/0x…/proposals/12 → ["0x…", "proposals", "12"], /pools/add/a/b/30 → ["pools", "add", "a", "b", "30"]). ENTRY is this page's entry.
  • onRoute(fn) registers the page's router; fn(route, {action: "push"|"pop"|"replace"}). Once one is registered, same-origin links to this entry are handled without a reload (modifier keys, other buttons, target and download links behave as usual; in-page #anchors scroll natively).
  • navigate(path, {replace}): within this entry pushState (or replaceState) and the page's router renders; any other path is a full load. replaceQuietly(path) / pushQuietly(path) change the address bar without rendering (a filter, a wizard step the page already shows).
  • encodePreset(obj) / decodePreset(str) (base64url UTF-8 JSON, plain objects only, 64 KB at most), daoHref(treasury, path), siteUrl(path) (CFG.siteUrl + path, for canonical links).
  • Version 1 URLs redirect on load: dao.html?dao=X#/proposal/3 and dao.html#/X/new/<kind>/<builder>/<json> to /dao/x/proposals/3 and /dao/x/propose/<kind>/<builder>?preset=…, descifi.html#/pool/P to /pools/p, docs.html#x to /docs#x, index.html?mine=1 to /daos?mine=1.
  • Hosts without rewrites answer clean paths with 404.html, whose /js/fallback.js sends them to /<entry>.html?route=<path>; the router then puts the clean path back with replaceState.

core/settings.js typed settings over localStorage (memory when storage is blocked): get(name), set(name, value) (invalid values throw; the default removes the key), onChange(name, fn) (this tab and others) for theme ("system" | "light" | "dark"), rpc ("" = automatic, or an endpoint URL), explorer ("" = the default, or an https URL) and currency (CURRENCIES: l1 usd eur gbp chf jpy cny krw inr rub uah try pln aed cad aud brl sgd hkd btc eth; CURRENCY_NAMES). setTheme(pref), applyTheme(), effectiveTheme(), isEndpointUrl(url); raw access readRaw, writeRaw, readJSON, writeJSON; STORAGE_KEYS ([{key, what}], shown on /privacy).

keyholds
collab-themeSystem, Light or Dark
collab-rpcthe chosen endpoint (absent: automatic)
collab-explorerthe chosen explorer (absent: the default)
collab-currencythe currency the visitor chose (absent: US dollars from the site's price)
collab-pricethe last L1 price the site published, or fetched for the chosen currency, and its time (reused for 5 minutes)
collab-cookie-infowhen the cookie notice was closed
collab-consent{v: 1, analytics, at} where analytics are configured
collab-walletthe last wallet's EIP-6963 rdns
collab-tokenstokens the visitor added
collab-descifislippage and deadline
collab-create-draftthe creation wizard's draft
collab-sealed-key-version:<chain>:<keyring>:<account>the registered sealed-data key version
collab-deploy:<chainId>:<account>progress of a deployment from /admin/deploy: external addresses, tx hash, nonce and address of each contract

js/theme.js moves the collab.* keys of version 1.0 to these names on the first visit.

core/price.js the L1 price. By default amounts show in US dollars from the site's own /data/price.json ({usd, usd24hChange, at, source}, at most 10 KB, written by the server's prerender; used within 24 hours, and only on the configured site, onSite()), so no visitor's browser contacts a price service by itself. Only when the visitor chooses a currency (settings, the top bar's chip) does the browser fetch it live from CFG.priceApi (CoinGecko simple price), cached 5 minutes; L1 needs no price, and a failure shows no fiat value. money(l1Wei, {maxFrac}) → <span class="money">1,234 L1 <span class="fiat">≈ $217</span></span> that updates itself when the price arrives or the currency changes; fiatOf(l1Wei) → "≈ $217" or null; formatFiat(value, vs), unitPrice() ("1 L1 = $0.0042"), price() ({vs, price, change, at}), sitePrice(), priceSource(q) ("CoinGecko, 14:05"), loadPrice(), onPrice(fn), currency(). chosen("currency") in settings.js tells a chosen US dollar from the default.

core/capped.js bodies of bounded size: readCapped(res, max) (bytes), textCapped, jsonCapped (null when larger) and discard(res). The body is streamed and the read stops at the cap, because Content-Length is the compressed size under gzip_static and absent on a chunked answer. Read every /data/ file and any other bounded answer through it.

js/consent.js (an ES module the shell imports) A compact card at the bottom left of the content column, over nothing that matters and with no overlay or focus trap. Without analytics: a one-time note ("COLLAB keeps your settings in this browser. No tracking or ad cookies.", Cookie policy, OK). With CFG.analytics.googleMeasurementId: the same card asks ("May COLLAB count visits with Google Analytics? …"), "Accept" and "Decline" the same size, nothing requested from Google before Accept, asked again after 6 months (Decline) or 12 (Accept); a browser that sends Global Privacy Control gets a note instead and counts as a Decline. openConsent() (the footer's Cookie settings), consentState() → {enabled, decided, analytics, loaded}.

Network: the settings page offers the CFG.rpcs presets, Automatic (every preset, with failover) and, with CFG.allowCustomRpc, a custom https URL (http only on localhost) that must answer eth_chainId with CFG.chainId and a recent block before it is saved (latency and head block are shown). tools/write_web_config.py --patch-csp writes the CSP that matches: connect-src lists the presets and the price API, plus https: when custom RPCs are allowed; Google's origins appear only with --ga-id. Every entry carries the same CSP.

The portal shell

Every page sits in one frame, after gl1x.com: a slim alpha strip across the top (text from the site settings), a sidebar (brand with the Alpha badge, the live network card with chain, latest block, L1 price and this site's deployment, then the navigation in three groups: Discover, DeSciFi, Build), a top bar over the content (global search, currency, theme, wallet), an announcement slot under it, and the ecosystem footer. Below 1024px the sidebar is a drawer opened from the top bar's menu button (it also holds currency and theme there); below 768px the search folds into a button.

The markup is static in every entry (no layout shift when the module runs; links crawlable), written between the <!-- shell:top … --> and <!-- shell:foot … --> markers by python3 tools/shell_html.py --write; tools/lint_web.py fails on an entry whose shell is missing or stale. Never edit those blocks by hand: change tools/shell_html.py and run it. A new entry gets the shell by running the tool once. The prerender writes copies with the site settings baked in. shell() in ui.js only fills in what is live.

The body is a grid: alpha-strip spans the top row; .side and .topbar span every row below it (that is what lets both stick without a wrapper); <main id="main"> and .site-foot sit in the content column. Pages keep <main id="main" class="…"> as before and lay out inside it: .bands, .band, .frame work as they did. A page never adds a second rail: section navigation inside a page is a bar (.tabs, or .ws-nav, which is a sticky bar under the top bar at every width), and contents of a long document go on the right.

Global search (core/search.js): DAOs by title, slug, share symbol or address, tokens by symbol, name or address (/data/tokens.json on the site, plus wL1), proposals as a DAO and a number ("opc 3"), and the site's pages; "/" or Ctrl/Cmd K focuses it. Hidden DAOs and tokens are left out.

Undeployed mode: CFG.deployed is false until the operator deploys from /admin. The shell shows "Not deployed" in the network card; every page calls deployedOrNotice() and shows its notice instead of reading the chain.

Design system

Tokens are CSS variables on :root, redefined under :root[data-theme="dark"] (the GenesisL1 zinc theme). Use them; never hard-code colours.

  • Surfaces: --bg (page), --bg-2 (bands, table heads, sunken areas), --bg-3 (hover, bar tracks), --surface (windows), --surface-2 (rows nested in windows).
  • Text: --ink, --ink-2 (secondary), --muted (labels, captions), --faint (placeholders, ticks).
  • Lines: --ring (every 1px hairline), --ring-2 (stronger: inputs, hover borders).
  • Blue: --primary, --primary-hover, --primary-soft (selected rows, info notices), --primary-line, --on-primary (text on filled blue). Wallet: --wallet, --wallet-ring, --wallet-ink.
  • Portal: --chrome (sidebar and top bar), --band and --band-line (genesisl1.com's soft blue band: page mastheads, .band-head), --strip-bg --strip-ink --strip-em --strip-link (the alpha strip), --live (a live status dot).
  • Market (DeSciFi): --buy, --buy-soft, --buy-line and --sell, --sell-soft, --sell-line: buying and selling, gains and losses. W-DEX owns their use.
  • DeSci accent: --science, --science-soft, --science-line (data, models, sealed assets, figures).
  • Feedback: --ok, --warn, --danger with -soft and -line. States: --st-<voting|deposit|passed|rejected|failed> with -ink and -line (the gov.html pairs). Votes: --yes, --no, --veto, --abstain. Data palette, in order: --d1 … --d6.
  • Shapes: --r-window 14px (windows, popovers, modals), --r-row 12px (rows in windows, notices), --r-ctl 10px (buttons, inputs, selects), --r-chip 6px (code), --r-pill (pills, badges). Windows have no shadow; popovers, menus and modals use --shadow-pop.
  • Type: --sans Inter (interface), --serif Source Serif 4 (documents), --mono JetBrains Mono (addresses, hashes, block numbers, numeric table cells). Sizes --fs-12 --fs-13 --fs-14 --fs-15 (base) --fs-17 --fs-20 --fs-24 --fs-30 --fs-38 --fs-48, and --fs-display (clamp(40px, 4.2vw, 64px), page heroes; class .display). Headings 600–650, tight tracking.
  • Layout: --max 1600px (the content frame, inside the content column), --gutter clamp(16px, 2.6vw, 40px), --topbar 64px, --side 256px (the sidebar; 0 below 1024px, where it is a drawer), --aside 360px. Sticky things inside a page stick at top: var(--topbar).

Which class for which purpose:

purposeclasses
page frame.frame (alias .shell): --max wide with gutters; .page adds vertical padding
full-width sections<main class="bands"> with <section class="band"> (.alt on --bg-2, .band-head for a page masthead) each holding a .frame
page header.crumbs (breadcrumb <nav> with an <ol>), .page-head with .page-head-text (h1 + .lede) and .page-head-actions
stacks and rows.stack .stack-sm .stack-lg, .row .row-tight (+ .between .end .grow)
grids.grid + .g2 .g3 .g4 (step down at 960/640px), .grid-cards (cards, at least 300px), .grid-auto (at least 240px)
main + side.split (360px side), .split-wide (420px), .sticky side; inside the DAO workspace they stack when the main column is under 900px (container query)
DAO workspace.ws with .ws-rail (display: contents: #dao-tabs.ws-nav, the section bar, and #dao-me.ws-me), .ws-main, #dao-aside.ws-aside (dao.js builds it); the section bar runs across the top at every width, the aside sits beside the main column from 1440px
undeployedundeployedNotice(what) (.undeployed), via deployedOrNotice()
window.card with .card-head (h2/h3 15px + actions), .card-body, .card-foot; .pad .pad-lg .pad-sm
rows.list (hairlines between children) of .item (links get a hover); .row-card (a bordered row); .sunken (a recessed area)
buttons.btn (secondary, white with a ring), .btn.primary (one per view), .btn.wallet, .btn.danger, .btn.ghost; sizes .sm .lg, .block; .icon-btn (36px square), .link-btn (a button that reads as a link)
form controlsfield() (.field with label, hint, .err), .input (.mono for hex), .select, .textarea (.prose, .code), .input-group + .addon, .check, toggle() (.switch), segmented() (.seg)
labels.pill (neutral facts: kind, symbol, link), .badge + state (voting deposit passed executed rejected vetoed failed expired queued executable critical science frozen), stateBadge(), kindBadge(), .chip (a small rounded button, e.g. currency), .code-chip (inline code)
navigation.tabs (a section bar: links with aria-current="page", .count), .ws-nav (the DAO rail), .crumbs
datatable.t in .table-wrap (th muted 12px; td.num mono; .r right-aligned; .t-compact for side panels), .kv (definition grid), .stat/.stats, .money + .fiat, .addr (addr()), .avatar.s24/s40/s48/s64/s88
figuresfigure.fig + figcaption.fig-cap (numbered "Figure N." by CSS), .chart SVG classes axis tick tick-label grid ref ref-label guide series marker floor-*, .legend, .progress (9px bar on --bg-3, fill in a <span>), .mini-ballot, .ballot
documents.doc (18px serif, 70ch; markdown output), .doc.compact (16px), .doc-note (margin notes such as "Version 3, adopted by proposal #12 on 22 Oct 2026"), .diff
feedbacknotice(kind, title, body) (.notice.info/.warn/.danger/.ok), toast(), modal(), confirmDialog(), popover() (.pop), skeleton() (.skel), empty(), errorBox(), .spinner
wizards.steps (numbered step buttons), .drop (file drop zone), .kinds / .kind (choice cards)

Motion: none except short (150–200 ms) responses to actions (opening, expanding, toggling) and the landing hero's single assembly; everything honours prefers-reduced-motion.

Head template for a new page

Copy this into web/<page>.html, then add the page to core/router.js (entry list and a url builder), to tools/serve_local.py (rewrites), tools/lint_web.py (ENTRIES), the server configs in deploy/ and the sitemap. tools/write_web_config.py --patch-csp keeps the CSP in step.

<!doctype html>
<!-- MIT License. Copyright (c) 2026 Decentralized Science Labs LLC. -->
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta http-equiv="Content-Security-Policy" content="(copy it from index.html)">
<meta name="referrer" content="strict-origin-when-cross-origin">
<title>Page name | COLLAB on GenesisL1</title>
<meta name="description" content="What the page is and what a visitor can do there, in one or two sentences.">
<link rel="canonical" href="https://collab.genesisl1.com/<clean-path>">
<meta property="og:type" content="website">
<meta property="og:site_name" content="COLLAB">
<meta property="og:title" content="Page name | COLLAB on GenesisL1">
<meta property="og:description" content="…">
<meta property="og:url" content="https://collab.genesisl1.com/<clean-path>">
<meta property="og:image" content="https://collab.genesisl1.com/assets/img/og.png">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:image:alt" content="COLLAB: scientific DAOs on GenesisL1, with molecule glyphs of DAOs">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Page name | COLLAB on GenesisL1">
<meta name="twitter:description" content="…">
<meta name="twitter:image" content="https://collab.genesisl1.com/assets/img/og.png">
<meta name="twitter:image:alt" content="COLLAB: scientific DAOs on GenesisL1, with molecule glyphs of DAOs">
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#09090b" media="(prefers-color-scheme: dark)">
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
<link rel="icon" href="/assets/img/icon-32.png" type="image/png" sizes="32x32">
<link rel="icon" href="/assets/img/icon-any-192.png" type="image/png" sizes="192x192">
<link rel="apple-touch-icon" href="/assets/img/apple-touch-icon.png">
<link rel="manifest" href="/manifest.webmanifest">
<link rel="preload" href="/assets/fonts/inter.woff2" as="font" type="font/woff2" crossorigin>
<link rel="stylesheet" href="/assets/css/app.css">
<script src="/js/theme.js"></script>
<script src="/config.js" defer></script>
<!-- preload (tools/shell_html.py writes this block: the page module's static imports) -->
<!-- /preload -->
<script type="module" src="/js/pages/<page>.js"></script>
<script type="application/ld+json">{"@context":"https://schema.org","@type":"WebPage","name":"Page name","url":"https://collab.genesisl1.com/<clean-path>","isPartOf":{"@type":"WebSite","name":"COLLAB","url":"https://collab.genesisl1.com/"}}</script>
</head>
<body>
<main id="main" class="frame page">
  <nav class="crumbs" aria-label="Breadcrumb"><ol><li><a href="/">COLLAB</a></li><li><span aria-current="page">Page name</span></li></ol></nav>
  <header class="page-head"><div class="page-head-text"><h1>Page name</h1><p class="lede">…</p></div></header>
  …
</main>
</body>
</html>

Then run python3 tools/shell_html.py --write to give it the shell markup and its preload block. The page module starts with shell("<nav key or empty>") and, when it reads the chain, if (deployedOrNotice("…")) …. Preload Source Serif 4 (/assets/fonts/source-serif-400.woff2) on pages that show documents. One h1 per page.

Core modules (js/core)

env.js CFG (validated config: deployed (false before the operator deploys the contracts: then addresses is {}, the integrations are empty and nothing may read a COLLAB contract), site (the public site settings the admin edits, normalized: alpha {show, text}, announcement {text, href, tone} | null, featuredDaos and hiddenDaos (slugs or lowercase addresses), verifiedTokens and hiddenTokens (lowercase addresses), seo {title, description}, links {website, x, telegram, discord, github}), chainId, chainName, nativeSymbol, rpcs (presets [{name, url}], failover order), rpcUrls (their URLs), allowCustomRpc, explorers (presets [{name, url}]), explorer (the default), priceApi (URL with {vs}, "" = prices off), analytics ({googleMeasurementId} or {}), siteUrl, startBlock, version, addresses.{factory, daoLens, marketLens, textStore, keyring, desciFactory, desciRouter, lockFactory, distributor, launcher, wl1} (nothing else), and the integrations this deployment lists, from config.js integrations: gl1f ([{id, name, site, registry, nft, market, runtime, store}]) and cipnft ([{id, name, site, address}]); ids are lowercase letters, digits and hyphens ("x-" is reserved), names plain and at most 40 characters and passing names.js, sites https, every address distinct and non-zero, at most 8 of each), ZERO, BURN (0x…dEaD), MAX_UINT64, explorerBase() (the visitor's explorer, else the default), explorerTx(hash), explorerAddr(addr) ("" without an explorer); daoHidden(dao), tokenHidden(address), tokenVerified(address) (the site settings: hidden means left out of listings, search and the sitemap on this site, the page stays reachable by URL with a notice; nothing changes on chain), isSlug(s), onSite() (this page is on CFG.siteUrl, the server that publishes /data/ and /og/: fetch those only when it is true).

names.js The rule for those names, shared with the deploy page and mirrored in tools/write_web_config.py (same tables and messages): nameProblem(name) ("" or the rest of a sentence after the entry): NFKC form; letters all Latin (A to Z and the Latin-1 letters only) or all CJK, with digits, spaces and common punctuation (no Greek, Cyrillic, Latin Extended or other letters with Latin look-alikes); nameKey(name), the look-alike skeleton names are compared by (trimmed, NFKC, lowercase, then 1/i/l/| and 0/o folded, rn read as m and vv as w: "GLIF" and "GL1F" are one); skeleton(text) the same for free text in any script (DAO titles): Cyrillic, Greek and Armenian letters that pass for Latin ones become them first (not part of the mirrored rule); nameSpace() → {problem(kind, name), add(kind, name, what)}, checked GL1F first: no two collections share a name, and no CIPNFT contract takes another one's name or a collection's address-book label ("GL1F market"). The deploy page's chain 29 comparison with Deploy.s.sol's defaults matches names by nameKey too.

dom.js h(tag, attrs, ...children) (attrs: class, style object, dataset, on<event> functions, text, booleans; children: nodes, strings, arrays, null), s() for SVG, frag, clear, mount(el, ...children), $, $$, icon(name), safeUrl, debounce, onVisible(el, fn).

fmt.js units(v, decimals=18, maxFrac=4), compact(v, decimals), l1(v, maxFrac), parseUnits(text, decimals) (throws readable errors; accepts digits with an optional dot, and commas, spaces or underscores only between groups of three digits: "12,500.5" is 12500.5, while "1,5" is refused with "Use a dot for decimals"), tryParseUnits (null instead of throwing), bps(n) → "33.4%", pct(part, whole), ratio, shortAddr, shortHash, duration(sec), relTime(unix, now), date(unix), day(unix), int(n), bytesLabel(n), excerpt(md, max).

bus.js state (account, walletChainId, block, blockTime), on(evt, fn), emit. Events: account (connect, disconnect, switch, chain change), block, rpc, tx.

rpc.js Reads go to the visitor's endpoint (settings rpc) alone, or to every preset in order with failover; a change in settings rebuilds the providers at once. endpoints(), activeRpc(), allowedEndpoint(url), checkEndpoint(url) → {chainId, block, timestamp, ms} (rejects with a readable message unless the endpoint serves this chain with a recent block), provider(), withProvider(fn) (failover), cached(key, ttlMs, fn), invalidate(prefix), refreshBlock(), blockReady() (await it before using chain time for a decision), waitForBlock(n, timeoutMs) (polls until the read RPC has block n), chainNow() (chain time in seconds; use it for every "ends in" computation), getLogs(filter, {fromBlock, toBlock, step, limit, newestFirst=true}): ranges of at most LOG_RANGE (10,000) blocks, 4 requests in parallel, the newest range ending at "latest" unless you pass toBlock; the order is deterministic (newest first by default, block and log index), and with limit the scan stops once the ranges nearest the start of that order hold enough logs. Pass fromBlock: dao.createdBlock for DAO contracts. getBlockLogs(filter, block), getCode, getBalance.

contracts.js iface(name) (the shared LenientInterface of a shipped ABI), contract(name, address, runner?), lenientInterface(abi) (for ABIs a module writes itself), read.<singleton>() handles (factory, daoLens, marketLens, textStore, keyring, desciFactory, router, lockFactory, distributor, launcher, wl1), daoContracts(dao) → {treasury, governor, shares, profile, policy, vault}, gl1fContracts(collection, runner?) → {registry, nft, market, runtime} and cipnftContract(entryOrAddress, runner?) for integrations (pass the entry from integrationsOf(dao): a handle built from an unlisted entry of a DAO carries collabIntegration, and send() then checks through the wallet's own network that the DAO's policy gives that address that role, as it checks daoContracts tags). The ModelRegistry and ModelMarketplace interfaces include GL1F Crypto's functions (changeModelLicense, setInternalsVisibility, internalsVisibility, licenseOf, getLicenseInfo, burnFees, pendingOwner…), so one ABI serves both GL1F families; calling one on an original GL1F contract reverts. explainError(e) → {title, detail} (plain title; for custom errors detail is the error tag such as NotAToken(0x…), for string reverts the reason code). signatureOf(selector) → "transfer(address,uint256)" or null and functionLabel(selector) → "transfer" (the full signature when the name is overloaded), over every shipped ABI; selectorIndex() → Map(selector → [{name, fragment}]). ABI names: DAOFactory DAOGovernor DAOTreasury ShareToken DAOPolicy DAOProfile CipherVault TextStore CollabKeyring DAOToken TokenLauncher MemberDistributor DeSciFiFactory DeSciFiPair DeSciFiRouter LockFactory LockBox DAOLens MarketLens PolicyBuilder CIPNFT ModelRegistry ModelNFT ModelMarketplace ForestRuntime WL1 ERC20 ERC721 ERC1155. Full signatures: docs/CONTRACT_API.md.

wallet.js connect(), restore(), disconnect(), ensureChain(), signer(), send({contract, method, args, value, label, gasBuffer=1.3, gasLimit, quiet, verifyDAO}) → receipt or null (the user already saw why). gasLimit (BigInt) is for transactions heavier than nodes estimate (eth_estimateGas stops at the json-rpc gas cap, 25,000,000 by default), such as a large logo: the preflight still runs and a revert still stops the transaction; only an estimate that failed at the cap, without a revert, sends with this limit. Before the wallet opens, a DAO contract from daoContracts() is read again from the DAO factory through the wallet's own network, and a handle of an unlisted integration (gl1fContracts() / cipnftContract() of an entry a DAO added itself) is checked against that DAO's policy (integrationRole) the same way; a mismatch sends nothing. It resolves once the read RPC has the receipt's block and the caches are cleared, so a view rendered right after it sees the transaction's state and events. Starting a transaction closes the confirmation and error toasts of earlier ones. sendValue({to, value, label}) (the same), signMessage(text), signTypedData(domain, types, value).

notify.js toast({title, body, kind: "ok"|"err"|"pending", link, timeout}) → {update, close}, modal({title, body, actions: [{label, kind, onClick}], wide, onClose}) (return true from onClick to keep it open), confirmDialog({title, body, confirm, danger}) → Promise<boolean>.

ui.js shell(activeNavKey) (call once per page: wires the portal shell, see below, then block polling and wallet restore; keys home daos votes tokens swap pools locks create docs, "" for none, or 2.1's descifi (the path picks swap, pools or locks); returns {deployed}), deployedOrNotice(what, target) (true when COLLAB is deployed on this site; otherwise it puts undeployedNotice(what) in target, by default replacing the page's main, and returns false: call it right after shell() and skip every chain read when it is false), undeployedNotice(what), popover(anchor, build, {label, align, cls}) (a small window under a button: closes on Escape and outside clicks, returns focus), copyText, addr(a, {full, label, link}), stateBadge(state), kindBadge(kind, critical), stat(label, value, unit), notice(kind: "info"|"warn"|"danger"|"ok", title, body), empty(title, body, action), skeleton(lines), errorBox(e, retry), field(label, control, hint, errEl) (returns a .field element: its <label> holds only the label text, and the hint and errEl sit under the control, inside the returned element, linked with aria-describedby, so they are not part of the control's accessible name; a control that is not an input, select or textarea is named as a group; an .sr-only label node stays visually hidden), input(attrs) (mono: true for hex), select(options, value), textarea(attrs) (variant: "code"|"prose"), toggle(label, checked, onChange) (el.set(v) sets it quietly), segmented(options, value, onChange) (options {value, label, disabled, reason}: a disabled option shows reason as its title; choosing another option calls onChange and then dispatches a bubbling change event; el.set(v) changes the choice without either), section(title, body, actions) (a card), pager(page, pages, onPage).

forms.js Validating fields. Each returns {el, read(), peek(), fail(msg)}: read() marks errors in place and returns the parsed value or null (a valid read clears the mark); peek() returns what read() would, without marking anything (for live summaries); fail(msg) shows msg in the field's error slot, for checks that span fields (duplicates, totals), and returns null; fail("") clears it. Most fields also expose input (their control) and set(value).

  • textField({label, value, hint, maxBytes, required, pattern: [RegExp, message], mono})
  • addressField({suggestions, allowZero, required}) (+ suggest(list)) → checksummed address.
  • amountField({decimals, symbol, max, min, allowZero, required}) (+ setToken, setMax, setLabel(text), isEmpty(): with required: false, read() is null both for an empty and an invalid amount).
  • tokenField({dao, owner, allowNative, value, extra, hint, onChange, held}): L1 is ZERO with native: true. Offers the DAO's tokens, wL1, tracked tokens and extra, and with dao (unless held: false) the tokens its treasury holds, added in the background from holdings(). get() / peek() give the loaded token (while a newer choice loads, still the previous one); async read() waits for the choice the select shows, so it always returns that token; async set(address); ready (initial load); select and other (the address input). A custom hint shows under the balance line.
  • durationField({seconds, min, max, allowZero}) → seconds; zero is allowed when min is 0 or with allowZero (a zero timelock is valid). set(seconds), input, unit (the unit select).
  • bpsField({bps, min, max}): percent in ("33.4" or "33.4%"), basis points out. set(bps), input.
  • dateField({unix, allowPast, required}) → unix seconds. "In the future" is checked against chainNow(); ready resolves once the chain head is known, so await it before read() where that decision matters. A value given as unix or through set(unix) reads back to the exact second while untouched (the control itself has minute precision). input.
  • markdownField (Write / Preview, byte counter; textarea, also as input).
  • fileField({accept, maxBytes, hint, onChange}) → {bytes, mime, name}. onChange(file | null) runs after a pick or a drop (a drop also dispatches a bubbling change, as picking does); set({bytes, mime, name}) shows an existing file as the current one; clear(); input.
  • listField({label, make, min, max, addLabel, initial, extra}): repeating rows (extra: more buttons shown beside Add, such as "Add members"), make(preset) → {el, read(), peek()?}. add(preset) → the row, rows() (current rows in order, for checks across rows), count(), clear() (re-enables Add). Adding or removing a row dispatches a bubbling change from el. Rows are .row.list-row: they never wrap, so the remove button stays beside a tall row.
  • maskField({options, mask}) (bit masks; set(mask)).

daodata.js getDAOs(offset, limit), isDAO(treasury), getDAO(key, ttlMs=10000) (key: the treasury address or the slug; an unknown slug rejects with "not a DAO"; cached; normalized summary + details: treasury governor shares profile policy vault creator createdAt createdBlock title name slug logoHash logoLength shareName shareSymbol totalSupply totalUnits memberCount treasuryL1 treasuryShares proposalCount constitutionVersion transfersEnabled descriptionHash descriptionLength links constitutionPtr/Hash/Timestamp/Proposal deposit{minDeposit, proposalThreshold, maxDepositPeriod, executionWindow, minInitialDepositBps, flags} standardRules criticalRules{votingPeriod, executionDelay, quorumBps, thresholdBps, vetoThresholdBps, minYesBps} reps repInfo[{perms, termEnd, since}] stewards activeEpoch epochCount announcementCount criticalKinds integrations holdingBlocks everTransferable; slug is the DAO's permanent URL name, logoHash/descriptionHash (lowercase, the zero hash when empty) and the lengths describe the bytes its profile stores (read them with core/daoprofile.js; summaries from getDAOs carry the logo but not the description); daoIdBySlug(slug) (0 = none), slugStatus(slug) → {available, taken}, treasuryOfSlug(slug) (no chain read; from DAOs already loaded); sameNameAs (on objects from getDAO and allDAOs) lists the slugs of the other DAOs whose title looks the same, oldest first: [] when none, null when the directory could not be read in time. Titles are not unique, slugs are: wherever a DAO vouches for something, show its slug and flag a shared name. nameKey(title) is the comparison key (cleanText, then skeleton() of names.js: look-alike letters of other scripts, compatibility forms, case, 1/l/I and 0/O), nameIndex() all DAOs by key (DAOLens.directory, 200 a call; read once per session, in memory and sessionStorage by chain and factory; after 5 minutes the DAO count is read again and only newer DAOs are fetched), sameNameAs(dao) waits for it, markSameNames(list, complete) sets the field on a list; minYesBps (also on every proposal's rules) is the least Yes, as a share of all voting power at the snapshot, that passes (0 = off, also when the chain has none); integrations is {gl1f: [{registry, nft, market, runtime}], cipnft: [{address, daoKey}], source: "lens" | "defaults"} from DAOLens (daoKey: the DAO's X25519 key registered on that contract, zero when none), or the deployment's defaults when the lens has none: read it through integrationsOf(dao) (integrations.js), never directly; holdingBlocks is the governor's holding period in blocks, null when unreadable; everTransferable is true once the shares have ever been transferable (ShareToken v2 never clears it; a v1 token reports its current setting), null when unreadable. Equal per-member airdrops are allowed only when it is exactly false; check again at submit time with readEverTransferable(dao.shares), which is never cached; title is the stored title for forms, name = daoName(title) the one to show: cleanText(title, 80), "Untitled DAO" when nothing visible is left; shareName and shareSymbol come cleaned), getProposals(dao, fromId, count) (rows carry holdSnapshot only when the deployed DAOLens returns it), getProposal(dao, id) (tally{yes, abstain, no, veto}, totalPower, snapshot, holdSnapshot, flags (the DAO's deposit flags when it was submitted, as the governor applies them), my* fields for the connected account), proposalSnapshot(dao, id) → {snapshot, holdSnapshot} (the two blocks votes are read at; each vote counts the lower of the two; equal without a holding period), getMembers(dao, accounts) (balance units votes delegate frozen lockUntil isRep repPerms repTermEnd isSteward keyringX25519 keyringPqcHash l1Balance, plus excluded when the deployed DAOLens returns it), memberAccounts(dao) (every account that held voting units or was frozen, oldest first, without the treasury and without accounts the DAO excluded as infrastructure), plain(result). The fields DAOLens v2 appends (holdingBlocks, holdSnapshot, excluded) are used only after a one-time probe shows the deployed lens has them; otherwise the governor and shares are read.

tokens.js NATIVE, tokenInfo(tokens, owner, spender) → Map(lowercase address → {token, ok, name, symbol, decimals, totalSupply, balance, allowance, launchedBy}), tokenMeta(tokens) (cached), trackedTokens(), trackToken(addr), baseTokens().

holdings.js holdings(dao) → Promise of the ERC-20 tokens the treasury holds now, sorted by symbol: {token, symbol, name, decimals, balance, totalSupply, launchedBy, daoToken, shares, pool, wl1} (shares: the DAO's own share token; pool: a DeSciFi LP token). Leads are Transfer logs to the treasury since dao.createdBlock (through getLogs: ranges of at most 10k blocks, a few at a time) plus wL1, DAO-launched and tracked tokens; every balance, name and symbol is then read from the chain (cleanText applied). Cached 30 s per treasury and never rejects: an RPC that refuses the log query only loses the leads. receivedTokens(treasury, fromBlock) → addresses that sent the treasury ERC-20 transfers ([] when refused).

daoprofile.js A DAO's logo and description (2.2): stored in its DAOProfile and overwritten in place on every edit (the title never changes). Reads go to the site's static cache first (/data/logos/<hash>.png, /data/texts/<hash>.txt, 64 lowercase hex without 0x, written by the prerender; fetched only when onSite()), then to the profile in 16 KB chunks (logoChunk, descriptionChunk); the bytes must hash to what the chain reports, and results are cached by hash. daoLogoUrl(dao) → a verified data:image/png URL or null, logoUrl({hash, profile, length}), logoBytes(…), daoDescription(dao) → the verified text ("" when none; throws when unreadable), descriptionText({hash, profile, length}), hasLogo(dao), hasDescription(dao), rememberBytes(hash, profile, length) (daodata.js calls it, so a logo can be drawn from its hash alone). Preparing writes: decodeImage(file) (any image the browser opens, as a canvas), drawSquare(canvas, {x, y, size}) (halving first, so detail survives), encodeLogo(canvas) (the smallest of a lossless palette PNG, a filtered truecolour PNG and the browser's own), pngInfo(bytes) / logoProblem(bytes) (the contract's Png.header checks, in its order), descriptionBytes(text) → {text, bytes, removed, over} (LF line breaks, other control characters removed). Gas, from the measured formula in spec 2.2 As built (constants in GAS and MARGIN): storeGas, procGas, createGasLimit(emptyEstimate, payloads), createGasUsed, executeGasLimit(actions, writes), submitGasLimit(actions, noteBytes), directGasLimit(bytes, old, {text}), profileWrites(dao, actions) (the logo and description writes in a batch), beyondEstimate(gas), failureFee(limit, price) and failureLine(limit, cause) (on GenesisL1 a failed transaction still pays half its gas limit: every transaction sent with COLLAB's own limit says so), gasPrice(). Limits: MAX_LOGO_BYTES 524,288, MAX_DESCRIPTION_BYTES 262,144, MAX_CHUNK_BYTES, LOGO_PIXELS 128.

blob.js readBlob(ptr, hash), readText(ptr, hash) (refuses on hash mismatch), readImageUrl(ptr, hash, mime), writeBlob(bytesOrText, {label, onProgress}) → {ptr, hash} (deduplicated; large blobs upload in chunks then commit).

actions.js proposalActions(governor, id, createdBlock), actionsHash(actions), simulate(dao, actions, proposal?): with proposal (getProposal()'s object) in state Executable, an eth_call of governor.execute(id, actions), exactly the transaction anyone may send, → {ok, error?}; otherwise treasury.executeBatch(actions) from the governor, without the governor's executingProposal context, → {ok, approx: true, note, error?} (note is a plain sentence to show as it is: airdrops count the previous block's members, constitution versions and key vault records keep no proposal); error is explainError()'s {title, detail}. addressBook(dao), actionMeta(actions, {dao}) → the meta Map for describe() (never throws): token metadata for every address in the targets and the decoded arguments, nested arrays and tuples included; pools (router paths, pairs, LP tokens) with their tokens from MarketLens.poolsInfo; LP lock boxes; the address a launch will create; blob sizes; Model NFT numbers per registry; GL1F Crypto licences; the integrations the batch adds. describe(action, {dao, meta, book}) → {title, lines, warn, hint} (irreversible effects are in warn; without meta addresses are shown instead of symbols). A party (recipient, spender, owner, fee recipient, module) is named only from addressBook(dao) (protocol contracts, the integrations this deployment lists by their config.js names such as "GL1F Crypto market", this DAO's contracts, the burn and zero addresses), otherwise by its checksummed short address: a contract's own symbol never names it, and an integration a DAO added itself is shown by its address with a warning. Model NFTs are named with their collection ("GL1F Crypto Model NFT #3"), CIPNFT assets with their contract ("CIPNFT #2"). Where treasury spends are critical, a GL1F pricing, access-plan or owner-key call on a registry the DAO lists carries a line saying why it is critical (RV-1); whether a proposal is critical still comes from policy.validate(). Symbols name only the token an action moves. Transfers always show the recipient's address in the title, and hint holds the full addresses: render it as the line's title attribute (h("span", {title: d.hint || null}, d.title)). decodeAction(action, {dao, meta}) → {contract, fn, signature, args} or null, and signatureOf / functionLabel (from contracts.js). When you add a builder for a function describe() does not know, the review shows a generic decoding; tell the core team which plain-language line it deserves.

charts.js Charts are figures: hairline axes, mono ticks, dashed reference lines with labels and a caption numbered by CSS ("Figure 2. …"). ballot(tally, totalPower, rules, {decimals, symbol, caption}) (a figure.fig: votes against all voting power with the quorum line, the four options, and turnout, Yes share and veto share each against its threshold, plus "Yes of all voting power" against rules.minYesBps when it is above 0), miniBallot, donut(segments, {size, thickness, center, caption}), poolCurve({eBase, eQuote, floorQuote, label, width, height}) (an <svg> with ticks, guides to the current point and the launch floor as a dashed line), allocationBar(parts, total) (its bar is role="img", named after the parts and their shares), figure(node, caption) (wraps any chart in a numbered figure). glyph.js glyph(seed, size) (atoms in the data palette, hairline bonds; tools/seo/glyph.py ports it for Open Graph images: change both together), avatar(seed, {logoPtr, logoHash, logoMime, logoLength, profile, size, label}) (a token's TextStore image by logoPtr; a DAO's logo by logoHash), daoAvatar(dao, {size, label}) (the one to use for a DAO object: its verified logo, else its glyph). The draws of the generator, in order: motif, rotation, the motif's own (ring size and one per bond, or chain length and one per bond after the first; none for the two rings), substituent count and one atom index each, hue, five for the palette (Fisher-Yates: for i = 5 down to 1, swap i with floor(R() * (i + 1))), then one per atom for its colour. directory.js allDAOs(max) → {list, total}, memberOf(list, account) → Set of lowercased treasuries or null, openProposals(dao) (open ones among the newest 20, counted again after 60 s, OPEN_TTL_MS, or when the proposal count changes), daoCard(dao, {member}) (the directory card; the whole card is the link; a shared name shows its slug and a "Name shared" badge), sameNameBadge(dao) (that badge, or null). kinds.js KIND, KINDS (id key name icon critical desc), STATE, STATES, OPTIONS, FLAGS (BURN_VOTE_QUORUM 1, BURN_DEPOSIT_PREVOTE 2, BURN_VOTE_VETO 4, PENALTY_TO_TREASURY 8, FREEZE_LOCK 16, FREEZE_BOND 32: a freeze proposal that ends Rejected or No quorum forfeits its deposit), REP_PERMS, TOKEN_FLAGS, INTEGRATION_ROLE, BLOCK_SECONDS (14, the average block time; use it for every "blocks, about N hours" estimate), kindOf, stateOf, isLive.

integrations.js The GL1F collections and CIPNFT contracts a DAO works with. Each DAO keeps its own lists on chain (DAOPolicy, changed only by its critical votes); the deployment's defaults in config.js are "listed". A contract a DAO added itself is unverified input, like a pasted token address: never named after what it says about itself.

  • integrationsOf(dao) → {gl1f: Collection[], cipnft: Cipnft[]} in the DAO's order (without a DAO: the listed ones). Collection = {registry, nft, market, runtime, store, id, name, site, listed, key, used}: listed when the four addresses equal one of CFG.gl1f exactly (id, name, site and store then come from it); otherwise id x-<registry 6 hex>, name "Unlisted GL1F collection", site "" and store null; key is the lowercase registry; used is true for the DAO's own entries. Cipnft = {address, id, name, site, listed, daoKey, key, used} ("Unlisted CIPNFT contract"; daoKey the DAO's registered key there, null when unknown). Entries are frozen.
  • collectionOf(dao, address) (by any of its five addresses) and cipnftOf(dao, address): the DAO's own entry first, else a listed one (used: false), else null. integrationName(dao, address) → "GL1F Crypto market", "CIPNFT" or null.
  • knownCollections(), knownCipnfts(), defaultIntegrations().
  • checkCollection({registry, nft, market, runtime}, dao?) and checkCipnft(address, dao?) → {ok, problems: [plain sentences]}: what DAOPolicy checks when the DAO adds one (valid, distinct, non-zero addresses with code, not COLLAB's or the DAO's own, not already integrated there, registry.modelNFT() = nft, market.nft() = nft, runtime.registry() = registry, nft.registry() = registry; a CIPNFT contract must answer like one).
  • collectionFeatures(c) → {family: "gl1f" | "gl1f-crypto" | null, changeModelLicense, setInternalsVisibility, adminBurnAndDelete, twoStepOwner} from the registry's code (for offering builders and for display only).
  • externalPowers(entry) → Promise of [{role, contract, holder, pending, can: [], cannot: [], reviewed}]: one item per outside admin key (GL1F: "Registry owner", "Market owner"; CIPNFT: "CIPNFT admin") with the live holder from owner()/admin() (pending: GL1F Crypto's two-step owner) and what the key can and cannot do to the DAO's assets, in plain sentences. An unlisted contract gets reviewed: false items for every owner()/admin() it answers: its powers are unknown.
  • ROLE (CIPNFT 1, GL1F_REGISTRY 2, GL1F_NFT 3, GL1F_MARKET 4, GL1F_RUNTIME 5), ROLE_NAMES, GL1F_ROLES, roleTarget(role) (DAOPolicy's rule key for every contract of a role, 0x…0100 + role), MAX_GL1F, MAX_CIPNFT (8 each).

DAO views

/dao/<slug>/<section>/<args…> is routed by js/pages/dao.js, which renders the workspace (section bar, breadcrumbs, aside) and calls the view module's export async function render(ctx) with the context below. It also writes the head as tools/prerender.py does for a DAO (title, canonical, robots, Open Graph and Twitter tags, and one JSON-LD block script[data-dao] with the Organization and its BreadcrumbList); on a prerendered page it keeps that page's description and image URLs.

ctx.dao          normalized getDAO() result (refetched on navigation once older than 4 s)
ctx.member       getMembers() row for the connected account, or null
ctx.root         element to render into (already cleared; this render's own container)
ctx.args         path segments after the section (URI-decoded): proposals/12 → ["12"],
                 propose/treasury/send → ["treasury", "send"], sealed/5 → ["5"],
                 models/1 → ["1"], constitution/v3 → ["v3"], constitution/compare/1/2
ctx.preset       the composer's prefill (decoded ?preset=), or null
ctx.aside        this render's aside container: what a view puts there shows as the right
                 column at 1440 px and wider and under the main column below that. A view with
                 its own .split / .split-wide side column and an empty aside gets no aside; any
                 other view gets the DAO's decision rules there.
ctx.href(path)   link inside this DAO, e.g. ctx.href("proposals/4") → /dao/<slug>/proposals/4
                 (version 1 names such as "proposal/4" and "new/<kind>" still work)
ctx.go(path)     navigate inside this DAO (no page load)
ctx.proposeHref(kindKey, builderKey, preset)   link to the composer, prefilled (see below)
ctx.refresh()    reload DAO data, header and the current view (call after a transaction); does
                 nothing once ctx.stale() is true
ctx.stale()      true once a newer render (a navigation or a refresh) has started
ctx.delegateDialog(dao, presetAddress)
ctx.setCrumb(label)   ends the breadcrumb with the item shown (DAOs / <DAO> / <Section> / <label>);
                 cleaned with cleanText(…, 120); reset on every navigation
ctx.setHead({title, description})   for the item shown: `title` replaces the section name in
                 the document title ("<title> | <DAO> | COLLAB on GenesisL1") and in og:title and
                 twitter:title; `description` (plain text or Markdown, cut to 155 characters and
                 cleaned) replaces the DAO's description, og:description and twitter:description.
                 Both last until the reader leaves the page; the canonical URL, og:url, images and
                 JSON-LD stay the DAO's
ctx.title(text)  shorthand for ctx.setHead({title: text})

setCrumb, setHead and title do nothing once the render is stale. A refresh renders the view into a hidden container and swaps it in when it is done, so the page never shows half a render. A view marks its own side column with [data-side]; the frame then leaves the aside for it.

The frame's markup (dao.html and pages/dao.js): the masthead #dao-mast holds #dao-crumbs and #dao-header.dao-id (the DAO's mark, name, summary, pills, key facts and actions); the workspace #dao-ws holds the rail (#dao-tabs, #dao-me), .ws-main and #dao-aside. On #dao-ws, data-view names the view, data-aside is on or off, data-me says whether the membership box sits in the rail or the aside, and data-aside-fit is fits or tall (a tall aside does not stick).

A view renders immediately with skeletons, loads with Promise.all where possible, shows errorBox(e, retry) on failure, and works without a wallet (read-only), with a wallet that is not a member, and for frozen members. It re-renders through ctx.refresh(); it does not keep timers. After a long await, check ctx.stale() before doing more work for a page the user has left.

DeSciFi and the tokens pages

descifi.html (/swap, /pools, /locks; js/pages/descifi.js) and tokens.html (/tokens, /tokens/<address>; js/pages/tokens.js) own their masthead and route by path. Each view in js/descifi/ exports render(ctx): ctx.root (render into it), ctx.args (path segments), ctx.head({h1, icon, lede, crumbs, actions, title, description}) (the masthead and the document head), ctx.scope (alive, and on(evt, fn) subscriptions that end when the reader navigates) and ctx.refresh(). Both pages call deployedOrNotice() before reading a contract.

  • Data: js/descifi/data.js reads pools and tokens through MarketLens (getPools and tokenInfo in pages of 40; aggregate with one token per batch, since its targets get all the gas) and holds the integer price math. js/descifi/market.js builds the market that /tokens, the token page and the swap's market head share (once per block). On the site it also reads /data/tokens.json, which tools/seo/tokens.py writes (schema in its docstring). Live values always win; the file only adds what needs history: holders, the 24 h change and the 7 day line.
  • Site settings: site.verifiedTokens adds the verified check and drops the unverified warning; site.hiddenTokens leaves a token out of lists, the picker and the sitemap. Its page still works, with a notice and noindex.
  • Styles: parts/dex.css only, with dx- class names (tabular figures apply under .dx-page). --buy/--sell (and --dx-on-buy/--dx-on-sell for text on them) mean buy, sell, up and down; everything else stays blue or neutral. Layouts follow the frame through container queries (dx on .dx-band > .frame, dxmain on the token page's main column), not the window. The swap ticket's data-side (buy, sell, neutral) colours its edge and main button.
  • Tests: tools/e2e/run.py descifi (swaps, liquidity, locks, fees, safety checks) and tools/e2e/run.py market (/tokens, token pages, tokens.json, hostile names). For a dev copy with data, run python3 tools/seo/tokens.py --rpc … --deployment deployments/local.json --data DIR, then serve it with serve_local.py --data DIR.

Proposal builders

The composer (js/views/dao-new.js) handles kind choice, the write-up, deposit, policy check (DAOPolicy.validate), decoding, simulation and submission. A proposal type's module js/views/new/<kindKey>.js only turns a form into actions:

export const builders = [{
  key: "send",                    // path segment: propose/<kindKey>/<key>
  label: "Send L1 or tokens",     // shown on the block and on "Add another action"
  icon: "send",                   // sprite id
  desc: "One sentence on what it does.",
  single: false,                  // true: the only block allowed in the proposal
  form({ dao, member, preset, changed, kind }) {
    // preset: plain object from ?preset= (strings and numbers) or null; the composer itself reads
    // preset.title and preset.description for the write-up
    // changed(): call after programmatic changes (typing is detected automatically)
    return {
      el,                         // the form
      async build() {             // → { actions: [{target, value: bigint, data: "0x…"}], title? }
        // Validate with the forms.js fields; throw new Error("what to fix") when invalid.
        // May send preparatory transactions (e.g. writeBlob for a constitution text).
      },
      preset() {                  // optional: the form's current values as a preset, so
        // "Copy a link to this draft" reopens the form filled in
      },
    };
  },
}];

Rules for builders: produce exactly the calls the DAO's default policy allows for that kind (contracts/core/PolicyBuilder.sol lists them; approvals only to the protocol spender of that kind). Reset approvals you raise (approve(spender, 0) after the call that spends). Use ZERO-checks, bounds and decimals from chain data. Prefill from preset so other views can link straight to a ready form: ctx.proposeHref("membership", "freeze", {account: "0x…"}) makes /dao/<t>/propose/membership/freeze?preset=<base64url JSON>. A preset only fills the form; the review still decodes, policy-checks and simulates exactly what will be submitted. Kind keys: signal treasury params constitution profile membership issuance tokenadmin descifi cipnft gl1f signature custom. A kind's module may also export note({dao, kind}), shown under its builders (the profile's says the DAO's name is permanent).

Logo and description editors

js/views/profile-editors.js serves the wizard, the profile builders and the representatives' tools, so the three behave the same:

  • logoEditor({seed, current, initial, allowRemove, name, onChange}): pick any image (or drop it); drag it and zoom to choose the square to keep (keyboard: arrows move, + and − zoom); the result is a 128 × 128 PNG drawn in the browser, shown at its actual size and as cards and lists show it, with its byte count against 512 KB. value() → {change: false} or {change: true, bytes} (empty bytes remove the logo); await settled() before reading it.
  • descriptionEditor({value, label, rows, hint}): Markdown up to 256 KB with a live counter, a bar and a preview; read() → {text, bytes, removed} as the contract stores it, or null when too long.
  • inPlaceNote(what) (an edit overwrites the stored bytes; the state does not grow), historyNote(what) (in a proposal: the bytes enter the chain's history three times, a representative's direct edit once) and titleNote(name) (the name is permanent), where people look for them. A large logo or description costs more gas than nodes estimate: builders show the execution's gas (executeGasLimit), the composer and the proposal page say when they cannot simulate it, and Submit, Execute, the wizard and the representatives' tools pass the formula's limit to send(). Where that limit applies they show failureLine(); Execute reads the proposal's state again just before sending, and the wizard checks the web address again just before the wallet opens.

The docs pages

The documentation is Markdown: the files of docs/ and this page (slug web-app), read on GitHub as they are. tools/build_docs.py (standard library only) turns them into the site's /docs section, after GL1X's docs. Its registry, PAGES, holds every page in reading order with its slug, file, title, group and writer; GROUPS orders the groups. A page starts with the licence line, then <!-- summary: … --> (one sentence, at most 160 characters: the index, search results and the meta description use it), then # Title, which must be the registry's title, then a one-paragraph lead.

python3 tools/build_docs.py --check [FILE ...]   # sources, the app's links into the docs, generated files
python3 tools/build_docs.py --check --strict     # a placeholder or missing page fails too (the release)
python3 tools/build_docs.py --write              # write every generated file
python3 tools/build_docs.py --list               # the registry with each page's state and words
python3 tools/build_docs.py --index-md           # the page list docs/README.md carries
python3 tools/docs_figures.py --src DIR [KEY ...]  # screenshots to web/assets/docs/<key>.webp
  • What --write writes. web/docs.html (the docs home from docs/README.md: audiences, a search box, and the index of every page by group in place of the README's page list), web/docs/<slug>/index.html (each page with the portal shell, tools/shell_html.py's markup and modulepreload block, its head, "On this page", the group's pages and previous/next), web/docs/<slug>.md (the Markdown as it is), web/docs/index.json (the registry with headings, words and dates), web/docs/search.json (each section's heading and text, for the search box) and web/llms.txt. A page's "updated" date is the day --write first saw its current text (index.json keeps a digest of each text), so writing again changes no date. In a release tree every asset URL carries the release's ?v=.
  • What --check refuses. A missing licence or summary line, a summary over 160 characters, a title that is not the registry's, a duplicate slug or heading anchor, a link to a page, an anchor or a repository file that does not exist, a link that is neither https:, a site path nor a docs link, an unknown figure or live figure key, a live figure twice on a page, more than four figures (screenshots and live figures together), a figure without its caption, a README page list that misses a page or leaves reading order, a url.docs() in web/js, an entry of DOC_MOVED or a /docs/… link in web/*.html that leads nowhere, and a generated file that is out of date. It warns on raw HTML (shown as text) and on a README summary that differs from the page's. tools/lint_web.py runs it.
  • The Markdown subset. # to #### headings (## and ### make the contents; ids as GitHub makes them), paragraphs, **bold**, *italic*, `code`, nested - and 1. lists, GFM tables, ```lang fences, ---, callouts (> **Note.**, > **Warning.**, > **Tip.**), figures (![Caption](figure:KEY) or figure:KEY:phone) and live figures (![Caption](live:KEY)), each alone on a line. Everything is escaped; HTML in the Markdown is shown as text. Links resolve relative to the file, as GitHub resolves them: another page (proposals.md#delegation, ../web/DEVELOPING.md#urls) becomes /docs/<slug>#id, a repository file that is not a page shows as its text (no repository URL is set: REPO_URL), and only https:// links leave the site. In docs/faq.md every ### question with the text under it becomes a Question of the page's FAQPage JSON-LD.
  • Figures. Screenshots of the site, committed as WebP in web/assets/docs/: the top 1360 × 850 of a gallery capture scaled to 1088 × 680, or 390 × 844 for :phone, at most about 120 KB each. tools/docs_figures.py holds the keys with their sources and crops; the docs build only reads their sizes and never converts.
  • Live figures. Drawings that js/pages/docs.js makes in the page, from a fixed list of keys (LIVE_FIGURES in the build): anatomy (a DAO's six contracts as a molecule), holding (which shares count with a holding period), lifecycle (the life of a proposal), ballots (the worked example under the standard and critical rules, as a proposal page draws its tally) and deployment (this site's network and contract addresses, from config.js and the DAO factory; "This site has no deployment yet" before deployment). The build writes the caption and an empty slot; the figure stays hidden until the module has drawn it, so without JavaScript there is no empty frame. A drawing with a narrow layout is made in both and the slot's width picks one (container queries), and colours are the theme's tokens. The .md copies keep the line as it is, and the search text has the caption.
  • The pages. One reading column of Source Serif at about 70 characters; "On this page" and the group's pages on the right from 1280px, and below that a bar under the top bar with both as menus (the site's sidebar stays the only rail). Styles are the docs section of app.css (dc- classes). js/pages/docs.js draws the live figures and adds the scroll-spy, copy buttons on code, keyboard access to tables and code that scroll sideways, the home's search over /docs/search.json, and it forwards the anchors of the single docs page of 2.2.0 (/docs#holding-period) to their new place. The top bar's search lists docs pages and sections from /docs/index.json as its "Docs" group.
  • Links from the app. url.docs(ref) takes "", a slug, slug#id, or an old anchor that DOC_MOVED in js/core/router.js maps to its page and section; --check fails when a target is gone, so renaming a heading the app links to means updating DOC_MOVED.
  • Serving. /docs is docs.html, /docs/<slug> is docs/<slug>/index.html, /docs/<slug>.md is served as text/markdown; charset=utf-8, /llms.txt as text. On the server they come from the release only, never from the prerender's folder. The sitemaps list the written pages after the static ones (python3 tools/prerender.py --static-sitemap refreshes web/sitemap.xml).
  • Tests. python3 tools/tests/test_build_docs.py (the renderer: escaping, links, tables, callouts, code, figures, live figures, FAQ) and tools/e2e/run.py docs (docs_live_figures draws every live figure at 390 and 1360 px in both themes and compares the addresses with config.js and the factory).

Running locally

Prerequisites: Python 3.10 or later, Foundry (anvil, cast, forge, on PATH or in $FOUNDRY_BIN), Playwright for Python with Chromium. The sealed-data scenarios also need a checkout of the CIPNFT site, whose own code plays the other side of every trade: point $CIPNFT_REF at it. Local development has the whole setup, the sandboxes, the test wallets and the common pitfalls; Testing has the suites.

python3 tools/local_chain.py up --seed          # chain on :8545 with demo data, writes web/config.local.js
python3 tools/serve_local.py --port 8080        # serves web/ with the clean paths; reload to see edits
python3 tools/serve_local.py --no-rewrites      # a host without rewrites: clean paths go through 404.html
python3 tools/dev_sandbox.py snapshot           # snapshot the seeded chain (once)
python3 tools/dev_sandbox.py up <name> --rpc-port 86xx --web-port 87xx   # your private copy
python3 tools/dev_sandbox.py reset <name>       # back to the snapshot
python3 tools/dev_sandbox.py warp <name> 3600   # move your chain clock forward
python3 tools/dev_sandbox.py down <name>
python3 tools/lint_web.py                       # the rules a script can check: 0 errors before every commit

serve_local.py serves the same rewrites as production (see "URLs"), prerendered pages (web/dao/<slug or address>/index.html, web/pools/, web/locks/) when they exist, and generates /config.js from web/config.local.js with the sandbox RPC, no explorer and a local price endpoint (/dev/price, a fixed demo price in CoinGecko's format), so nothing leaves the machine. Its CSP allows any http://127.0.0.1:* endpoint, to try a custom RPC in settings.

Use your own sandbox when you send transactions or move time, so you never disturb anyone else. The test wallet in tools/e2e/harness.py (Browser(base, rpc, account)) injects an EIP-6963 wallet backed by anvil's unlocked development accounts, and marks the cookie notice as seen (cookie_notice=True shows it). b.go(path) loads a page; b.nav(path) follows an in-app link instead (no page load, so what the tab holds in memory, such as unlocked keys, stays). Look at what you build: take screenshots (b.shot(name)), read them, and fix what looks wrong; check light and dark (dark=True) and a 390 px wide viewport. b.errors must stay empty of errors.

End-to-end tests: tools/e2e/scenarios/<area>.py exports SCENARIOS = [fn, …]; each fn(env) gets a fresh sandbox (env.browser(i), env.dao(n), env.dao_url(n, *parts, preset=) (the clean path), env.send(i, to, sig, *args, value=), env.cast(...), env.warp(s), env.rpc(m, p), env.accounts, env.deploy, env.demo). Version 1 URLs (dao.html#/<t>/proposal/3) still redirect, so older scenarios keep working. Run python3 tools/e2e/run.py <filter> --name <you> --rpc-port <p> --web-port <q>, and python3 tools/e2e/sweep.py --base <web> --rpc <rpc> for the main pages and one DAO's tabs in light, dark and 390 px (not /votes, /tokens or the pages under /docs/).

serve_local.py's other modes: --undeployed (config.js before the operator deploys), --site site.json (site settings), --data DIR (DIR at /data/, and this server becomes the configured site), --prerender DIR (the prerender's output before web/, as on the server). A missing /data/ file answers 204.

Release build: versions, preloads and compression

There is no bundler. python3 tools/build_release.py stages a copy of the tree and, in the copy: tools/version_assets.py appends one release-wide ?v=<12 hex> to every module import (static and dynamic), every /js/… string, the stylesheets, fonts, the icon sprite and CSS url()s, and fails when a reference is left unversioned or one module is reached under two URLs (two instances), the modulepreload links in each entry included (the page module's static imports, written by tools/shell_html.py, which reads the version from the copy's BUILD.json); the static pages (/pools, /locks) and the docs pages are written again from the versioned tree; then every text file of 1 KB or more gets a reproducible .gz sibling for nginx's gzip_static. The extracted archive is checked again (version_assets.py --check, shell_html.py --check, prerender.py --static-pages --check, build_docs.py --check --strict). The server caches URLs with the current release's ?v= for a year as immutable (the installer reads it from web/BUILD.json) and revalidates any other ?v= and the rest. Building a release lists every step. What this means for code: import modules by relative path (./x.js, never with a query of your own), name scripts root-absolute (/js/…), and keep the modulepreload blocks fresh with tools/shell_html.py --write after changing a page's imports (lint warns). The e2e tests of a release run against the built tree. web/pools/ and web/locks/ are copies of descifi.html with their own head: after changing descifi.html, run python3 tools/prerender.py --static-pages (lint fails on stale copies).

Demo data in the snapshot

Accounts are anvil's development accounts #0–#11 (1,000,000 L1 each). deployments/demo.json holds the demo USDC (6 decimals; #0–#9 hold 250,000).

  • Every demo DAO has a slug from its title (open-proteomics-collective, longevity-data-commons, university-microscopy-core, crypto-signals-research-guild, open-model-exchange), a 128 × 128 PNG logo (script/assets/<slug>.png, 1.9–4.3 KB, drawn by python3 tools/make_demo_logos.py), a short Markdown description and one representative, its second founder, with permissions 30 (description, logo, links and announcements).
  • DAO 1, Open Proteomics Collective (OPC): members #0–#7 with 400/250/150/80/50/40/20/10 OPC; the treasury holds 200 OPC, about 2,250 L1, 45,000 USDC and PROT. Representative #1. Stewards #0 and #1. Minimum deposit 2 L1. Standard rules 1 h voting, 10 min timelock, quorum 33.4%, pass 50%, veto 33.4%; critical 2 h, 20 min, 40%, 66.67%, 33.4%. Proposals: #1 PROT issuance (executed), #2 250 L1 grant to #9 (executed), #3 FAIR signal (passed), #4 lower deposit (executed), #5 freeze #7 (rejected), #6 GL1F pricing, #7 raw payment, #8 constitution v2 (all voting), #9 hackathon signal (deposit period). PROT: launched by the DAO; member airdrop claimed by #0–#5; a one-sided PROT/USDC launch pool (1% fee, LP held by the treasury) and a two-sided PROT/USDC pool (0.3%, LP locked forever, fees to the treasury). GL1F Model NFT #1 is owned by the treasury. One announcement by #1.
  • DAO 2, Longevity Data Commons (LDC): members #2, #3, #8, #9; 800 L1 in the treasury.
  • DAO 3, University Microscopy Core (UMC): members #4–#8 with 100 shares each.
  • DAOs 1–3 use the GL1F collection and the first CIPNFT contract. DAO 4, Crypto Signals Research Guild (CSRG): members #6, #7, #8 with 150/100/50, 300 L1, the GL1F Crypto collection and the second CIPNFT contract only, and a GL1F Crypto Model NFT #7 donated. DAO 5, Open Model Exchange (OMX): members #1, #5, #9 with 120/90/60, 300 L1, both collections and both CIPNFT contracts, and two models #5 donated (one GL1F Crypto, one GL1F).
  • A public wL1/USDC pool (0.3%). CIPNFT #1 "Phospho cohort sealed" listed by #9 for 25 L1 (dummy ciphertext), and on the second CIPNFT contract "Natural history registry sealed", listed by #9 for 40 L1. Keyring entries for #0–#3 are dummy keys.

Icons

icon(name) uses /assets/img/icons.svg: menu sun moon wallet copy external check x plus minus search chevron-right chevron-down arrow-left arrow-down signal vault sliders scroll users shield coins token swap lock unlock model pen terminal clock info warn flask molecule download upload eye key chart refresh trash edit link doc gift fire layers home vote droplet send gear sparkline. Need another? Draw it inline with s() in your module and mention it in your report.