<!-- MIT License. Copyright (c) 2026 Decentralized Science Labs LLC. -->
<!-- summary: The schema, an example, size limits, writer and readers of config.js, site.json, deployment records, the deploy bundle and the /data and /og files. -->
# 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](site-settings.md) and what the prerender writes when in [Static pages, data files and prices](prerender.md).

## Files at a glance

| File | Where | Written by | Read by | Limit |
|---|---|---|---|---|
| `config.js` | `/config.js`: `shared/config.js` on a server, `web/config.js` on a static host | `tools/write_web_config.py` (the admin service and the installer use it) | `web/js/core/env.js`, `tools/prerender.py`, `collab apply` | |
| `site.json` | `shared/site.json` | the admin service (`POST site`), the installer (defaults) | `write_web_config.check_site()` | 65,536 bytes normalized |
| `deployments/<chainId>.json` | `deployments/` in the repository, `shared/deployments/` on a server | `script/Deploy.s.sol`, the admin page's Deploy tab (download) | `write_web_config.py`, `prerender.py`, the admin service | 64 keys |
| `collab-protocol.json` | `web/assets/deploy/` | `tools/export_deploy_artifacts.py` | the admin page's Deploy and Check tabs | |
| `price.json` | `/data/price.json` | the prerender | `web/js/core/price.js` | 10 KB |
| `tokens.json` | `/data/tokens.json` | the prerender (`tools/seo/tokens.py`) | `web/js/descifi/market.js`, `web/js/core/search.js` | 4 MiB |
| logos and texts | `/data/logos/<hash>.png`, `/data/texts/<hash>.txt` | the prerender | `web/js/core/daoprofile.js` | 524,288 and 262,144 bytes |
| `network.json` | `/og/network.json` | the prerender (`tools/seo/network.py`) | `web/js/core/directory.js` | 512 KB |
| `BUILD.json` | `web/BUILD.json` in a release tree | `tools/version_assets.py` | `tools/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:

```js
/* 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:

| Field | Value |
|---|---|
| `appName` | `"COLLAB"` |
| `chainId` | 29 |
| `chainName`, `nativeSymbol`, `nativeName` | `"GenesisL1"`, `"L1"`, `"L1 coin"` |
| `rpcs`, `rpcUrls` | the 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 |
| `allowCustomRpc` | whether visitors may add their own endpoint (true unless `--no-custom-rpc`) |
| `explorers`, `explorer` | the explorer presets and the default one's URL (`GL1X`, `https://gl1x.com`; site.json's `explorer` replaces it) |
| `priceApi` | the 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 `{}` |
| `siteUrl` | the site's public origin for canonical links (default `https://collab.genesisl1.com`; on a server, its own origin) |
| `startBlock`, `version` | from the record; 0 and `""` when undeployed |
| `deployed` | `false` until a deployment is published |
| `addresses` | `factory`, `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 |
| `site` | the 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:

```json
{
  "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.

```bash
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](site-settings.md) says what each one changes on the site.

| Key | Type and limits | Default | Meaning |
|---|---|---|---|
| `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](site-settings.md#the-alpha-strip) |
| `announcement` | `null`, 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 none | `null` | [the announcement](site-settings.md#the-announcement) |
| `featuredDaos` | up to 24 slugs | `[]` | [featured DAOs](site-settings.md#featured-daos) |
| `hiddenDaos` | up to 1,000 slugs or treasury addresses | `[]` | [hidden DAOs](site-settings.md#hidden-daos) |
| `verifiedTokens` | up to 1,000 token addresses | `[]` | [verified tokens](site-settings.md#verified-tokens) |
| `hiddenTokens` | up to 1,000 token addresses | `[]` | [hidden tokens](site-settings.md#hidden-tokens) |
| `seo` | `{title, description}`, up to 70 and 200 characters | empty | [search engines](site-settings.md#search-engines) |
| `links` | `{website, x, telegram, discord, github}`, each `""` or an https URL of up to 300 characters | empty | [links](site-settings.md#links) |
| `analytics` | `{ga4}`: `""` or a Google Analytics 4 ID such as `G-XXXXXXX` | `{ga4: ""}` | [analytics](site-settings.md#analytics) |
| `rpcs` | up to 8 `{name, url}`, names up to 60 characters (default: the host), distinct https URLs (http on localhost); empty means the defaults | `[]` | [RPC endpoints](site-settings.md#rpc-endpoints) |
| `explorer` | `null` (the default, GL1X) or `{name, url}` | `null` | [block explorer](site-settings.md#block-explorer) |

Rules for every value ([the whole file](site-settings.md#rules-for-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):

```json
{
  "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`.

| Key | Value |
|---|---|
| `chainId` | the chain id, 29 |
| `startBlock` | the 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`, `marketLens` | the protocol's contracts |
| `wl1` | wL1 (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 |
| `rpc` | optional: 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:

```json
{
  "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](publish.md#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.

| Key | Contents |
|---|---|
| `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: […]}}` |
| `structs` | the ABI layouts of `Gl1fCollection`, `Implementations` and `Protocol` |
| `deployments` | `{format: "forge-writeJson", keys: [{key, type, from or value}]}`: how to write the record |
| `contracts` | the 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:

```json
{"at":"2026-11-01T22:40:00Z","source":"CoinGecko","usd":0.0042,"usd24hChange":2.4}
```

| Field | Value |
|---|---|
| `usd` | the price of 1 L1 in US dollars, above 0 and at most 10⁹ |
| `usd24hChange` | the 24-hour change in percent, or `null` |
| `at` | when 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`.

| Field | Value |
|---|---|
| `version`, `chainId`, `block`, `time` | 1, the chain, the block read and its time (Unix seconds) |
| `generated` | when the file was written (ISO 8601 UTC) |
| `complete` | `false` while older history is still being read; the next run resumes |
| `wl1` | the wL1 address |
| `seriesTimes` | 28 Unix times 6 hours apart, then `time`: the points of every `series` |
| `tokens` | the rows below |

| Token field | Value |
|---|---|
| `address` | lowercase |
| `symbol`, `name` | cleaned, at most 24 and 64 characters |
| `decimals`, `supply` | decimals; total supply in base units, as a string |
| `priceL1`, `liquidityL1`, `marketCapL1` | price 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`, `price24hL1` | the 24-hour change in percent with 2 decimals, and the price then; `null` when unknown |
| `holders`, `holdersExact` | accounts holding it; `holdersExact` false means "at least"; `null` when its `balanceOf` fails |
| `series` | 29 prices in L1 (numbers) or `null`s 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` |
| `launchedBy` | that DAO's treasury, else `null` |
| `verified` | listed in `verifiedTokens`, launched by a featured DAO, or wL1 |
| `hidden` | listed in `hiddenTokens` |
| `wl1` | `true`, on wL1's row only |

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

```json
{"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](reading-the-chain.md#logos-and-descriptions)).

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:

```json
{"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}
```

| Field | Value |
|---|---|
| `version`, `chainId`, `factory`, `block`, `generated` | 1, 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 |
| `uniqueMembers` | accounts 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:

```json
{
  "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](release-build.md)).

## Related

- [Site settings](site-settings.md): what each setting does on the site.
- [Static pages, data files and prices](prerender.md): when the prerender writes these files.
- [Admin API reference](admin-api.md): how the admin page stores site.json and records.
- [Deploying COLLAB: reference](DEPLOY.md): config.js and the server configurations for static hosts.
- [Reading COLLAB from the chain](reading-the-chain.md): the chain reads behind these files.
