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

COLLAB

Data formats

COLLAB's site is static, so a handful of files carry everything it needs besides the chain: the configuration, the site settings, the deployment record, the deploy bundle, and the cache files the server's prerender job publishes. This page gives each one's schema, an example, its size limits, and which tool writes it and which code reads it. It is for developers who generate, check or consume these files; operators will find what each setting does in Site settings and what the prerender writes when in Static pages, data files and prices.

Part
Developers
Version
COLLAB 2.2.0
Updated
Reading time
16 min

Files at a glance

FileWhereWritten byRead byLimit
config.js/config.js: shared/config.js on a server, web/config.js on a static hosttools/write_web_config.py (the admin service and the installer use it)web/js/core/env.js, tools/prerender.py, collab apply
site.jsonshared/site.jsonthe admin service (POST site), the installer (defaults)write_web_config.check_site()65,536 bytes normalized
deployments/<chainId>.jsondeployments/ in the repository, shared/deployments/ on a serverscript/Deploy.s.sol, the admin page's Deploy tab (download)write_web_config.py, prerender.py, the admin service64 keys
collab-protocol.jsonweb/assets/deploy/tools/export_deploy_artifacts.pythe admin page's Deploy and Check tabs
price.json/data/price.jsonthe prerenderweb/js/core/price.js10 KB
tokens.json/data/tokens.jsonthe prerender (tools/seo/tokens.py)web/js/descifi/market.js, web/js/core/search.js4 MiB
logos and texts/data/logos/<hash>.png, /data/texts/<hash>.txtthe prerenderweb/js/core/daoprofile.js524,288 and 262,144 bytes
network.json/og/network.jsonthe prerender (tools/seo/network.py)web/js/core/directory.js512 KB
BUILD.jsonweb/BUILD.json in a release treetools/version_assets.pytools/shell_html.py, server/install.sh

On a server the files under /data/ and /og/ are written by the user collab-prerender; a /data/ file or og/network.json that is not there yet answers 204, and pages then read the chain. Every file the browser reads is checked before use, and anything named by a hash is verified against the hash the chain reports.

Site configuration

config.js is the only deployment-specific file of the site. It is a classic script, loaded by every page with <script src="/config.js" defer>, so that no page needs a fetch to learn where it runs:

/* MIT License. Copyright (c) 2026 Decentralized Science Labs LLC. */
// Generated by tools/write_web_config.py; do not edit by hand.
// Every address below must match deployments/<chainId>.json.
window.COLLAB_CONFIG = Object.freeze({ … });

The JSON between Object.freeze( and the last ), indented by two spaces, is the configuration; config_of_js() and the prerender read it back that way. Its fields, in order:

