Skip to content

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_hash proves 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_KEY configured, the receipt becomes version: 2 and adds sig_alg: "hmac-sha256" plus signature = 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_SALT must 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 public salt_version, so that a salt rotation never breaks revocability (see below).
  • The receipt is reproducible. The receipt_hash is a SHA-256 over the canonical body (keys recursively sorted with stableStringify), 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_id and receipt_hash are that attestation's columns, and chain_anchor is 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):

FieldDerivation
receipt_hashSHA-256 hex over the canonical body — every other field, keys sorted.
contribution_idd_ + 24 hex: hash of (contributor_id, title, description, geographies, pillars, license, privacy, issuedAt, UUID nonce). Unique per pledge.
contributor_idc_ + 24 hex: sha256(salt + ":" + email). Stable, pseudonymous, irreversible.
points / points_breakdownTotal + decomposition {base, coverage, breadth, license, privacy}.
corpus_postureWhat the license allows in the corpus (the license gate, stamped up front).
salt_versions_ + 12 hex: a public label of which salt produced the id — a one-way hash that reveals nothing about the salt.
issued_at / versionIssuance ISO-8601 · schema version: 1 (hash only) or 2 (issuer-signed with HMAC).
chain_ready / chain_anchortrue · 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:

KeyStructureRole
trust:ledger:v1LISTAppend-only source of truth (RPUSH / LRANGE).
trust:revocations:v1LISTTombstones, in a sibling list — each list is single-type, so a typed read never mistakes a row.
trust:ledger:index:v1HASH id→recordO(1) lookup for revocation (HGET); best-effort — if it fails, the append does not fail.
trust:revocations:index:v1SETIdempotent revocation: SADD = 0 → already revoked, no-op.
trust:ledger:dedupe:v1SETPledge 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 via SADD; the contributor still receives a valid receipt, with a note that it was not duplicated. And the dedupe fails open: a store error returns fresh: true — a Redis hiccup never blocks a genuine contribution.
  • Rate and body limits. POST /api/contribute accepts 10 requests/minute per IP and a body of at most 64 KB with a two-stage guard (immediate rejection on Content-Length, plus byte counting during the read against a lying Content-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/contribute publishes aggregates over a bounded window of 500 records, labeled as such (stats.window {limit, complete}), and the public list deliberately omits the contribution_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 + matchingSalt in ledger.ts): the candidate salts are the current TRUST_ID_SALT, then each comma-separated entry of TRUST_ID_SALT_PREVIOUS, with the code's default salt as fallback. If the record carries a stamped salt_version, the candidate whose label matches is looked up and only that one must reproduce the contributor_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 HSET index (contribution_id → record) resolves the lookup with a single HGET without 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.

LicenseAction in the corpusWhat it allows
open / cc-by / share-alikeindicatorValues can be baked into the corpus and the public API.
non-commercialaggregates_onlyDerived aggregates only; never row-by-row republication.
cite-onlycitation_onlyCitation 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 gateLive/contribuir
Hash-verifiable receipt (reproducible SHA-256)Live
Append-only ledger + tombstone revocationLive (Upstash; honest when unconfigured)
License gate (cite-only never becomes a value)Live (build rule)
Transparent points rubricLive
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 signatureOn the roadmap — without it, issuance is proven by presence in the ledger
Custody by an independent trusteePhased — long-term, administration moves out of the operator
Operational privacy tiers / clean roomsPhased — the schema already models them; execution comes later
On-chain settlementPhased — schema ready (chain_anchor reserved); no chain yet
Sensitivity tiers + record encryptionPhased: today the privacy mode (PrivacyMode) is declared metadata only, not enforced; records are stored in plaintext
Zero-knowledge proof-of-recordPhased: no code yet; it would prove a record's presence in the ledger without revealing it
Community notes on contributionsPhased: no code yet
Contributor incentive charterPhased: 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.

Every figure with its source — traceability is the contract.