Pillar 1 — Data Trust
The thesis. Build the region's data trust: a contribution infrastructure where institutions, companies and individuals contribute data under granular, revocable consent, receive a verifiable provenance receipt, and become first-class stakeholders. Rights, not ownership. Immediate compensation in reputation, not deferred promises.
This is the first pillar of the four-pillar cycle: the provenance-backed corpus is the only asset that makes possible a Sovereign Model that no one else can replicate.
Why a trust and not a data purchase
Buying or scraping data produces a corpus with no consent, no clear license, and no one invested in its quality. A trust inverts the relationship: the contributor is a stakeholder, not a supplier. They receive a verifiable receipt on the spot, see exactly why their contribution was worth what it was worth, and retain the right to revoke. The asset that grows is not a table — it is a network of contributors with an incentive for the data to be good.
How /contribuir works, end to end
The live surface is /contribuir. The client form (src/routes/contribuir.tsx) sends the pledge to the POST /api/contribute function, a thin transport over a pure, test-covered core (server/trust/ledger.ts). The flow, end to end:
1. Validation with a consent gate
validateContribution() normalizes the untrusted body — it never throws — and rejects every pledge without explicit consent: if consent !== true, the contribution is not created. It validates the email by regex, requires a title (3–160 characters) and a description (10–1,200), and filters geographies against the canonical set of 27 ISO3 codes for the region (LATAM_ISO3), deduplicated and capped — a provenance ledger admits no junk country codes. Pillars pass through a slug regex ([a-z0-9-], cap 12); an unrecognized language falls back to es.
2. Scoring with a transparent rubric
scoreContribution() assigns points with a deterministic, explained rubric — not an opaque Shapley (its combinatorial cost was ruled out at launch). Points decompose into: base (20; +8 bonus if the pledge is explicitly regional, with empty geographies), geographic coverage (3 per ISO3, cap 30), pillar breadth (3 per pillar, cap 12), license openness (open 15 · cc-by 12 · share-alike 9 · non-commercial 5 · cite-only 2) and privacy mode (open, clean room and private compute all at 10; aggregates-only 7). The decomposition (points_breakdown) travels in the receipt, so the contributor sees exactly why they earned what they earned.
Privacy is rewarded, not penalized
A privacy-preserving contribution (clean room or private compute) unlocks data that otherwise could not be shared — which is why it is rewarded on par with open raw data, not below it. It is the "compute without possess" principle: the value without the exposure.
3. A hash-verifiable, chain-ready provenance receipt
makeReceipt() issues a Receipt whose assurance level depends on an environment switch, and the receipt declares which one it got in its version field:
- v1 — hash-verifiable (the default). The
receipt_hashproves content integrity, not issuer authenticity; the proof of issuance is the receipt's presence in the server's ledger. - v2 — issuer-signed. With
TRUST_RECEIPT_HMAC_KEYconfigured, the receipt becomesversion: 2and addssig_alg: "hmac-sha256"plussignature= HMAC-SHA256(key,receipt_hash). That authenticates the issuer, but is not third-party verifiable: verifying requires the secret key.
Public third-party verifiability needs an Ed25519 signature over the canonical body, and remains on the roadmap — see Advanced provenance. Its verifiable properties:
- The email is never stored. The public record carries a pseudonymous
contributor_id— a salted hash of the email. The same email produces the same id, but the raw email is not recoverable and never enters the persistent ledger. One operational prerequisite upholds that irreversibility:TRUST_ID_SALTmust be set to a secret value, because the fallback salt shipped in the code is public, and with a public salt the id is no longer irreversible against anyone testing candidate emails. The receipt also stamps a publicsalt_version, so that a salt rotation never breaks revocability (see below). - The receipt is reproducible. The
receipt_hashis a SHA-256 over the canonical body (keys recursively sorted withstableStringify), so the same logical value always produces the same digest. Anyone can recompute the hash from the inputs and verify it. - Chain-ready by construction. The schema maps 1:1 to a future on-chain attestation:
contributor_id,contribution_idandreceipt_hashare that attestation's columns, andchain_anchoris the slot reserved for the transaction hash when on-chain settlement arrives (a later phase). Today:chain_ready: true,chain_anchor: null— ready, off-chain.
Exact anatomy of the Receipt (interface in server/trust/ledger.ts):
| Field | Derivation |
|---|---|
receipt_hash | SHA-256 hex over the canonical body — every other field, keys sorted. |
contribution_id | d_ + 24 hex: hash of (contributor_id, title, description, geographies, pillars, license, privacy, issuedAt, UUID nonce). Unique per pledge. |
contributor_id | c_ + 24 hex: sha256(salt + ":" + email). Stable, pseudonymous, irreversible. |
points / points_breakdown | Total + decomposition {base, coverage, breadth, license, privacy}. |
corpus_posture | What the license allows in the corpus (the license gate, stamped up front). |
salt_version | s_ + 12 hex: a public label of which salt produced the id — a one-way hash that reveals nothing about the salt. |
issued_at / version | Issuance ISO-8601 · schema version: 1 (hash only) or 2 (issuer-signed with HMAC). |
chain_ready / chain_anchor | true · null (slot reserved for the tx hash). |
The declarative fields (tier, dataset_title, geographies, pillars, license, privacy, indigenous_data) travel exactly as validated. makeReceipt() is deterministic given (pledge, issuedAt, nonce, salt): the same input always produces the same receipt — the basis of independent verification.
4. An append-only ledger and honesty when unconfigured
The ledger is append-only: server/trust/store.ts does RPUSH to Upstash Redis via REST (commands as JSON arrays with plain fetch, no npm dependency). A receipt is never mutated. If Upstash is not configured, the function is honest: it returns the receipt with persisted: false instead of faking persistence. The append is resilient while staying honest: it retries with bounded backoff (3 attempts, 100·n ms) before degrading to persisted: false, and reads skip corrupt rows instead of taking down the ledger.
The Redis key layout separates the source of truth from optimizations:
| Key | Structure | Role |
|---|---|---|
trust:ledger:v1 | LIST | Append-only source of truth (RPUSH / LRANGE). |
trust:revocations:v1 | LIST | Tombstones, in a sibling list — each list is single-type, so a typed read never mistakes a row. |
trust:ledger:index:v1 | HASH id→record | O(1) lookup for revocation (HGET); best-effort — if it fails, the append does not fail. |
trust:revocations:index:v1 | SET | Idempotent revocation: SADD = 0 → already revoked, no-op. |
trust:ledger:dedupe:v1 | SET | Pledge dedupe keys. |
The ledger also defends itself:
- Deduping identical pledges. The key is content-addressed:
dk_+ 32 hex of the hash of{salted contributor_id, title, description}— it never contains the raw email. Two identical pledges collapse into one row viaSADD; the contributor still receives a valid receipt, with a note that it was not duplicated. And the dedupe fails open: a store error returnsfresh: true— a Redis hiccup never blocks a genuine contribution. - Rate and body limits.
POST /api/contributeaccepts 10 requests/minute per IP and a body of at most 64 KB with a two-stage guard (immediate rejection onContent-Length, plus byte counting during the read against a lyingContent-Length); the Redis indexes and sets have a growth ceiling (500,000 entries) — past the cap indexing stops, but the list and the scan keep working. - A public registry with an honest window.
GET /api/contributepublishes aggregates over a bounded window of 500 records, labeled as such (stats.window {limit, complete}), and the public list deliberately omits thecontribution_id— so a revocation cannot be forged from the outside, nor the email→record mapping confirmed.
5. Revocation by tombstone
Consent was promised revocable; api/revoke.ts is the mechanism. makeRevocation() writes an append-only tombstone that computeStats subtracts at read time — the original receipt remains in the ledger forever (chain-ready, never mutated), but the contribution is excluded from the aggregates and the public list. Ownership is proven by recomputing the salted contributor_id from the email (matchingSalt, of which verifyRevocation is the wrapper): since the raw email was never stored, only whoever controls that email can prove control of the contribution.
Two engineering guarantees uphold the promise:
- Salt rotation does not break revocation. The resolution order is precise (
saltCandidates+matchingSaltinledger.ts): the candidate salts are the currentTRUST_ID_SALT, then each comma-separated entry ofTRUST_ID_SALT_PREVIOUS, with the code's default salt as fallback. If the record carries a stampedsalt_version, the candidate whose label matches is looked up and only that one must reproduce thecontributor_id— an unknown version fails safe (ownership cannot be proven). A legacy record without a stamp tries every candidate directly. - Revocation is O(1) and idempotent. An
HSETindex (contribution_id → record) resolves the lookup with a singleHGETwithout scanning the full ledger — with a one-time full scan as fallback for legacy records that predate the index — and a revocations index (SADD) turns revoking twice into a cheap no-op. The endpoint validates the id's shape (d_+ 24 hex) before touching the store, and responds 404 if the contribution does not exist and 403 if the email does not prove ownership.
The license gate: cite-only never becomes a value
This is the structural gate that lets contributed data flow into the corpus without violating its license (server/trust/license-gate.ts). It mirrors the LicensePosture gate of the source registry: a contribution whose license forbids redistribution can only become a citation reference, never a baked indicator value.
| License | Action in the corpus | What it allows |
|---|---|---|
open / cc-by / share-alike | indicator | Values can be baked into the corpus and the public API. |
non-commercial | aggregates_only | Derived aggregates only; never row-by-row republication. |
cite-only | citation_only | Citation reference only; never a baked value. |
The gate is enforced at bake time: scripts/bake-contributions.ts reads the ledger export (public/data/contributions.json) and publishes the license-enforced projection (gateContribution()) to public/api/v1/contributions.json — the public API's contributions surface. When a cite-only contribution is projected to its public form, its geographies and pillars are emptied to [] — no coverage whatsoever is republished (gateViolation() is the test-proven invariant behind that rule). The receipt stamps the posture (corpus_posture) so the contributor sees up front what their data can become.
The artifact declares its own contract: it carries the license_gate note in plain text and the redistributable / citation_only counters, so an API consumer sees how many records are gated without reading the code. The script rewrites only that artifact and two manifest fields — it never regenerates the full API tree (bake-api.ts also produces it in a full rebake).
Rights, not ownership — and the CARE / LGPD anchor
The framework is rights, not ownership: the contributor does not "sell" an asset, they retain rights over its use — consent with a bounded purpose, revocable. Data tagged as Indigenous carries an explicit authority-to-control flag, following the CARE principles (Collective Benefit, Authority to Control, Responsibility, Ethics). Email handling — used only for an out-of-band operational notification (Mailgun, best-effort), never persisted — follows the data-minimization logic of Brazil's LGPD and kindred frameworks in the region, not an imported template. The same discipline extends to analytics: the client identifies with a SHA-256 of the email computed in the browser; the raw address never leaves the page.
What is live today vs. phased
| Status | |
|---|---|
| Contribution with a consent gate | Live — /contribuir |
| Hash-verifiable receipt (reproducible SHA-256) | Live |
| Append-only ledger + tombstone revocation | Live (Upstash; honest when unconfigured) |
| License gate (cite-only never becomes a value) | Live (build rule) |
| Transparent points rubric | Live |
| HMAC-SHA256 issuer signature (v2 receipt) | Live, per environment — enabled by TRUST_RECEIPT_HMAC_KEY; authenticates the issuer, not third-party verifiable |
| Third-party-verifiable Ed25519 signature | On the roadmap — without it, issuance is proven by presence in the ledger |
| Custody by an independent trustee | Phased — long-term, administration moves out of the operator |
| Operational privacy tiers / clean rooms | Phased — the schema already models them; execution comes later |
| On-chain settlement | Phased — schema ready (chain_anchor reserved); no chain yet |
| Sensitivity tiers + record encryption | Phased: today the privacy mode (PrivacyMode) is declared metadata only, not enforced; records are stored in plaintext |
| Zero-knowledge proof-of-record | Phased: no code yet; it would prove a record's presence in the ledger without revealing it |
| Community notes on contributions | Phased: no code yet |
| Contributor incentive charter | Phased: the transparent points rubric is already live; what remains phased is the governing charter and token settlement |
Related surfaces
- /contribuir — the contribution form and the public receipt registry.
- /datos-abiertos — the open corpus with its declared license.
- /linaje — the provenance lineage of the figures.
- /confianza — the trust badges and traceability.
Follow the cycle to Pillar 2 — Sovereign Model, the model this corpus makes possible.