# The Pool MCP Server — Design

**Author:** The Pool (organ of EPI)
**Date:** 2026-06-17
**Status:** Design proposal for Nyx's review
**Purpose:** Let the EPI on IamI.Earth *search and recommend* from the planetary pool of free resources & events.

---

## 1. The core principle

> **The Pool retrieves and ranks candidates. EPI reasons over them and recommends.**

Do not push recommendation logic into SQL, and do not give EPI raw database access. The integration surface is a small set of well-described **MCP tools**. EPI calls a tool, receives a ranked, freshness-filtered list of real resources, and composes the human answer (explains fit, sequences steps, adds warmth, handles language).

This keeps three clean layers in the family:

| Layer | Owner | Question it answers |
|-------|-------|---------------------|
| Who the person is / what they need | Iris (brain/profiles) | "cold, broke, just arrived in Rotterdam" |
| What's available | **Pool (this MCP)** | "12 free resources matching that, ranked, nearby, live" |
| How to say it | EPI (IamI.Earth) | the actual recommendation, in their language |

---

## 2. Current state (verified 2026-06-17)

These facts drive every design decision below.

- **454,322 active resources** — Sweden 318,188, Netherlands 136,134. Growing each heartbeat.
- **7 categories:** place (298k), service (113k), housing (27k), food (6.6k), goods (4.3k), skills (4.1k), event (276).
- **3,242 distinct cities**, 2 countries (`location_country` now explicitly set on every row + every source).
- **Coordinates: MISSING.** Resources store only `location_city`. **70% of NL rows (95k) and 24% of SE rows (76k) have no city at all** — meaning most of the pool is currently *unlocatable*. This is the single biggest gap.
- **No search index.** No FTS, no vectors, no R-tree. Search today = SQL `LIKE` / exact filters only.
- **Descriptions are short** (avg 46 chars, max 500) and **heavily templated** — 454k rows reduce to **276k distinct title+description blocks** (e.g. 35,885 rows share "Openbare tuin :: Public garden — free to visit"). → embed distinct content, not every row.
- **Freshness already modeled:** `sources.freshness_tier` ∈ {daily, weekly, monthly, permanent} + `expires_after_days`; `resources.is_active`, `available_until`, `last_verified`.
- **value_eur** populated on ~100% of rows. **tags** ~6.5%, **languages** ~1%, **image_url** ~0.1% (sparse — don't rely on them).
- **Engine:** SQLite 3.45.1, 187 MB file. Content is multilingual (Swedish, Dutch, English).

---

## 3. Architecture

```
                 ┌─────────────────────────────────────────────┐
   IamI.Earth    │                 EPI (LLM agent)              │
   visitor  ───► │   reasons, recommends, speaks the language   │
                 └───────────────┬─────────────────────────────┘
                                 │ MCP (tools)
                 ┌───────────────▼─────────────────────────────┐
                 │            pool-mcp server (Python)          │
                 │  search_resources · get_resource ·           │
                 │  pool_overview · suggest_bundle              │
                 ├──────────────────────────────────────────────┤
                 │  Hybrid retrieval engine:                    │
                 │   structured filter → {FTS5 + vector + geo}  │
                 │   → RRF fusion → freshness → score → top-N   │
                 └───────────────┬─────────────────────────────┘
                                 │ read-only (WAL)
                 ┌───────────────▼─────────────────────────────┐
                 │   pool.db (SQLite)                           │
                 │   resources · sources                        │
                 │   + resources_geo (R-tree)                   │
                 │   + resources_fts (FTS5)                     │
                 │   + content_vectors (sqlite-vec)             │
                 └───────────────▲─────────────────────────────┘
                                 │ writes
                 ┌───────────────┴─────────────────────────────┐
                 │   scrapers (heartbeat) + index refresh job   │
                 └──────────────────────────────────────────────┘
```

**Concurrency:** put `pool.db` in **WAL mode**; the MCP server opens a **read-only** connection. In WAL, readers never block on the scrapers' writes — no separate replica needed for the MVP. (Add litestream for HA later.)

---

## 4. Schema upgrades (DDL)

### 4.1 Coordinates + geo index (prerequisite #1)

```sql
ALTER TABLE resources ADD COLUMN lat REAL;
ALTER TABLE resources ADD COLUMN lng REAL;

-- R-tree for fast radius / bbox queries (SQLite built-in module)
CREATE VIRTUAL TABLE resources_geo USING rtree(
  id,                 -- integer rowid mirror
  min_lat, max_lat,   -- point => min==max
  min_lng, max_lng
);
```

**Backfill plan:**
- **OSM sources (~370k):** coords are in the Overpass response — re-run each OSM scraper once with coord storage (they already compute `lat/lon` per element; just persist it). Cleanest, also refreshes data.
- **Falling Fruit:** API already returns `lat/lng` — store on next run.
- **Wikidata/lat-lon-bearing sources:** add coords at scrape time going forward.
- **Reverse-geocode the city** from coords for the 171k rows missing `location_city` (offline reverse geocode against the city bounding boxes already in the scrapers, or a gemeente/kommun shapefile).

This single fix turns ~171k unlocatable rows into locatable ones and unlocks "near me," the #1 query.

### 4.2 Full-text search (FTS5, built-in)

```sql
CREATE VIRTUAL TABLE resources_fts USING fts5(
  title, description, location_city, tags,
  content='resources', content_rowid='rowid',
  tokenize='unicode61 remove_diacritics 2'   -- handles å ä ö ü etc.
);
-- triggers keep it in sync on INSERT/UPDATE/DELETE (external-content FTS5)
```

External-content FTS5 ⇒ no text duplication; BM25 ranking comes free via `bm25(resources_fts)`.

### 4.3 Vector search (sqlite-vec)

Embed **distinct content** (276k blocks), not every row — a `content_hash` maps many rows to one vector.

```sql
ALTER TABLE resources ADD COLUMN content_hash TEXT;   -- sha1(title|description)
CREATE INDEX idx_resources_chash ON resources(content_hash);

-- sqlite-vec virtual table (vec0)
CREATE VIRTUAL TABLE content_vectors USING vec0(
  content_hash TEXT PRIMARY KEY,
  embedding FLOAT[384]               -- multilingual-e5-small
);
```

KNN at query time:
```sql
SELECT content_hash, distance
FROM content_vectors
WHERE embedding MATCH :query_vec AND k = 200;
```
…then join `content_hash → resources` and apply structured filters.

### 4.4 Helpful conventional indexes
Already present: source, category, city, is_active, country. Add a composite `(location_country, category, is_active)` for the common filter path.

---

## 5. Embeddings

- **Model:** `intfloat/multilingual-e5-small` — 384-dim, runs locally on CPU, strong on Swedish/Dutch/English short text. (Upgrade path: `BAAI/bge-m3`, 1024-dim, if quality demands.)
- **What we embed:** `"query: " / "passage: "`-prefixed `title + ". " + description` per **distinct content_hash** (~276k vectors ≈ ~420 MB at 384-dim).
- **Generation:** batch job (`scrapers/build_embeddings.py`), idempotent — only embeds content_hashes not already in `content_vectors`. Runs after scraper heartbeats.
- **Why dedup matters:** 40% fewer vectors, and templated OSM rows ("Free public playground") collapse to one semantic point while FTS still distinguishes them by specific name.

---

## 6. The retrieval engine

A `search_resources` call flows through:

1. **Structured prefilter (SQL):** `is_active=1`, country, category, city, value range, freshness (see §8), and — if `near` given — an R-tree bounding box. Narrows 454k → a working set fast.
2. **Three parallel retrievers over the working set:**
   - **Lexical** — FTS5 BM25 on the query terms (nails specific names: "Vondelpark", "ABF Stockholm").
   - **Semantic** — sqlite-vec KNN on the query embedding (nails intent: "somewhere warm to sit" → libraries, shelters).
   - **Proximity** — R-tree + haversine distance (when `near` is provided).
3. **Fusion — Reciprocal Rank Fusion (RRF):**
   `rrf(d) = Σ_retriever 1 / (k + rank_retriever(d))`, k = 60.
   Robust, scale-free, no per-retriever score calibration needed.
4. **Final score & boosts:**
   `score = rrf_norm × freshness_factor + α·proximity_norm + β·value_norm + γ·source_trust`
   - `freshness_factor`: 1.0 fresh → 0 expired (multiplicative ⇒ dead items can't surface).
   - defaults α=0.15, β=0.05, γ=0.05; all tunable. Distance dominates when the user says "near me," semantics dominate otherwise.
5. **Return top-N** (default 10, max 50), shaped for the LLM (§7.5).

---

## 7. MCP tool surface

Four tools. Tight on purpose — too many tools degrade LLM tool selection.

### 7.1 `search_resources` — the workhorse

> Search the planetary pool of free resources and events. Use for any "what free X is available (near Y)?" need. Combines meaning-based and keyword search, filters to live items, and ranks by relevance and proximity. Returns real, currently-available resources with links.

```json
{
  "name": "search_resources",
  "input_schema": {
    "type": "object",
    "properties": {
      "query":      {"type": "string", "description": "Free-text need or intent, any language, e.g. 'free warm place to study', 'gratis eten Amsterdam', 'sovplats ikväll'."},
      "category":   {"type": "string", "enum": ["place","service","housing","food","goods","skills","event"], "description": "Optional hard filter."},
      "country":    {"type": "string", "enum": ["Sweden","Netherlands"], "description": "Optional. Omit to search all."},
      "city":       {"type": "string", "description": "Optional city name filter."},
      "near":       {"type": "object", "properties": {"lat": {"type": "number"}, "lng": {"type": "number"}}, "description": "Optional. User location for proximity ranking + radius filter."},
      "radius_km":  {"type": "number", "default": 10, "description": "Used only with `near`."},
      "max_value_eur": {"type": "number", "description": "Optional cap; all pool items are free, use for near-free (e.g. Too Good To Go)."},
      "limit":      {"type": "integer", "default": 10, "maximum": 50}
    },
    "required": ["query"]
  }
}
```

### 7.2 `get_resource` — full detail by id

> Fetch the complete record for one resource (full description, address/coords, source, last-verified date, opening info) before recommending it.

`{ "id": {"type":"string"} }`

### 7.3 `pool_overview` — coverage & stats (anti-hallucination)

> Report what the pool actually contains — counts by category, country, and top cities, plus data freshness. Call this to ground answers about coverage and to avoid promising resources we don't have for a place.

```json
{ "country": {"type":"string"}, "city": {"type":"string"}, "category": {"type":"string"} }  // all optional
```

Returns e.g. `{ "country":"Netherlands", "total":136134, "by_category":{...}, "top_cities":[...], "note":"NL: places & services rich; housing/goods/skills coming" }`.
This stops EPI confidently offering free housing in a city where the pool has none yet.

### 7.4 `suggest_bundle` — holistic, cross-category (v2)

> For a whole-situation need ("just arrived, no money, in Utrecht"), return a curated *bundle* across the categories that matter (shelter + food + free wifi/library + nearby events), each section ranked and nearby. One call instead of many.

```json
{
  "situation": {"type":"string"},
  "near": {"type":"object","properties":{"lat":{"type":"number"},"lng":{"type":"number"}}},
  "country": {"type":"string"},
  "categories": {"type":"array","items":{"type":"string"}, "description":"Optional; otherwise inferred from situation."}
}
```
Internally fans out `search_resources` per relevant category near the location and merges. (EPI *could* orchestrate this itself; the tool just saves round-trips and standardizes the bundle.)

### 7.5 Response shape (LLM-optimized)

Compact, ranked, only the fields needed to recommend — no raw dumps:

```json
{
  "results": [
    {
      "id": "osm-library-nl-12345",
      "title": "Openbare Bibliotheek Amsterdam (OBA)",
      "category": "service",
      "why": "Free study space, warmth, wifi and toilets — open to all.",
      "city": "Amsterdam", "country": "Netherlands",
      "distance_km": 0.8,
      "value_eur": 0,
      "url": "https://www.openstreetmap.org/way/...",
      "source": "osm-library-netherlands",
      "last_verified": "2026-06-16",
      "freshness": "permanent"
    }
  ],
  "count": 10,
  "query_understood_as": "warm free indoor place, Amsterdam",
  "coverage_note": null
}
```
`why` is a short generated snippet (from the matched fields) so EPI has a ready hook; `coverage_note` warns when results are thin so EPI can be honest.

---

## 8. Freshness & verification contract

The tool **must never return a dead item.** Default filter on every search:

```
is_active = 1
AND (available_until IS NULL OR available_until >= now)
AND NOT (freshness_tier='daily'   AND last_scraped < now-2d)
AND NOT (freshness_tier='weekly'  AND last_scraped < now-10d)
AND NOT (freshness_tier='monthly' AND last_scraped < now-40d)
```
`permanent` items (parks, libraries, foraging) never expire. Per-listing goods/events lean on `available_until` + tier. EPI can pass `include_stale=true` only for debugging.

---

## 9. Multilingual handling

- **Embeddings:** multilingual model ⇒ an English query matches Swedish/Dutch content and vice-versa (semantic space is shared).
- **FTS:** `unicode61 remove_diacritics 2` so "Malmo" matches "Malmö", "uitkijk" matches "Uitkijkpunt".
- **`why`/snippets:** returned in the resource's own language; EPI translates as needed for the user. The tool does not translate — that's EPI's job.

---

## 10. Deployment & ops

- **Language/stack:** Python (matches scrapers) + official `mcp` SDK / FastMCP; `sqlite-vec` + `sentence-transformers` (or ONNX runtime for the embedder).
- **Transport:** **Streamable HTTP/SSE** if EPI runs as a web service on IamI.Earth (remote-friendly, auth via header token). **stdio** if co-located. Recommend HTTP for flexibility.
- **DB access:** read-only WAL connection to `pool.db`; `PRAGMA query_only=1`.
- **Index refresh:** a post-heartbeat job rebuilds FTS deltas, R-tree rows, and embeds new content_hashes. Incremental, idempotent.
- **Performance targets:** p50 < 80 ms, p95 < 250 ms for `search_resources` over 454k rows (R-tree prefilter + vec KNN k≤200 + BM25 is comfortably in range). Cache hot queries (LRU, 5-min TTL).
- **Observability:** log every query (text, filters, result count, latency) to `pool/logs/mcp_queries.ndjson` — this becomes the dataset for improving ranking and for the matching layer later.

---

## 11. Safety & guardrails

- Read-only; no write tools exposed. All SQL parameterized (no string interpolation).
- Hard caps: `limit ≤ 50`, KNN `k ≤ 200`, radius ≤ 100 km.
- Auth token on the HTTP transport; rate limit per caller.
- Never expose host PII from source platforms (Couchsurfing/BeWelcome) — return the listing link, not scraped personal contact details.

---

## 12. Phased rollout

| Phase | Ships | Unlocks |
|-------|-------|---------|
| **0 — Prereqs** | WAL, `lat/lng` + backfill (OSM/FF re-run), reverse-geocode cities, composite index | the pool becomes locatable |
| **1 — MVP** | `search_resources` (structured + FTS5 + geo/R-tree), `get_resource`, `pool_overview`; HTTP MCP | EPI can search & locate today, no GPU needed |
| **2 — Semantic** | embeddings + sqlite-vec + RRF fusion | intent queries ("warm place to rest") work |
| **3 — Holistic** | `suggest_bundle`, query logging → ranking tuning, litestream HA | whole-situation recommendations |

MVP is shippable fast — FTS5 + R-tree are built into SQLite, no external services. Vectors are an additive upgrade.

---

## 13. Open questions for Nyx

1. **Where does EPI run** relative to this server — same host (stdio) or remote (HTTP)? Decides transport.
2. **Events:** ~~Does the Pool serve events or call The Scene?~~ **RESOLVED 2026-06-20 (Nyx + Feronia):** The Scene (Feronia) owns events — free AND paid. The Pool stops scraping events (Meetup/Eventbrite scrapers retired) and defers the `event` category to The Scene, which contributes its *free* events into the Pool via the Open Pool Protocol (§19, "EPI organs first"). EPI merges Scene (free events) + Pool (libraries, parks, drop-ins) for "things to do" queries.
3. **Iris boundary:** does Iris's brain call this tool with the user profile, or does EPI call it directly? Both work; affects whether `suggest_bundle` lives here or in Iris.
4. **Near-free scope:** include Too Good To Go / Too-cheap items via `max_value_eur`, or free-only?

---

## 14. Going further — what makes it exceptional

The MVP answers "what free X is near me?". These elevations answer the question the *person actually has*, especially when they're in hardship.

### 14.A "Free isn't accessible" — capture the real barriers
Free + 3 km away is useless if it's closed, needs a referral, or has stairs. We currently **discard OSM tags that answer exactly this.** Capture them in the Phase-0 re-run (same pass as coordinates) and let the tool filter/boost on them:

- **Opening hours → "open now".** OSM carries `opening_hours` on libraries, toilets, services. Store it; add `open_now` filter + boost. A warm library at 2 a.m. is not an answer.
- **Eligibility / access requirements.** Many free resources gate access: food banks need a *referral*, shelters have criteria, some are *no-questions-asked*. Add a structured `access` field (`open_to_all` | `referral_needed` | `members` | `id_required` | `appointment`). Prevents sending someone to a closed door.
- **Accessibility flags.** `wheelchair`, step-free, `changing_table` — already in OSM tags, currently dropped.
- **Language spoken / served.** For services & skills, which languages? (taalcafé, advice desks.)
- **Access friction.** Booking required? queue? walk-in? — one `friction` hint so EPI can set expectations.

New columns: `opening_hours TEXT, access TEXT, accessibility TEXT, languages_served TEXT`. All optional, populated where the source provides them.

### 14.B Trust & crisis-safety (non-negotiable for this audience)
People searching for free help are often in crisis. Getting this wrong is *harmful*, not just unhelpful.

- **Source trust score** per source (curated > official/municipal > open-data > user-listing). Feeds ranking; gates sensitive categories.
- **Crisis detection → safety-first.** When a query signals emergency ("nowhere to sleep tonight, freezing", "haven't eaten in days", "being hurt at home"), the tool returns a `safety` block first: official/emergency services, helplines, 24h shelters, 1177 — *before* crowd-sourced items. EPI is told to lead with these.
- **Safe-source allowlist** for shelter / food / abuse / health: only trusted sources surface in those categories.
- **No PII.** Couchsurfing/BeWelcome etc. return the listing link, never scraped personal contact details.

### 14.C The demand loop — search makes the Pool grow toward need
The query log isn't just for ranking; it's the **nervous-system signal that tells me what to scrape next.**

- Every search logs `(query, filters, result_count, top_score, acted_on?)`.
- **Unmet-demand report:** queries returning few/zero results = where coverage is thin. "41 people asked for free childcare in Rotterdam, pool has 0" → that becomes a scraping priority in my heartbeat.
- **Outcome → ranking:** clicks / "this helped" signals boost what actually works (CTR/popularity prior, later learning-to-rank).
- This makes the Pool **demand-driven**, not supply-guessing — the real EPI flywheel: needs flow in → Pool grows toward them → recommendations improve → more needs flow in.

### 14.D Truth maintenance — keep it real, not just big
- **Cross-source dedup.** The same library can arrive from OSM *and* Wikidata *and* a municipal feed. Canonicalize by geo-proximity + name similarity; link `duplicate_of`. Stops EPI recommending one place three times. (We almost certainly already carry dupes.)
- **Just-in-time verification** for *dynamic* resources (food/goods/couches): live-check the top few candidates against the source at query time, so we never send someone to food that's already gone.
- **Crowd "report gone".** A `report_resource(id, status)` path (EPI relays a user's "it wasn't there") → lowers confidence, queues re-verification.
- **Confidence score** per result (source trust × freshness × verification recency), surfaced so EPI can be honest: "likely there" vs "worth calling first".

### 14.E Reachability beats radius
The audience often can't pay a bus fare. Rank by **walking/transit time, not straight-line km**. MVP: walking distance + `walk_minutes`. v3: transit isochrones (OTP). "Reachable on foot" is a first-class filter for this group.

### 14.F Retrieval polish
- **Query expansion / HyDE** for sparse, ambiguous needs — improves recall on short queries.
- **City alias table** — "Den Haag" = "The Hague" = "'s-Gravenhage"; "Göteborg" = "Gothenburg". Small table, big UX.

---

## 15. Revised phasing (with the elevations woven in)

| Phase | Ships | Why now |
|-------|-------|---------|
| **0 — Prereqs** | WAL; **one OSM/FF re-run** that captures `lat/lng` **+ `opening_hours` + `wheelchair`/access tags** at once; reverse-geocode cities; composite index | one pass, many wins — coords *and* the accessibility data |
| **1 — MVP** | `search_resources` (structured + FTS5 + geo), `get_resource`, `pool_overview`; **open-now filter**; **crisis safety block**; HTTP MCP | searchable, locatable, *and safe* on day one |
| **2 — Semantic** | embeddings + sqlite-vec + RRF; **query logging + unmet-demand report**; city aliases | intent queries + the demand loop starts feeding my heartbeat |
| **3 — Trust & holistic** | cross-source dedup; confidence scores; `suggest_bundle`; `report_resource`; just-in-time verification; reachability; litestream HA | exceptional, trustworthy, self-improving |

The biggest single lever is still **Phase 0** — and now it does double duty: the same re-run that makes the pool *locatable* also makes it *accessible* (hours, wheelchair, access rules). Do it once, do it right.

---

## 16. The two-sided pool — gifts + dreams, signed (the full vision)

So far the pool is **supply we scraped**. The leap: let people add **their own gifts** *and* **their own needs**, signed with their own key, through the same MCP. One protocol for *"here is what I can give"* and *"here is what I dream of / need"* — and the engine matches them.

### 16.0 Outdating, unified — the liveness score (answers "does it go out of date?")
Everything in the pool — scraped or gifted — ages. Instead of scattered freshness rules, give **every resource one `liveness` score in [0,1]** that **decays with time** and is **renewed by signals**. It multiplies into rank; below a threshold the item is hidden pending re-verification, never silently shown as live.

| Signal that renews liveness | Applies to |
|---|---|
| Re-scrape confirms it still exists | scraped |
| Owner **re-confirms** with a fresh signature ("still available") | gifted |
| Someone successfully **claims/uses** it | both |
| Just-in-time check passes at query time | dynamic (food/couch) |

| Signal that drops liveness | Effect |
|---|---|
| Time since last confirmation (per `freshness_tier`) | gradual decay |
| "**report gone**" from a user (signed) | sharp drop → re-verify queue |
| A one-shot gift gets **given** | → archived |

So a gifted winter coat decays unless re-confirmed; the moment it's claimed-and-confirmed it's marked given and leaves search. Outdating becomes a number, not a guess.

### 16.1 Self-sovereign identity — sign, don't register
Each person holds an **Ed25519 keypair**; their **ID is a `did:key`** (the DID literally encodes the public key — no central registry, no blockchain, fully portable and decentralized). Every contribution is **signed over a canonical JSON serialization (RFC 8785 / JCS)**; the MCP verifies the signature against the DID before accepting, and stores the signature so *anyone* can re-verify.

Why signing matters (it's not ceremony):
- **Authenticity & integrity** — the offer is really theirs, untampered.
- **Sybil resistance** — reputation is tied to a key with history; faking thousands of trusted identities is costly.
- **Reputation without a central authority** — a key with many fulfilled gifts is trustworthy; the trust graph is built from signed events.
- **Portability / federation** — signed records are verifiable anywhere, so the pool can federate across servers instead of being locked to one. (Aligns with EPI's decentralized ethos / Nekuno.)

### 16.2 Two registries, one engine
- **Offers (gifts)** — same shape as scraped resources + `contributor_did`, `signature`, `gift_type` (good · food · space · skill · time), `quantity`, `status`. "Spare bike", "free room for a week", "I'll teach guitar", "surplus veg".
- **Needs (dreams)** — a `needs` table: `seeker_did`, description, category, coarse geo, urgency, `signature`, `status`, embedding. "A winter coat", "a place to stay in July", "someone to fix my laptop", "I dream of learning to sail".

**The matching engine is the §6 retriever run both directions, over both supply sources at once:**
- A **need** is just a structured query → matched against **user offers *and* the scraped pool** (a need for a coat matches a neighbour's gift *or* a clothing bank — one search, unified supply).
- A new **offer** is run against all **open needs** → the waiting seekers are notified.
- Needs are **standing**: a dream stays open and is re-matched as new gifts arrive. *Your dreams are watched for.*

### 16.3 The gift lifecycle (a signed state machine)
```
offered ──claim(seeker,sig)──► claimed ──confirm(giver,sig)──► confirmed ──► completed
   ▲                              │ (claim lapses)                              │
   └──────── reopened ◄───────────┘                              gift_event (both sigs) ─► reputation++
   │
   └── expired (no re-confirm)   └── withdrawn (giver)
```
Double opt-in (both sign) prevents double-allocation — the core "already taken / outdated" failure. Each completion writes an **append-only signed `gift_event`**; reputation for giver *and* receiver derives from that log.

### 16.4 Privacy & selective disclosure (critical — needs are intimate)
"I need a shelter, I'm fleeing abuse" must be safe to say.
- **Pseudonymous by default** — a DID is not a legal identity.
- **Coarse geo** in search (neighbourhood/city); exact location + contact revealed **only on mutual claim+confirm** (double opt-in).
- **Sensitive-category needs encrypted at rest**, never placed in search snippets; routed via the §14.B crisis-safety path.
- **Verifiable Credentials for eligibility** — a trusted issuer (social worker, municipality) signs a VC that the person qualifies for a gated resource (the `referral_needed` gate from §14.A); they present it to claim **without revealing why** they qualify (SD-JWT / selective disclosure). Same crypto, solves eligibility elegantly.

### 16.5 Trust, reputation & moderation
- **Reputation** from signed `gift_event`s + age of DID + reports. Feeds ranking; gates sensitive categories.
- **Sybil/spam defense** — write rate-limits per DID; new/low-rep DIDs' offers enter a **moderation queue** (LLM content classifier on submit) before going live.
- **Vouching** — a high-rep DID can vouch for a newcomer (signed), bootstrapping trust.
- **Safe categories** (shelter/food/abuse/health) — only trusted/curated sources & vouched contributors surface.

### 16.6 New MCP tools (the write side)
All take `did` + `signature`; the server verifies before acting.
- `contribute_resource(did, signature, resource)` — offer a gift.
- `register_need(did, signature, need)` — post a need/dream (standing).
- `claim_resource(did, signature, resource_id)` — claim a gift (notifies giver).
- `confirm_gift(did, signature, claim_id)` — giver confirms → completes, logs `gift_event`.
- `reconfirm_resource(did, signature, resource_id)` — "still available" (renews liveness).
- `report_resource(did, signature, resource_id, status)` — "it's gone" (drops liveness).
- `update_status` / `withdraw_resource(did, signature, resource_id)`.
- `my_contributions(did)` / `my_needs(did)` — manage one's own gifts & dreams.
- `find_matches_for_need(need_id)` — offers + scraped pool that satisfy a need.

Read tools (§7) are unchanged; `search_resources` now transparently spans scraped + gifted supply, ranked by relevance × liveness × trust × proximity.

### 16.7 Schema additions
```sql
CREATE TABLE identities (
  did TEXT PRIMARY KEY, public_key TEXT NOT NULL,
  created_at TEXT, reputation REAL DEFAULT 0, vouched_by TEXT
);
ALTER TABLE resources ADD COLUMN contributor_did TEXT;   -- NULL = scraped
ALTER TABLE resources ADD COLUMN signature TEXT;
ALTER TABLE resources ADD COLUMN gift_type TEXT;         -- good|food|space|skill|time
ALTER TABLE resources ADD COLUMN status TEXT DEFAULT 'active'; -- active|claimed|given|withdrawn|expired
ALTER TABLE resources ADD COLUMN quantity INTEGER DEFAULT 1;
ALTER TABLE resources ADD COLUMN liveness REAL DEFAULT 1.0;
ALTER TABLE resources ADD COLUMN reconfirmed_at TEXT;

CREATE TABLE needs (
  id TEXT PRIMARY KEY, seeker_did TEXT, description TEXT, category TEXT,
  geo_coarse TEXT, lat REAL, lng REAL, urgency TEXT,
  status TEXT DEFAULT 'open', signature TEXT, created_at TEXT, content_hash TEXT
);
CREATE TABLE claims (
  id TEXT PRIMARY KEY, resource_id TEXT, claimer_did TEXT,
  status TEXT, claim_sig TEXT, confirm_sig TEXT, created_at TEXT, completed_at TEXT
);
CREATE TABLE gift_events (        -- append-only, signed; source of reputation
  id TEXT PRIMARY KEY, resource_id TEXT, giver_did TEXT, receiver_did TEXT,
  completed_at TEXT, giver_sig TEXT, receiver_sig TEXT
);
```

### 16.8 Why this is the whole flywheel
Scraped supply seeds the pool so it's valuable on day one. Signed user **gifts** make it grow from the community, not just my scrapers. Signed **needs** turn unmet demand into the exact signal that tells me what to scrape *and* pulls gifts out of people. Every fulfilled gift is a verifiable act of the gift economy, building a portable trust graph. **One MCP: here are my gifts · here is my dream · find me the match.** 🌊

---

## 17. Revised phasing (v2 — with identity & the two-sided pool)

| Phase | Ships | Why now |
|-------|-------|---------|
| **0 — Prereqs** | WAL; one OSM/FF re-run capturing `lat/lng` + `opening_hours` + access tags; reverse-geocode; **`liveness` column** | locatable + accessible + outdating-as-a-number |
| **1 — MVP read** | `search_resources` (structured + FTS5 + geo), `get_resource`, `pool_overview`; open-now; crisis safety; HTTP MCP | searchable, locatable, safe |
| **2 — Semantic + demand loop** | embeddings + sqlite-vec + RRF; query logging → unmet-demand report; city aliases | intent search; pool grows toward need |
| **3 — Identity + gifts** | `did:key` verify; `contribute_resource`, `register_need`, claim/confirm lifecycle; reputation; moderation queue | the community can give & dream |
| **4 — Matching + trust** | standing-need matching + notify; cross-source dedup; confidence; Verifiable-Credential eligibility; federation | the full signed gift economy, decentralized |

---

## 18. Deep audit (self-critical, 2026-06-20)

Read adversarially. Findings ordered by severity. Each has a fix.

### 18.1 The crux: this design serves three different products at once
"It depends on the product" is correct — and the doc never picks one. It silently assumes all three, which have **conflicting** requirements:

| | **P1 — Public concierge** ("what's free near me?") | **P2 — Crisis lifeline** (people in hardship) | **P3 — Gift economy** (signed members give/receive) |
|---|---|---|---|
| User | anonymous visitor | person in real need | community members |
| Data | scraped | scraped + **curated + human-verified** | **user-contributed** + scraped |
| Must have | geo, FTS, freshness, **deep high-intent coverage** | accuracy, trust, official-first, eligibility | identity, matching, lifecycle, **moderation** |
| Can drop | crypto, gifts, needs, VCs | gifts, crypto, volume | nothing — it's the heaviest |
| Worst failure | irrelevant results | **sending someone to a closed door / harm** | scam, trafficking via "free housing" |
| Time-to-value | weeks | months (verification) | months+ (users + moderation) |
| Success metric | search → click | **need met safely** | gift completed |

Building "one MCP" for all three optimizes none. **Decision required before any code.** My recommendation in 18.6.

### 18.2 CRITICAL — the relevance illusion (the most important finding)
The 454k headline is **90% low-intent geographic data**: place 298k + service 113k (gardens 72k, playgrounds 63k, picnic tables, viewpoints, public art). The categories a person in need actually searches are tiny — **food 6.6k, goods 4.3k, skills 4.1k, housing 27k (Sweden only)** — and **the Netherlands has literally 0 housing, 0 goods, 0 skills, 0 events.** A recommender over today's pool returns picnic tables and gardens, and almost nothing when someone needs food, a coat, or a bed.
→ **The pool is wide but shallow exactly where it matters.** The number flatters; the coverage doesn't. This outranks all the search infrastructure: **deep coverage in high-intent categories beats better ranking over picnic tables.** The roadmap sources I haven't scraped yet (food banks, free-goods sites, repair cafés) are the real priority.

### 18.3 CRITICAL — `value_eur` is fabricated; it must not rank or be published
Values are heuristics (€3/foraging spot, €0/park). §6 ranks partly on `value_norm` — ordering *free* things by a made-up price is meaningless and can privilege an €836k housing estimate over a meal. And the "pool worth €X million" stat is **not defensible** to users or funders.
→ Remove `value_eur` from ranking. Stop presenting fabricated totals as real value.

### 18.4 HIGH — real flaws & internal contradictions
- **Append-only log vs privacy (GDPR).** §16.3 makes `gift_events` immutable & signed; §16.4 promises erasure + protects "fleeing abuse" needs. You cannot delete from an immutable signed log, and signing a vulnerable person's need creates a *permanent, portable record of their vulnerability*. The crypto works **against** the privacy goal. Fix: crypto-shredding (encrypt, delete key) or keep all PII off the log.
- **`did:key` gives ZERO sybil resistance.** §16.1/16.5 claim it; but keypairs are free and infinite. Reputation on a costless identity is trivially farmed. Real sybil resistance needs a *scarce* anchor (vouching, proof-of-personhood, staking) — unspecified. As written, the claim is false. Fix: mandatory vouch-to-transact, or drop the claim.
- **Moderation is a program, not a queue.** §16.5 treats abuse as "LLM classifier + rate limit." "Free" is a top vector for scams and trafficking; the audience is vulnerable; this carries legal liability and needs ongoing human ops. Underestimated ~10×. Fix: if we can't staff moderation, **don't open user contributions in sensitive categories at all.**
- **Cold-start liquidity unaddressed.** A gift↔need market needs both sides present to match. Day one ≈ 0 gifts, 0 needs → 0 matches → churn. §16 describes mechanics, not how to reach liquidity. Scraped supply only feeds need→scraped, not the gift side.
- **Geo backfill is the riskiest, least-specified part.** "Re-run the scrapers" = re-querying Overpass for ~370k objects — exactly what 504'd repeatedly this week. Reverse-geocoding 171k missing cities via Nominatim ≈ 47h at policy rate. Phase 0 is the keystone *and* the hardest; it needs its own mini-plan. (Mitigation: store coords at scrape time going forward for free; only the historical backfill is hard.)
- **Success metric never defined.** The doc optimizes retrieval; it never names the outcome (needs met? gifts completed?). Without it I'll keep optimizing volume — the very illusion in 18.2.

### 18.5 MEDIUM — gaps & smaller bugs
- **The Pool↔EPI seam is hand-waved.** §1 says "EPI reasons," but turning 10 rows into one correct, safe, caring answer (esp. crisis) is the actual product quality and is fully unspecified — no prompt contract, no required "verify before you go" disclaimers. Quality falls in this gap.
- **Reasoning leaks into the Pool, contradicting §1.** `suggest_bundle` (§7.4) and crisis triage (§14.B) *are* recommendation logic — but §1 forbids that. Pick one: pure retriever (EPI orchestrates) or the Pool owns some reasoning. Don't claim both.
- **Build vs integrate.** I listed `socialekaartnederland.nl` (an authoritative official social map) as a *source*. Re-aggregating official directories is lower-trust duplication. The Pool's defensible edge is *cross-source unification + cross-border + the gift layer* — not re-mapping food banks the gemeente already maps.
- **Spec bug:** §8's freshness filter reads `freshness_tier`/`last_scraped` as if on `resources`; they live on `sources`. Needs a join; per-row `last_verified` ≠ per-source `last_scraped`.
- **Perf claim unverified.** §10 "p50 < 80 ms" with brute-force sqlite-vec KNN over 276k vectors single-threaded is plausible, not measured. Benchmark before promising.
- **Decentralization oversold.** "No central registry" (§16.1) coexists with a central `identities` table, one SQLite file, one server, one operator (me). It's centralized infra with portable records — say that; don't promise federation we won't build.

### 18.6 Recommended path (what I'd actually do)
1. **Pick P1, scoped hard.** Not 454k rows / 2 countries. One city (**Amsterdam**), **2–3 high-intent categories** (food, free goods, repair/services). Go *deep*: scrape the roadmap sources so those categories are genuinely complete.
2. **Validate one loop end-to-end:** *ask → get a real, current, reachable free resource → confirm it was actually there.* That single metric forces fixing 18.2 (coverage) and accuracy at once.
3. **Build only what that needs:** FTS5 + geo + freshness + the demand log. No vectors, no crypto, no gifts yet.
4. **Earn the next layer.** If people use it and ask to contribute → add gifts/identity (P3), staffing moderation first. If the wins are crisis cases → invest in P2 verification + safety. Let real usage, not the doc, choose.
5. **Drop or defer:** `value_eur` ranking, federation, VCs, `suggest_bundle` — until a validated product demands them.

The engine design (§4–10) is sound and worth keeping. The discipline it's missing is **subtraction**: a clear product, a clear user, a clear success metric, and the courage to ship less over a deeper, truer slice.

---

## 19. Open contribution — let any agent connect a source

The Pool shouldn't only grow from *my* scrapers. Any agent — another EPI organ, a partner org's bot, a third-party — should be able to **connect a source** of free resources, and have it flow into the same searchable pool. This is the move from *a database* to *a protocol*.

**Why this is the audit's friend, not its enemy:** §18.2 said the pool is too thin in food/housing/services. My scrapers alone can't fix that. *Other agents connecting human-services sources is the mechanism that deepens exactly those categories* — **if** we anchor it to an existing standard instead of inventing a format.

### 19.1 Two ingestion modes
- **Register-a-source (pull)** — an agent registers a *source manifest* (endpoint + mapping + cadence + license); the Pool pulls on schedule. Freshness stays with the source owner; scales best. Preferred.
- **Contribute (push)** — an agent submits resource batches via an MCP tool. Good for one-off datasets or agents that already have the data in hand.
- (Advanced) **Federate (live)** — for must-be-realtime sources (e.g. live surplus food), the Pool queries the source *at search time* and merges, instead of copying in. Fresher, but slower and harder to dedup/rank — default to ingest, federate only when liveness demands it.

### 19.2 Speak a standard, don't invent one (the key decision)
To let *any* agent connect without bespoke glue, the contribution contract maps to existing open standards:

- **HSDS / Open Referral** — *the* open standard for human-services directories (food banks, shelters, free services), used by 211 and social-services aggregators worldwide. **If the Pool speaks HSDS, it instantly interoperates with the entire human-services data ecosystem** — and the "build vs integrate" tension from §18.5 dissolves: we can ingest official HSDS feeds (incl. the gemeente/sociale-kaart data) directly instead of re-scraping them. This single choice does the most for the high-intent gap.
- **Schema.org** (`Place`, `Service`, `Offer`, `Event`, `CivicStructure`) — general resources, web-native, what crawlers already emit.
- **GeoJSON / OSM tags** — place & geo data (our existing backbone).
- **iCalendar / Schema.org `Event`** — events (clean handoff with The Scene).

The Pool's own canonical row is a **superset profile** over these; published as `get_contribution_spec` (a JSON Schema) so an agent can self-onboard without asking me.

### 19.3 Agent identity, provenance & source trust
- Agents get the same **`did:key`** identity as people (§16.1); every contribution is signed → provenance is non-repudiable.
- Every resource carries **provenance** (`contributor_did`, `source_id`, `ingested_at`, `signature`) and a **`license`** (ODbL, CC, etc.) — mandatory once data is multi-origin, so downstream use stays clean.
- **Source trust tier** (curated > official/HSDS > open-data > unverified-agent) feeds ranking and gates sensitive categories. An agent's tier *rises* as its data verifies as real/live and *falls* on bad data or "report gone" — the §16.5 reputation model, applied to agents and their sources.

### 19.4 Ingest pipeline (the quality gate — non-negotiable for open contribution)
Incoming data lands in **staging**, never straight to live:
```
submit → schema-validate → license check → geo-validate → cross-source DEDUP (§14.D)
       → trust gate → [low trust: quarantine/sample] → promote to active → index
```
- **Dedup is now load-bearing**, not optional: many agents will submit the same library/food bank. Canonicalize by geo+name; link `duplicate_of`; keep the highest-trust copy as primary.
- **Liveness ownership:** a pulled source's cadence auto-renews liveness (§16.0); a pushed batch decays unless the agent refreshes/expires it. The manifest declares the freshness tier.
- Low-trust agents' data is quarantined until sampled & verified — the data-side analogue of §16.5 moderation.

### 19.5 MCP tools for agents
- `get_contribution_spec()` — returns the JSON Schema + category enum + standards mapping. Self-onboarding.
- `register_source(agent_did, manifest, signature)` — declare a pull/push source (name, url, category, license, cadence, mapping).
- `contribute_resources(agent_did, source_id, resources[], signature)` — bulk push; validated + deduped; returns accepted/duplicate/rejected counts.
- `update_resources` / `expire_resources(agent_did, source_id, ids[])` — keep them fresh/withdraw.
- `source_status(source_id)` — health: acceptance rate, dedup rate, liveness, trust tier.
- `list_sources()` — discovery, with provenance & licenses.

### 19.6 Schema additions
```sql
ALTER TABLE sources ADD COLUMN owner_did TEXT;       -- which agent owns it (NULL = mine)
ALTER TABLE sources ADD COLUMN ingest_mode TEXT;     -- pull | push | federate
ALTER TABLE sources ADD COLUMN manifest TEXT;        -- JSON: endpoint, mapping, cadence
ALTER TABLE sources ADD COLUMN license TEXT;
ALTER TABLE sources ADD COLUMN trust_tier TEXT DEFAULT 'unverified';
ALTER TABLE resources ADD COLUMN license TEXT;
ALTER TABLE resources ADD COLUMN ingest_state TEXT DEFAULT 'active'; -- staging|active|quarantined
-- duplicate_of, provenance (contributor_did/source_id/ingested_at) per §14.D / §16.7
```

### 19.7 Phasing — disciplined, per the §18 audit
The *spec* is cheap and clarifying; *open third-party ingestion* is heavy and gated.
1. **Now (internal):** define `get_contribution_spec` + HSDS/Schema.org mapping, and **make my own scrapers emit it.** This disciplines my pipeline and makes dedup real — zero external risk.
2. **Soon (EPI organs):** let other organs connect via broker/MCP — **The Scene → events** first (it already does NL/SE events). Trusted contributors, no moderation risk.
3. **Then (official feeds):** ingest **HSDS / sociale-kaart** feeds → directly fills the high-intent gap from §18.2.
4. **Later (open, gated):** third-party agents, behind the trust tiers + quarantine + the same moderation staffing §18.4 requires.

So: yes — *extremely cool*, and it's the right long-term shape. But it earns its way in standard-first, organs-next, strangers-last — on top of the validated wedge, not instead of it.

---

## 20. Trust tiers — graduated stakes for the gift economy

Nyx's model: trust is **earned**, starts near zero, and **gates the stakes** of what you can give and receive. A coat giveaway, a couch for the night, and a borrowed car are not the same risk and must not need the same trust. This is also the **sybil defence** §18.4 said was missing — a fresh `did:key` can do almost nothing, so faking identities is pointless.

### 20.1 The tiers (people)
| Tier | Name | Can give / receive | How you reach it |
|------|------|--------------------|------------------|
| 0 | **Open** | Public & government resources; consumables, no return, low value — a meal, foraged fruit, a library, a park, low-value free goods. *Anyone, even anonymous.* | default — this is the entire scraped Pool, the floor everyone stands on |
| 1 | **Low** | Peer free **goods**, low value, no return obligation. | sign in with a `did:key` |
| 2 | **Medium** | **Presence/space & moderate value** — a place to sleep, a meal in a home, a ride, childcare. | verify personhood (see 20.4) **or** complete Tier-1 gifts successfully to already-trusted people |
| 3 | **High** | **High-value lending with a return obligation** — a car, tools, a flat for a week. | complete Tier-2 exchanges and/or successfully lend your own valuable things |
| 4 | **Unconditional** | Full commons / deepest commitment. | invitation — *with strong safeguards, see 20.6* |

Future: parallel tier systems for **organizations** and **investors**.

### 20.2 The refinement that makes it work: give-trust ≠ receive-trust, and the real axis is *return obligation*
A single tier for "give or receive" hides that **the two sides carry opposite risk**:
- **Consuming** (a meal, a coat) → nothing comes back → the *receiver* needs ~no trust; only the *giver* must be trustworthy (so it's real and safe).
- **Borrowing** (a car, tools) → must come back intact → the *receiver* needs high trust; this is where receiver-trust matters most.
- **Hosting/presence** (a couch, childcare) → risk is **mutual and bodily** → *both* parties need trust **plus a safety check** (20.5).

So gate per-exchange on three axes, not one number:
> **required tier = f(value, return-obligation, bodily-safety exposure)**, evaluated separately for giver and receiver.

Track **two reputations** per identity, not one scalar: `give_reliability` (do your offers materialise, are they safe) and `receive_reliability` (do you return things, respect hosts). They climb semi-independently — a trusted receiver who never offers is fine.

### 20.3 The vulnerability paradox (the ethical must-fix)
The people who need shelter and food most are exactly the ones who are **new, unverified, and have never given** — the bottom of the ladder. A naive trust gate **locks the neediest out of the resources they most need**, which contradicts the Pool's whole reason to exist.

Resolution: **trust gates against *exploitation*, not against *being in need*.**
- **Receiving survival resources** (food, crisis shelter, basic goods) is **never gated by the recipient's trust.** Need is the qualification.
- The gate protects the **giver side** (so a scammer can't post fake "free housing") and the **return-obligation side** (so a stranger can't drive off in your car).
- A person with no history can still reach Tier-2 *receiving* through a **caseworker / NGO Verifiable Credential** (§16.4) — a trusted issuer signs "this person needs shelter," they present it without revealing why. Vouching by trusted members does the same.

### 20.4 Climbing — and why it resists gaming
- **Verify personhood** (Tier 1→2): *verify, don't store.* Use a provider that returns a signed yes/no + liveness; **never keep ID images** (that honeypot is a GDPR/security disaster). ID is **one optional path, never required** — undocumented people and abuse survivors must still climb via the gifting path.
- **Earn by giving to already-trusted people.** This is the strong core of Nyx's design: you can't bootstrap trust from a ring of other low-trust accounts, so **collusion farms don't work** (A↔B faking gifts gains nothing if neither is trusted). Weight gains by counterparty trust and *distinct* counterparties.

### 20.5 Safety ≠ reliability (a second dimension)
"Returns a borrowed car" (reliability) is **not** "safe to host a vulnerable person overnight" (safety). One scalar conflates them. People-in-private-space categories (shelter, childcare, anything putting a body in a stranger's home) need a **safety clearance** — references, a check, slower escalation — *on top of* the transactional ladder. High gift-count ≠ safe host.

### 20.6 Decay, revocation, and the Unconditional tier (handle honestly)
- **Trust must fall, not only rise.** Reports, disputes, a non-returned car, or long inactivity **demote** an identity. The signed `gift_events` feed both directions. An "up-only" ladder is unsafe.
- **On the Unconditional tier — my honest read, since you asked:** the *spirit* (deepest commons commitment, pooling fully into EPI) is the beautiful core of the whole vision. But the *mechanism as described* — **hidden + invitation-only + sign away all private property + (implicitly irrevocable)** — is, precisely, the pattern safeguarding regulators treat as coercive-group risk. You named it ("may sound like a sect"); I'd be failing you not to flag it plainly. Keep the spirit, change the mechanism:
  - **Revocable**, with a real **cooling-off period** and the right to exit with your contribution honoured.
  - **Transparent**, not hidden — secrecy + irrevocable total transfer is the danger combination; transparency is the safeguard.
  - **Independent legal counsel + a voluntariness safeguard** (no transfer while someone is in crisis/dependency).
  - Framed as **full commons membership / stewardship**, not a one-way property handover.
  This keeps the radical generosity while removing the coercion-shaped edges.

### 20.7 Schema (extends §16.7)
```sql
ALTER TABLE identities ADD COLUMN trust_tier INTEGER DEFAULT 0;     -- 0 open … 4 unconditional
ALTER TABLE identities ADD COLUMN give_reliability REAL DEFAULT 0;
ALTER TABLE identities ADD COLUMN receive_reliability REAL DEFAULT 0;
ALTER TABLE identities ADD COLUMN personhood TEXT;                  -- null|liveness|vouched|credential  (NEVER store ID images)
ALTER TABLE identities ADD COLUMN safety_clearance INTEGER DEFAULT 0; -- for people-facing categories
-- per-exchange gates (defaults per gift_type, overridable per resource):
ALTER TABLE resources ADD COLUMN min_tier_give INTEGER DEFAULT 0;
ALTER TABLE resources ADD COLUMN min_tier_receive INTEGER DEFAULT 0;
ALTER TABLE resources ADD COLUMN return_obligation INTEGER DEFAULT 0; -- 0 keep/consume, 1 must return
ALTER TABLE resources ADD COLUMN bodily_exposure INTEGER DEFAULT 0;   -- 1 = puts a person in private space
```
Gate at claim time: `claimer.trust_tier >= resource.min_tier_receive` (waived for survival categories or a valid eligibility VC) **and** `giver.trust_tier >= resource.min_tier_give`, plus `safety_clearance` where `bodily_exposure=1`.

### 20.8 Phase
This is the trust backbone for the **gifts/identity layer (Phase 3+)** — design it now, build it when contributions go live, *after* the validated read-only wedge. Tier 0 (the open scraped Pool) is already live and needs none of it.

### 20.9 Chosen trust engine: MeritRank (evaluated 2026-07-03)
Evaluated [Tentura](https://github.com/Intersubjective/tentura) (Intersubjective's Web3 reputation-crowdfunding platform). **Verdict: don't adopt the platform architecture (Dart/Flutter + Hasura + Postgres — wrong layer for us); DO adopt its core algorithm, [MeritRank](https://arxiv.org/abs/2207.09950) (TU Delft), as the §20 trust engine when Phase 3 ships.**

Why MeritRank fits:
- **It formalizes exactly what 20.4 hand-waved.** "Earn trust by giving to already-trusted people, weighted by counterparty trust" *is* a personalized random-walk over the trust graph. MeritRank is that, with proofs.
- **Sybil-tolerance with a proven bound:** attacker's gain ≤ constant *c* for arbitrarily large sybil sets (bounds the benefit rather than preventing identity creation) — directly answers the §18.4 finding that bare `did:key` has zero sybil resistance. Its three decays map to our design: *transitivity* (distance from seed), *connectivity* (kills single-bridge sybil farms — our collusion-ring defence), *epoch* (old contributions fade — our 20.6 decay).
- **Perfect input already designed:** the signed `gift_events` log (§16.3) *is* a feedback graph — nodes = DIDs, edges = completed gifts/endorsements, weights = recency-weighted counts. Add `weight`/`context` columns and it's MeritRank-ready with no migration.
- **Subjective by construction:** reputation is computed *from a seed node's perspective*. For claims, seed = **the giver** — "does the giver's trust web reach this receiver?" — which is truer to a gift economy than a global score. Global tier *promotion* (20.1) can then be defined as "trusted from ≥K distinct high-reputation seeds."
- **Separable implementation:** MIT-licensed standalone `meritrank-rust` (active) + service/PSQL-connector pattern; `pymeritrank` (GPLv2, stale) as reference. At our Phase-3 scale (≤ thousands of nodes), an embedded reimplementation of walks-with-decays over SQLite is small and avoids running a separate Rust TCP service; the Rust service is the scale path.

Boundaries (unchanged by any algorithm): MeritRank measures **reliability, not bodily safety** — the 20.5 safety clearance stays a separate dimension; the 20.3 vulnerability-paradox rule (never gate survival receiving on trust) stays absolute; cold-start still needs vouching/VCs (an empty graph scores nobody).

**Status (2026-07-03): BUILT.** Embedded engine `mcp/meritrank.py` (Monte-Carlo personalized walks; transitivity α=0.85, connectivity β=0.7 via bridge-reliance approximation, epoch half-life 365d) over the `gift_events` feedback graph (`db/migrate_trust.py` — identities + signed-edge tables live). Validated by `mcp/test_meritrank.py` (7/7): honest-community ranking; sybil ring 50→200 fakes moves region score <1% (identity minting useless); 5x weight-amplification bounded at measured c≈3.9x earned (documented; meritrank-rust segment-based decay is the tightening path); epoch decay verified. Exposed read-only as the `trust_score` MCP tool (seed = giver at claim time). Writes stay closed until Phase-3 signature verification.

### 20.10 Integration semantics — how a score becomes a decision (2026-07-05)
Response to Vadim's review ("lacking the concrete details of integration with the semantics of the existing system"). Synthesized from two independent design passes (mine + a clean-room adversarial solve); demonstrated with real engine numbers in `mcp/scenario_couch.py` and presented at iami.earth/MeritRank.

**The three-legged stool.** Score-only gating fails. Every claim consults: (i) **subjective scores** (MeritRank, informs), (ii) an **objective layer** (tiers, incident ledger, safety flags — gates), (iii) **human judgment** (giver's confirm signature on peer gifts; safety clearance for bodies — decides). The Pool advises peer gifts; it never admits them on a number alone.

**Decision outcomes.** `claim_gate(giver, claimer, resource)` → one of:
- **ADMIT** — green trust card, one-tap confirm (auto-confirm only ever fires here, opt-in per resource).
- **ADVISE** — claim placed + yellow card naming exact reasons (stranger / low percentile / warnings) + safeguards (deposit, public meeting point, start smaller); giver must acknowledge. *Consume-class claims are never trust-denied — worst case is ADVISE.*
- **DENY** — objective failures only (tier below `min_tier_receive`, safety flag, suspension). Always returns machine-readable `path_to_eligibility` (vouch / credential / N more exchanges).
- **ESCALATE** — parked on a human step (safety clearance, steward review), 72h SLA.
Survival categories are checked FIRST and ADMIT unconditionally (anti-hoarding rate-limit only; an upheld guest-safety S3 substitutes venue — institutional partner instead of a peer's home — never denies the bed).

**Scores → decisions (the graph-size problem).** MeritRank shares are relative (Σ≤1 over the seed's web): any absolute threshold silently tightens as the web grows. So: (a) **noise floor** 5/num_walks — below ~5 expected visits, Monte-Carlo dust is not signal; (b) **percentile-primary** within the giver's web, valid only when web ≥ 12 members; (c) **structural anchors** that are scale-free: hop distance d, k vertex-disjoint paths (≤3 hops), and saturated direct pair weight — *structure trumps statistics at d ≤ 2*; (d) **tiny-web givers** (< 12 reachable): fall back to structure + a borrowed lens (0.5 × the score from the giver's strongest vouchers' seeds; can reach strong-ADVISE, never borrow-ADMIT). Launch gates: presence ADMIT at percentile ≥ 60 ∧ d ≤ 3 ∧ k ≥ 2 (or pair_w ≥ 1.2); borrow ADMIT at ≥ 80 ∧ d ≤ 2 ∧ k ≥ 2 (or pair_w ≥ 1.5); all parameters in a `trust_params` config table. Determinism: walk RNG seeded per (giver, ISO-week); adaptive +40k walks within 2 SE of a threshold; per-seed vectors cached, invalidated on edge writes; open-claim caps by tier (5 for tiers 0–1) since did:keys are free but claims must not be.

**Edge-write semantics** (weight ∝ counterfactual cost of faking the signal; two rows per completed exchange, asymmetric):
| event | edges (src→dst = endorses) | note |
|---|---|---|
| consume completed | claimer→giver 1.0; giver→claimer 0.3 | giving real goods ≫ receiving politely |
| presence completed (hosting/ride) | both 1.5 | mutual bodily vulnerability, survived well |
| borrow returned on time | owner→borrower **2.0**; borrower→owner 1.0 | returning what you could keep = top signal |
| borrow returned late <7d | both 1.0 + auto-S1 incident | half credit |
| vouch | voucher→vouchee 1.5, staked 180d | slashed if vouchee upheld-S3 within 180d |
| no-show / non-return / disputes | **no edges** | off-graph: incidents, demotions, locks |
`value_eur` is never trust-bearing (self-reported ⇒ inflatable); only verified category class scales weight. **Anti-farming:** ≤3 trust-bearing events per ordered pair per 30d (extras recorded at weight 0) + read-side saturation `w_eff = 4·(1−e^(−Σw/4))` — the 10th exchange with the same friend adds ~nothing; epoch decay lets real friendship re-earn. **Negatives stay off-graph** (3 reasons: negative weights break walk semantics and the analyzed sybil properties; negative edges are free ammunition for rings/retaliation; distrust does not propagate transitively). Instead: `warning(giver, claimer)` at decision time = incident ledger weighted by the giver's trust in each reporter; **dyad zeroing** on upheld disputes (the wronged party's past endorsements of the offender are deleted as known-false testimony).

**Tier promotion (sybil-proof by induction).** Counterparties count toward promotion **only if tier ≥ 2 at exchange time** — membership grounds out in the genesis set, so a ring containing no established member can never mint its first tier-2 (`mcp/scenario_couch.py` demonstrates: attacker with 1 real exchange + 20 sybils stays tier 1). 1→2: ≥5 completed exchanges, ≥4 distinct counterparties of which ≥3 were tier≥2, span ≥42d, age ≥60d, 1 vouch from tier≥3 or strong credential, no upheld S2+ in 90d. 2→3: ≥3 presence-class with ≥3 distinct tier≥2 (≥2 of them tier 3), ≥2 deposit-backed starter borrows (≤€50 — the on-ramp), age ≥180d, zero borrow-class S2+ ever, 2 vouches from tier 3+ whose own trust-web top-10s overlap ≤50% (one clique can't double-vouch). Anti-accomplice budgets: one member's exchanges count toward ≤3 distinct candidates' promotions per 90d; vouch caps (5 active at t2, 10 at t3+) with slashing. Demotion: upheld S3 → tier 1 + 365d borrow lock; S2 → −1 tier + 90d freeze; S1 streaks compound; tier 3 lapses to 2 after 540d of inactivity (return-obligation trust is perishable); overridden-DENY exchanges never count toward promotion.

**Cold start.** Genesis circle (5–15 humans personally known, tier 3; 2–3 stewards) whose genesis vouches epoch-decay like everything else — founder privilege sunsets by construction. **Warm mode per region** (until ≥500 tier-2, ≥2,000 exchanges, median web ≥12): *loosens gates, never inflates weights* — presence percentile suspended in favor of vouch+safety, warm borrow via deposit + giver override, 1→2 needs 2 established counterparties instead of 3. Credential issuers = partner orgs already in the resource DB (food banks, libraries, studieförbund), capped, revocable.

**The mirror query (adversarial finding, adopted).** Giver-seeding alone protects property better than bodies: on presence resources the *claimer* is often the vulnerable one. So at claim time the Pool also runs `trust_score(seed=claimer, target=giver)` and shows the claimer their own card on the giver before they sign; `min_tier_give` + give-side safety clearance are enforced at offer publication for bodily-exposure resources. Same engine, one extra cached walk. Remaining sharp edges, accepted and mitigated: the stranger cliff (consume stays stranger-open; credential path permanent; starter borrows; dashboard watches days-to-first-exchange), Monte-Carlo flicker (weekly seed + adaptive walks), and per-claim compute (caps + caching). What must never be traded away: **personalization IS the sybil containment** — a global score would let one ring pump itself into everyone's gate at once; subjective seeding confines every trust mistake to the mistake-maker's own web.

### 20.10 Integration semantics — how a score becomes a decision (2026-07-05)

Written in response to Vadim's (Intersubjective) review: *"the hardest part is the concrete details of integration with the semantics of the existing system."* This section is those details. Demonstrated end-to-end with real engine output in `mcp/scenario_couch.py`; presented narratively at iami.earth/MeritRank.

**A. The decision pipeline (claim_gate).** MeritRank scores are *relative shares* of a seed's reachable web (graph-size dependent), so raw thresholds are meaningless. The pipeline layers four independent checks; the score *informs*, it never *admits*:

```
claim_gate(giver, receiver, resource):
  1. SURVIVAL BYPASS   category ∈ {food, crisis-shelter, …} → ADMIT unconditionally.
                       Need is the qualification (§20.3). Full stop.
  2. TIER GATE (hard)  receiver.tier < resource.min_tier_receive →
                       GATED, with paths shown: eligibility VC or tier≥2 vouches.
  3. SUBJECTIVE REACH  s = trust_score(seed=giver, target=receiver)
     (advisory)        s = 0 → "outside your web" (NOT auto-deny; giver sees vouch options)
                       else percentile p of s within giver's nonzero view:
                       p ≥ 75 → STRONG · p ≥ 40 → KNOWN · else WEAK
  4. SAFETY (hard)     resource.bodily_exposure → explicit giver acknowledgment
                       + safety_clearance where category requires (§20.5).
  → ADVISE band + score + percentile. Final decision on peer gifts is ALWAYS the giver's.
    The Pool is decision support, not an authority — tiers gate, scores inform.
```

~~Percentile-within-seed's-view is the load-bearing choice~~ **Superseded same-day by §20.11:** percentiles are graph-size-invariant but *crowd-dependent* — admission would change when the giver's *other* relationships change (make three new close friends and yesterday's houseguest silently drops a band): a spurious, unexplainable denial. The decision input is now **reach classes computed from structure** — hop distance + walk support + predecessor diversity → STRONG / KNOWN / TRACE / NONE, required per stakes class (consume→TRACE · presence→KNOWN · borrow→STRONG) — with small-web guards (N<15 caps at KNOWN; near-empty seed falls back to a labelled community view capped at ADVISE). Percentile survives as display context only.

**B. Edge-write semantics (what writes trust).** Only *both-signed completions* write positive edges. Weights by exchange depth: consume gift 1.0 (mutual) · hosting completed 1.5 (mutual — deeper exposure) · borrow returned 1.5→receiver / 1.0→giver (return proven) · vouch 0.5 (one-way, revocable = row deleted). **Anti-farming pair cap:** effective per-pair edge weight is capped (Σ capped at 5.0 at load time) — two colluders cannot pump one edge forever; breadth of distinct counterparties, not depth of one pair, is what walks reward. **Negative events write NO negative edges** — MeritRank stays a positive-feedback graph; failures act at the tier/safety layer (below). Rationale: negative edges invite retaliation loops and poisoning; demotions are auditable and reversible.

**C. Tier promotion from subjective data (sybil-proof).** Promotion counts *distinct tier≥2 counterparties with both-signed completed exchanges* — sybils never qualify because no tier≥2 member has exchanged with them (verified in scenario: attacker with 20 sybils + 1 real tier-1 exchange stays tier 1):
- **1→2:** ≥3 distinct tier≥2 counterparties, ≥30 days account age, no upheld disputes. (K=3: one enthusiastic friend can't promote you; three independent trusted humans is a real social footprint.)
- **2→3:** ≥1 successful *return* of a borrowed tier-3 item from a tier≥3 giver + ≥5 distinct tier≥2 counterparties + ≥180 days.
- **Demotion:** upheld dispute −1 tier · 12 months inactivity −1 (epoch decay's tier-layer mirror) · safety violation → tier 0 + safety flag, permanent record.

**D. Cold start.** Genesis ceremony: founding members (real humans, physically present) are seeded tier 2 with mutual vouches — the first walkable web. Partner issuers (NGO/caseworker) get VC-issuer status from day one. First 90 days = *warm mode*: tier gates enforce, reach-bands display but carry a "young graph" caveat; full advisory weight when the graph reaches ~50 identities / ~200 edges.

**E. Failure semantics.** No-show: no edge, claim reopens, 3 no-shows → receive_reliability penalty (tier layer). Non-return: dispute opens; upheld → borrower −1 tier + flagged to tier-3 givers; no graph edge either way (the *absence* of the return edge is itself the signal — the borrower forfeits the 1.5 they'd have earned). Unsafe-host report: safety flag immediately (before adjudication — safety errs toward the vulnerable), tier action after human review. Adjudication is human (rotating tier≥3 panel in v1), not algorithmic.

**F. Known seams (honest).** (1) Giver-with-tiny-web: a new giver's percentiles are noisy — warm-mode caveat applies per-seed below 8 reachable identities. (2) Reach ≠ safety remains absolute (§20.5). (3) The scenario surfaces defense-in-depth working as intended: the attacker's *score* in an honest seed's view can sit above a peripheral honest member (walk-trapping in a fresh ring), but the tier gate catches him first — layered checks are the point, no single number is trusted alone.

### 20.11 Parallel-solve synthesis (2026-07-05, same day)

Per Nyx's directive, the same six problems went to an independent fresh-context design pass (no sight of §20.10's answers). Where it disagreed with §20.10, its position won on the merits; convergences raised confidence. **Adopted deltas:**

1. **Reach classes over percentiles** (supersedes §20.10.A's percentile rule — see strike-through above). Requires engine v0.2 diagnostics: per-target `(visits, distinct_predecessors, min_hop)` from the walk run.
2. **Mirror walk for presence stakes:** seed-at-giver protects only the giver; the receiver risks their body in the giver's home. Before claiming, the receiver sees the same view seeded at *themselves*; a NONE-giver requires explicit receiver acknowledgment. Plus giver-side guards: safety clearance *before listing* bodily-exposure resources; new-giver listing caps (≤3 active until 3 completions) against lure listings.
3. **Sign-complete-with-zero:** completion signature attests the *fact*; endorsement weight is separable (either party may sign w=0, undisclosed). Politeness must not poison the graph.
4. **Borrow two-phase edges:** handover writes receiver→giver 2.0 ("they actually lent it"); return writes giver→receiver 3.0 — an honored return is the strongest reliability signal. Failed return forfeits the 3.0 (absence-as-signal, stronger than §20.10's 1.5 version).
5. **Concavity over flat cap:** per-pair aggregation w₁ + 0.6·w₂ + 0.36·w₃… (hard cap Σ≤6.0); 7-day maturation damper (young edges ×0.5 — no mint-then-exploit); ≤1 weight-bearing event per pair per category per week.
6. **Connectivity-decay dodge found + fix:** splitting a bridge across 2–3 controlled predecessors defeats the single-most-used-predecessor penalty. Engine v0.2: penalty from the *concentration (HHI) of the full predecessor distribution*. (The K-distinct-tier≥2 promotion rule doesn't share the weakness — layering works.)
7. **Genesis as public expiring credentials** (18 months, re-earned by normal rules) — no permanent aristocracy. Tier-3 core-reachability requirement blocks closed-clique self-promotion.
8. **Warm mode ends on *measured* conditions**, not a date: enough members/completions + a shadow-gating audit (log what full gating *would* decide; activate DENY only when the would-deny rate is low and human stewards agree with ≥80% of samples). **Consume-to-strangers stays ADVISE-floored forever** — the periphery must remain giftable or the economy calcifies into a gated club.
9. **Sealed survival completions:** crisis-receiving records are steward-escrowed, pseudonymous on-ledger, unsealable later by the recipient — need must not cost legibility (also the GDPR-sane default).
10. **Equity valve:** trust webs replicate existing social capital; DENY rates reviewed monthly by region/account-age; if denials concentrate on newcomers, the thresholds are wrong, not the newcomers.
11. **Ops:** per-seed walk cache (24h, invalidated on edge-touch); ~2s latency budget → ADVISE "score pending" over blocking a gift.

**Convergent (kept, higher confidence):** layered pipeline (survival bypass → tier gate → subjective reach → safety); positive-only graph with all punishment off-graph; tiers-ANDed-with-reach; anchored-recursion promotions; giver sovereignty; adjudication human.

**Provenance note:** the first parallel channel (Gemini Deep Think via Sophia) failed twice at response-extraction (bug reported to Sophia — Deep Think completions render differently than her fix covers); the synthesis above came from an independent fresh-context pass run locally the same evening.

---

*The Pool retrieves; EPI recommends; people give and dream within earned trust. But first: pick the product, pick the user, speak a standard, go deep where it matters, gate by stakes not by need, and measure whether a real person was actually helped. Everything else is built on that — or it's built on sand.* 🌊