FieldValue
appName"COLLAB"
chainId29
chainName, nativeSymbol, nativeName"GenesisL1", "L1", "L1 coin"
rpcs, rpcUrlsthe RPC presets [{name, url}] in failover order, and their URLs. Default: GenesisL1 RPC https://rpc.genesisl1.org and GL1X JSON-RPC https://gl1x.com/api/eth-rpc; site.json's rpcs replace them
allowCustomRpcwhether visitors may add their own endpoint (true unless --no-custom-rpc)
explorers, explorerthe explorer presets and the default one's URL (GL1X, https://gl1x.com; site.json's explorer replaces it)
priceApithe L1 price URL with {vs} for the currency (CoinGecko's simple price for genesisl1), "" for no prices
analytics{googleMeasurementId} when site.json's analytics.ga4 is set, else {}
siteUrlthe site's public origin for canonical links (default https://collab.genesisl1.com; on a server, its own origin)
startBlock, versionfrom the record; 0 and "" when undeployed
deployedfalse until a deployment is published
addressesfactory, daoLens, marketLens, textStore, keyring, desciFactory, desciRouter, lockFactory, distributor, launcher, wl1; {} when undeployed
integrations{gl1f: [{id, name, site, registry, nft, market, runtime, store}], cipnft: [{id, name, site, address}]}, the deployment's default lists
sitethe public site settings: alpha, announcement, featuredDaos, hiddenDaos, verifiedTokens, hiddenTokens, seo, links (normalized, every key present)

The undeployed form, which the installer writes before the operator deploys, from a test run:

{
  "appName": "COLLAB", "chainId": 29, "chainName": "GenesisL1", "nativeSymbol": "L1", "nativeName": "L1 coin",
  "rpcs": [{"name": "GenesisL1 RPC", "url": "https://rpc.genesisl1.org"}, {"name": "GL1X JSON-RPC", "url": "https://gl1x.com/api/eth-rpc"}],
  "rpcUrls": ["https://rpc.genesisl1.org", "https://gl1x.com/api/eth-rpc"],
  "allowCustomRpc": true,
  "explorers": [{"name": "GL1X", "url": "https://gl1x.com"}], "explorer": "https://gl1x.com",
  "priceApi": "https://api.coingecko.com/api/v3/simple/price?ids=genesisl1&vs_currencies={vs}&include_24hr_change=true",
  "analytics": {},
  "siteUrl": "https://127.0.0.1:8443",
  "startBlock": 0, "version": "", "deployed": false,
  "addresses": {}, "integrations": {"gl1f": [], "cipnft": []},
  "site": {"alpha": {"show": true, "text": "Alpha: COLLAB runs on GenesisL1 mainnet and has had no external audit. Use small amounts."},
           "announcement": null, "featuredDaos": [], "hiddenDaos": [], "verifiedTokens": [], "hiddenTokens": [],
           "seo": {"title": "", "description": ""},
           "links": {"website": "", "x": "", "telegram": "", "discord": "", "github": ""}}
}

Integration entries follow one rule in the tool and in env.js: an id of 1 to 32 lowercase letters, digits and hyphens that starts and ends with a letter or digit (x- is reserved), a plain name of at most 40 characters in one script (A to Z and the Latin-1 letters, or Chinese, Japanese and Korean) that no other entry uses and that reads like no collection's labels ("GL1F market"), ignoring case and look-alikes such as 1/l/I and 0/O, an https site or "", and distinct non-zero addresses, at most 8 entries of each kind. Every URL must be https (http only for 127.0.0.1 and localhost) with a host of ASCII letters, digits, dots and hyphens and an optional port, because its origin goes into the pages' Content-Security-Policy as it is.

  • Writer. tools/write_web_config.py is the one writer. A value on its command line wins over site.json, and site.json over the defaults. On a server the admin service and the installer (collab_admin.py config) call its build_config() and config_js(). The admin page's Deploy tab builds the same file in the browser (buildConfig() in web/js/deploy/files.js) and an end-to-end test checks the two are byte for byte equal. tools/serve_local.py serves a development variant made from web/config.local.js.
  • Readers. web/js/core/env.js validates it once and stops the app with COLLAB configuration error: … on a missing or malformed value. tools/prerender.py --deployment <config.js> and collab_admin.py apply read it with config_of_js(); the policy step checks every value again (csp_of_config()) before it writes the policy into the pages.
python3 tools/write_web_config.py deployments/29.json --patch-csp                  # web/config.js and every page's CSP
python3 tools/write_web_config.py --no-deployment --site site.json --out config.js   # undeployed
python3 tools/write_web_config.py --csp-of config.js --header                        # the policy as a response header

Site settings

site.json holds what the operator changes from the admin page's Site settings tab. check_site() in tools/write_web_config.py reads it strictly: unknown keys are refused, every key is optional, and the normalized result has every key. Site settings says what each one changes on the site.

KeyType and limitsDefaultMeaning
alpha{show: bool, text}, text up to 200 characters; an empty text means the standard one{show: true, text: "Alpha: COLLAB runs on GenesisL1 mainnet and has had no external audit. Use small amounts."}the alpha strip
announcementnull, or {text, href, tone}: text up to 300, href "", an https URL or a path of this site (/votes), tone "info" or "warn"; an empty text means nonenullthe announcement
featuredDaosup to 24 slugs[]featured DAOs
hiddenDaosup to 1,000 slugs or treasury addresses[]hidden DAOs
verifiedTokensup to 1,000 token addresses[]verified tokens
hiddenTokensup to 1,000 token addresses[]hidden tokens
seo{title, description}, up to 70 and 200 charactersemptysearch engines
links{website, x, telegram, discord, github}, each "" or an https URL of up to 300 charactersemptylinks
analytics{ga4}: "" or a Google Analytics 4 ID such as G-XXXXXXX{ga4: ""}analytics
rpcsup to 8 {name, url}, names up to 60 characters (default: the host), distinct https URLs (http on localhost); empty means the defaults[]RPC endpoints
explorernull (the default, GL1X) or {name, url}nullblock explorer

Rules for every value (the whole file): texts are trimmed and may hold no line break, control, bidirectional or invisible character; URLs may hold only plain URL characters and pass the URL rule of config.js; slugs follow the factory's rule (3 to 48 of a-z, 0-9 and -, a letter first, no --, no - at the end); addresses are 40 hex digits, not zero, with a right EIP-55 checksum when mixed case, and are stored lowercase; no list repeats an entry; a DAO cannot be both featured and hidden, nor a token both verified and hidden; the normalized settings take at most 65,536 bytes; and the file is strict JSON, without repeated keys or NaN. A refusal names the field ("featuredDaos.3", "rpcs.1.url").

In config.js, alpha, announcement, the four lists, seo and links become site; rpcs, explorer and analytics fill config.js's own rpcs, explorers and analytics. A saved example, as the admin service stores it in shared/site.json (the file has every key, with a two-space indent; nested objects are shown on one line here):

{
  "alpha": {"show": true, "text": "Alpha: COLLAB runs on GenesisL1 mainnet and has had no external audit. Use small amounts."},
  "announcement": {"text": "Voting on the 2027 budget is open.", "href": "/votes", "tone": "info"},
  "featuredDaos": ["open-proteomics-collective"],
  "hiddenDaos": [], "verifiedTokens": [], "hiddenTokens": [],
  "seo": {"title": "", "description": ""},
  "links": {"website": "", "x": "", "telegram": "", "discord": "", "github": ""},
  "analytics": {"ga4": ""},
  "rpcs": [],
  "explorer": null
}

Deployment records

A deployment record lists one deployment of the protocol: deployments/<chainId>.json, so 29.json for GenesisL1. script/Deploy.s.sol writes it after the 20 creations (or to the path in DEPLOY_OUT; tools/local_chain.py writes deployments/local.json, since local chains use chain id 29 too). Foundry's writeJson sorts the keys at every level and indents by two spaces. The admin page's Deploy tab offers the same record for download, and its Publish tab sends it to the admin service, which keeps it as shared/deployments/<chainId>.json.

KeyValue
chainIdthe chain id, 29
startBlockthe block the deploy script started at; logs are read from here
version"COLLAB DAO v2.2.0", the factory's VERSION
textStore, keyring, desciFactory, desciRouter, lockFactory, distributor, launcher, policyBuilder, factory, daoLens, marketLensthe protocol's contracts
wl1wL1 (not deployed by COLLAB)
gl1f[{id, market, name, nft, registry, runtime, site, store}], the factory's default GL1F collections
cipnft[{address, id, name, site}], the factory's default CIPNFT contracts
rpcoptional: an RPC URL (local chains)

The implementations behind the clones are not in the record: read them from factory.implementations(), desciFactory.pairImplementation(), lockFactory.implementation() and launcher.tokenImplementation(). The local demo chain's record:

{
  "chainId": 29,
  "cipnft": [{"address": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512", "id": "cipnft", "name": "CIPNFT", "site": "https://cipnft.com"}, …],
  "daoLens": "0x9E545E3C0baAB3E08CdfD552C960A1050f373042",
  "desciFactory": "0x68B1D87F95878fE05B998F19b66F4baba5De1aed",
  "desciRouter": "0x3Aa5ebB10DC797CAC828524e59A333d0A371443c",
  "distributor": "0x4ed7c70F96B99c776995fB64377f0d4aB3B0e1C1",
  "factory": "0x84eA74d481Ee0A5332c457a4d796187F6Ba67fEB",
  "gl1f": [{"id": "forest", "market": "0x0165…Eb8F", "name": "GL1F", "nft": "0xCf7E…0Fc9", "registry": "0x9fE4…a6e0",
            "runtime": "0x5FC8…5707", "site": "https://gl1f.com/", "store": "0xa513…C853"}, …],
  "keyring": "0x959922bE3CAee4b8Cd9a407cc3ac1C251C2007B1",
  "launcher": "0xa85233C63b9Ee964Add6F2cffe00Fd84eb32338f",
  "lockFactory": "0x59b670e9fA9D0A427751Af201D676719a970857b",
  "marketLens": "0xa82fF9aFd8f496c3d6ac40E2a0F282E47488CFc9",
  "policyBuilder": "0x4A679253410272dd5232B3Ff7cF5dbB88f295319",
  "rpc": "http://127.0.0.1:8545",
  "startBlock": 15,
  "textStore": "0x0B306BF915C4d645ff596e518fAf3F9669b97016",
  "version": "COLLAB DAO v2.2.0",
  "wl1": "0x5FbDB2315678afecb367f032d93F642f64180aa3"
}

Two levels of checking:

  • Any record (build_config(), so every config.js): the eleven keys of config.js's addresses are addresses, chainId and startBlock are numbers, and the gl1f and cipnft lists pass the integration rule above.
  • A published record (check_record(), used by the admin service and by write_web_config.py --strict): also a JSON object of at most 64 keys; chainId a positive whole number, equal to the site's chain; startBlock a block number; version reading COLLAB DAO v<major>.<minor>.<patch> with the release's major and minor version (2.2.x for the 2.2.0 server kit); policyBuilder present; every other key an address apart from gl1f, cipnft and rpc; every address non-zero, used once, and with a right EIP-55 checksum when mixed case. What the server refuses shows the operator's side.

Neither check reads the chain. The admin page's Check tab compares each contract's code and wiring on chain with the deploy bundle before Publish unlocks.

The deploy bundle

web/assets/deploy/collab-protocol.json (format collab-protocol/2, about 790 KiB) is everything the admin page needs to repeat script/Deploy.s.sol from a browser wallet and to check an existing deployment. tools/export_deploy_artifacts.py writes it from Foundry's out/ and from the script itself; --check exits 1 when it is missing or stale, and the release build runs it again in the extracted archive.

KeyContents
format, release, version, generator"collab-protocol/2", "2.2.0", "COLLAB DAO v2.2.0", the tool
script{path, keccak256} of script/Deploy.s.sol
compiler{solc: "0.8.28+commit.7893614a", evmVersion: "istanbul", optimizer: true, runs: 200, viaIR: true, bytecodeHash: "ipfs"}
externals[{key: "wl1", env: "EXT_WL1", constant: "GL1_WL1", chain29: "0x59a1…1fe2"}]
integrations{env: "EXT_INTEGRATIONS", file: "script/integrations.json", chain29: {cipnft: […], gl1f: […]}}
structsthe ABI layouts of Gl1fCollection, Implementations and Protocol
deployments{format: "forge-writeJson", keys: [{key, type, from or value}]}: how to write the record
contractsthe 20 creations in order: {index, key, name, source, artifact, constructor, args, bytecode, deployedBytecode, immutableReferences, codeSize, codeHash, checks}

Each constructor argument in args names where its value comes from ({"deployment": "pairImpl"}, {"external": "wl1"}, the integration lists), and checks names the getter that proves it on chain (factory.implementations(), the lenses' factory() and so on). codeHash is keccak-256 of the deployed code with every immutable zeroed, so the page recognises this release's code at any address. The tool refuses to write when an artifact was compiled from sources that changed since, when the compiler settings differ from foundry.toml, or when the script holds a statement it does not understand.

The L1 price

The prerender fetches L1's price in US dollars from priceApi (CoinGecko by default) and writes /data/price.json, so that no visitor's browser asks CoinGecko for the default display:

{"at":"2026-11-01T22:40:00Z","source":"CoinGecko","usd":0.0042,"usd24hChange":2.4}
FieldValue
usdthe price of 1 L1 in US dollars, above 0 and at most 10⁹
usd24hChangethe 24-hour change in percent, or null
atwhen the price was fetched, UTC to the second
source"CoinGecko"

Keys are sorted and the file is at most 10 KB; a failed fetch keeps the previous file. web/js/core/price.js reads it only on the configured site (onSite()), at most 10 KB, and uses it for 24 hours; after that, or without it, amounts show in L1 only, unless the visitor chose a currency, which the browser then fetches live. tools/serve_local.py answers a fixed demo price ("source": "dev") when it serves no file.

The tokens file

/data/tokens.json lists every token with a DeSciFi pool, the most liquid first. tools/seo/tokens.py builds it at one block, two below the head, and its docstring explains how each figure is made. Every L1 figure is an exact decimal string, never a float, and a figure past 10³⁰ L1 is published as null.

FieldValue
version, chainId, block, time1, the chain, the block read and its time (Unix seconds)
generatedwhen the file was written (ISO 8601 UTC)
completefalse while older history is still being read; the next run resumes
wl1the wL1 address
seriesTimes28 Unix times 6 hours apart, then time: the points of every series
tokensthe rows below
Token fieldValue
addresslowercase
symbol, namecleaned, at most 24 and 64 characters
decimals, supplydecimals; total supply in base units, as a string
priceL1, liquidityL1, marketCapL1price along the deepest path of pools to wL1, the value of both sides of its pools, price × supply; null without a path to wL1
change24h, price24hL1the 24-hour change in percent with 2 decimals, and the price then; null when unknown
holders, holdersExactaccounts holding it; holdersExact false means "at least"; null when its balanceOf fails
series29 prices in L1 (numbers) or nulls on seriesTimes; the last is block's price
pools[{address, other, otherSymbol, feeBps, launch, reserve, otherReserve, liquidityL1}], reserves in base units
dao{slug, title, treasury} of the DAO that launched it (factory DAOs only), else null
launchedBythat DAO's treasury, else null
verifiedlisted in verifiedTokens, launched by a featured DAO, or wL1
hiddenlisted in hiddenTokens
wl1true, on wL1's row only

A row from the demo chain (series and pools shortened):

{"address": "0x7ad46273be08b0c6dbcd71aa36d5d631b90a9e67", "symbol": "PROT", "name": "Proteome Research Credit", "decimals": 18,
 "supply": "1000000000000000000000000", "priceL1": "0.022408110506666666", "liquidityL1": "6069.021216863815589928",
 "marketCapL1": "22408.110506666666", "change24h": null, "price24hL1": null, "holders": 12, "holdersExact": true,
 "series": [null, null, …, 0.0224081],
 "pools": [{"address": "0x8e02b8b5c74811c86ff91fc4abdff242dc1be487", "other": "0x1613beb3b2c4f22ee086b2b38c1476a3ce7f78e8",
            "otherSymbol": "USDC", "feeBps": 30, "launch": false, "reserve": "86242583137850144887540",
            "otherReserve": "5797600000", "liquidityL1": "3865.066666666666666666"}, …],
 "dao": {"slug": "open-proteomics-collective", "title": "Open Proteomics Collective", "treasury": "0x31892f2cc3ddd22f596012e31b7489db2131ace7"},
 "launchedBy": "0x31892f2cc3ddd22f596012e31b7489db2131ace7", "verified": false, "hidden": false}

The file is at most 4 MiB: past that the series go first, then the pool lists, then the least liquid tokens. The pages read it only when CFG.deployed and onSite() hold, check the chain id and every row, and recompute prices, liquidity and market caps live at each block; the file adds what needs history (holders, the 24-hour change, the series). The search box reads its address, symbol, name and dao. For a development copy: python3 tools/seo/tokens.py --rpc URL --deployment deployments/local.json --data DIR, then tools/serve_local.py --data DIR.

Logos and descriptions

With --data, the prerender copies every DAO's logo and description from the chain into /data/logos/<hash>.png and /data/texts/<hash>.txt. The name is the keccak-256 the profile reports (64 lowercase hex digits, no 0x); each file is read in 16 KB chunks, verified against that hash, written only once, and removed when no DAO refers to it. A text file holds the description's exact bytes, UTF-8 with LF line breaks. Since the name is the content's hash, nginx serves these files as immutable for a year.

web/js/core/daoprofile.js fetches them only on the configured site, reads at most the contract's limit, checks keccak-256 again and falls back to the chunked chain reads when a file is missing (204), larger or different (Reading COLLAB from the chain).

For tools and tests that plant content with anvil_setStorageAt, the profile's storage layout is fixed. The base slot s is 0x574af0c02d0394ee061982225855fe38f89b5dd7c2c0247e080882f897e4e300 for the logo and 0x7c9b6d45da86457cb1555196070d0c7fd73621431116e88fee376d3a7f69f800 for the description, the ERC-7201 slots of "collab.profile.logo" and "collab.profile.description". Slot s holds the length, s + 1 the keccak-256 of the bytes (0 when empty), and data word i, big-endian like a Solidity bytes, is at uint256(keccak256(abi.encode(s))) + i, with the bytes past the length zero. Write the words, then the length, then the hash, so logoInfo/descriptionInfo and the chunks agree.

The member network

/og/network.json tells the home page which DAOs share members, as counts, never as member addresses. tools/seo/network.py builds it from every share token's UnitsChanged logs, reading only the blocks after its cache (kept beside the prerender's lock, mode 0600), and proves each member set against the token (isMember, memberCount) before writing. From the demo chain:

{"version":1,"chainId":29,"factory":"0x84eA74d481Ee0A5332c457a4d796187F6Ba67fEB","block":127,"generated":"2026-11-02T05:28:10Z",
 "daos":[{"treasury":"0x31892f2CC3ddD22f596012e31b7489dB2131aCE7","slug":"open-proteomics-collective","members":8},
         {"treasury":"0x2697F708536c8e99d54EFa0B2614317608e1E104","slug":"longevity-data-commons","members":4}, …],
 "shared":[[0,1,2],[0,2,4],[0,3,2],[0,4,2],[1,2,1],[1,3,1],[1,4,1],[2,3,3],[2,4,1]],
 "uniqueMembers":10}
FieldValue
version, chainId, factory, block, generated1, the chain, the factory (EIP-55), the block read through, when written
daos[{treasury, slug, members}]: each DAO the site shows, with its member count; hidden DAOs are left out
shared[i, j, n] with i < j indexes into daos: those two DAOs share n members
uniqueMembersaccounts that are members of at least one listed DAO

parseNetworkGraph() in web/js/core/directory.js accepts the file only when it is at most 512 KB, version 1, for this chain and factory, and consistent: whole numbers, distinct treasuries, each pair once, n between 1 and the smaller DAO's members, and uniqueMembers between the largest DAO's members and their sum. When the network cannot be read or proven, the prerender keeps the old file and its cache, with a warning.

The build stamp

web/BUILD.json exists only in a release tree. tools/version_assets.py writes it when it versions the tree, and refuses a tree that already has one or that is the working tree:

{
  "version": "2.2.0",
  "assets": "b2a3f12adb4f"
}

assets is the release-wide ?v= that every script, stylesheet, font and icon URL carries: 12 hex digits of a SHA-256 over the web tree's files. tools/shell_html.py reads it so the prerender's copies of the entries use the same URLs; the release's documentation pages carry it too. server/install.sh copies it into nginx on every run, rollbacks included, so only URLs with this release's ?v= are cached for a year as immutable, and any other ?v= is revalidated. A tree without BUILD.json, such as a working copy, keeps nothing for long. version_assets.py --check verifies a versioned tree against it (Building a release).