The Pool × Intersubjective

MeritRank, embedded.

How EPI's Pool made MeritRank its trust engine for a planetary gift economy — subjective, sybil-bounded, and running today, validated against a real attack.

Built on MeritRank: Sybil-Tolerant Reputation for Merit-based Tokenomics (TU Delft) and the meritrank-rust reference implementation (Intersubjective, MIT).
Prepared for Vadim & the Intersubjective team · July 2026.
How we integrated it The proof

The problem you already solved

01 Why we needed MeritRank

The Pool is building a gift economy: people offer what they can give, others receive. Identity is self-sovereign — a did:key, free to mint. Our own security audit flagged the obvious hole: a costless identity gives zero sybil resistance. Reputation built on it is farmable.

We had designed a trust ladder — you climb by giving successfully to people who are already trusted. But that was an intuition, not a guarantee. MeritRank is that intuition with theorems:

"Climb by giving to already-trusted people" is a random walk on the trust graph. MeritRank is exactly that — and its connectivity decay is precisely our defence against collusion rings.

The decisive idea: don't prevent identities — bound their benefit. No matter how many sybils an attacker mints, their gain stays under a constant. That was the third way we couldn't design ourselves.

The architecture

02 How we integrated it

Three pieces, all already latent in the Pool's design — MeritRank slotted in with almost no adaptation.

a. The feedback graph = our signed gift_events. Nodes are DIDs; edges are completed, signed exchanges; weights carry epoch decay. One completed gift writes two edges — both parties experienced it. This is a MeritRank feedback graph, unchanged:

-- db/migrate_trust.py  (live in pool.db)
CREATE TABLE gift_events (
  id TEXT PRIMARY KEY,
  resource_id TEXT,        -- the gift (null for a vouch)
  src_did TEXT NOT NULL,   -- edge source (feedback giver)
  dst_did TEXT NOT NULL,   -- edge target
  weight  REAL DEFAULT 1,  -- feedback strength
  context TEXT,            -- gift | vouch
  completed_at TEXT,       -- ISO; epoch decay uses this
  src_sig TEXT, dst_sig TEXT  -- Ed25519 signatures
);

b. The engine — mcp/meritrank.py. An embedded, pure-Python-over-SQLite twin of your algorithm: Monte-Carlo personalized random walks with all three decays from the paper.

class MeritRank:
    def __init__(self, graph,
                 alpha=0.85,      # transitivity: continue-walk prob
                 beta=0.7,        # connectivity: bridge-reliance penalty
                 num_walks=10_000): # epoch: 365-day weight half-life

transitivity decay → value falls with distance from the seed; connectivity decay → arrivals funnelled through one predecessor ("bridge") are discounted, so sybil regions hanging off a bridge are starved; epoch decay → old feedback fades (trust must fall, not only rise).

c. Seed-at-claim-time, exposed as an MCP tool. Reputation is computed from a seed's perspective. When someone claims a gift, the seed is the giver — the question becomes "does the giver's trust web reach this receiver?" Subjective by construction; intersubjective by design. It is the Pool's 5th MCP tool:

gift_events ──► MeritRank(seed = giver) ──► trust_score ──► claim decision (feedback personalized walks subjective (+ safety, eligibility — graph) + 3 decays reputation separate dimensions)
# MCP tool — pool_mcp.py
trust_score(seed_did, target_did?) → subjective MeritRank reputation
  # with target: pair score at claim time (seed = giver)
  # without:     the seed's full ranked trust view

Deliberately embedded. The Pool is SQLite + Python with zero external services, so at our scale we reimplemented walks-with-decays in-process rather than run a separate service. This is an embedded twin, not a fork — and meritrank-rust is our documented production scale path.

The part you asked for

03 Integration semantics — how a score becomes a decision

You said the doc lacked "the concrete details of integration with the semantics of the existing system." Fair. This section and the next are those details: the exact decision pipeline, what writes which edges, how global tiers emerge from subjective walks — then a narrative run against the live engine.

The core problem: MeritRank scores are relative shares of a seed's reachable web — graph-size dependent, so absolute thresholds are meaningless. And in a gift economy the giver, not the platform, must keep final say. Both constraints shaped one pipeline:

claim_gate(giver, receiver, resource):
  1 SURVIVAL BYPASS   category ∈ {food, crisis-shelter} → ADMIT unconditionally.
                      Need is the qualification. Trust gates exploitation, never need.
  2 TIER GATE (hard)  receiver.tier < resource.min_tier_receive →
                      GATED — with the paths shown: eligibility VC, or vouches from tier≥2.
  3 SUBJECTIVE REACH  walk from seed=giver → a reach CLASS, structure first:
    (advisory)        STRONG  live direct edge (hop 1), or strong support via ≥2
                              vertex-disjoint paths
                      KNOWN   hop ≤ 2 with ≥2 independent predecessors, or moderate
                              support above the noise floor
                      TRACE   web faintly reaches them (can only ever feed ADVISE)
                      NONE    0 walks arrive → "outside your web" — NOT auto-denied,
                              giver sees vouch options
                      required: consume→TRACE · presence→KNOWN · borrow→STRONG
  3b MIRROR WALK     presence stakes cut both ways: the receiver sees the same
                      view seeded at THEMSELVES — does their web reach the giver?
                      Their body, their risk, their explicit acknowledgment.
  4 SAFETY (hard)     bodily_exposure → giver holds live human-issued safety
                      clearance + explicit per-claim acknowledgment.
  → ADVISE class + paths ("reached via Amara + 2 independent others"),
    percentile shown only as context. The giver decides.
    Tiers gate; scores inform; safety overrides. No single number is trusted alone.

Why classes, not raw scores — and why percentile is only context. Raw shares are graph-size dependent: a fixed cutoff admits everyone in a 3-person web and no one in a 5,000-person one. Percentiles fix that but carry a subtler bug our parallel design pass caught: they make admission depend on the rest of the crowd — make three new close friends and yesterday's houseguest silently drops a band. So the decision input is hop distance + walk support + predecessor diversity, each with absolute meaning, and percentile is display context only. Three refinements from the adversarial pass harden it: a noise floor of 5/num_walks (below ~5 expected visits, Monte-Carlo shares are dust, not signal — treated as 0); scale-free structural anchors — hop distance, count of vertex-disjoint paths, saturated direct pair weight — with the rule that structure trumps statistics at d ≤ 2 (percentiles need a web of ≥ 12 to mean anything; a direct completed exchange needs no statistics); and determinism — walk RNG seeded per (giver, ISO-week), re-run with more walks near a threshold, so a boundary decision doesn't flicker between refreshes.

The mirror query — our adversarial pass's sharpest finding, adopted. Seeding only from the giver protects property better than bodies: on hosting, the claimer is often the vulnerable one — a guest in a stranger's home. So at claim time the Pool also runs the reverse walk, trust_score(seed=claimer, target=giver), and shows the claimer their own trust card on the giver before they sign; give-side tier and safety clearance are enforced at offer publication for bodily-exposure resources. Same engine, one extra cached walk — and the system's protection becomes symmetric.

What writes edges — only both-signed completions; weights by depth of exposure; nothing negative ever enters the graph:

Lifecycle eventEdges writtenWeight
Consume gift completedmutual (2 edges)1.0
Hosting completedmutual, per stay (per-night is farmable)2.0 — deeper exposure
Borrow — two-phasehandover: receiver→giver ("they actually lent it") · return: giver→receiver2.0 · 3.0 — an honored return is the strongest reliability signal we have
Vouchstrictly one-way (being vouched for endorses nobody); revocable = signed tombstone; carries weight only from tier≥2 vouchers (sybil vouches are inert); ≤12 active per voucher0.5
No-show / non-return / reportnone — acts at tier & safety layerthe borrower forfeits the 3.0 a return would have earned; the walks feel that absence forever

Global tiers emerge from subjective data, sybil-proof: promotion counts distinct tier≥2 counterparties with both-signed exchanges — anchored recursion: trust flows downhill from a humanly-anchored core, and sybils never qualify because no tier≥2 member has exchanged with them. Tier 1→2: ≥K distinct tier≥2 counterparties (K=3–4, calibrated in warm mode) + account age ≥30–60 days + no upheld disputes — one enthusiastic friend can't promote you; several independent trusted humans is a real social footprint. Time is the one resource sybil factories can't parallelize. Tier 2→3: a proven return of a borrowed item + ≥5 distinct + ≥180 days + reachability from the regional core (so a closed clique can't self-promote to car-borrowing). Demotions: upheld dispute −1 · 12-month dormancy decays tiers like epoch decays edges · safety violation → 0, flagged, permanent.

Cold start: a genesis ceremony seeds founding members (physically present, mutual vouches) at tier 2 — the first walkable web — as public, expiring credentials (18 months, re-earned by normal rules: no permanent aristocracy). NGO/caseworker issuers get credential status from day one, rate-capped and public. Warm mode runs until measured conditions (not a calendar date): enough members, enough completions, and a shadow-gating audit showing the would-be denial rate is low and stewards agree with it. One floor is permanent: consume-level gifts to strangers stay open forever — the periphery must remain giftable or the graph stops growing and the gift economy calcifies into a gated club.

Narrative, with the engine running

04 The couch story — four claims, real numbers

A small community, real dates (epoch decay applies), scored by the live engine (mcp/scenario_couch.py, deterministic seed). Cast: three founders (genesis tier 2) · Maya, an active giver · Lena, newer — coat from Amara, cuttings to Kim, bike help for Nyx · Rio, peripheral · Jonas, brand new, zero history · "Spam", an attacker with one real exchange and a 20-sybil fake ring at 3× weights.

Scene 1 — Lena claims Maya's couch. They have never met.

tier gate:  lena tier 2 ≥ 2 ✓   (promoted: amara + kim + nyx = 3 distinct tier≥2)
reach:      hop 2 via 3 independent paths (amara · kim · nyx) → STRONG
            context: share 0.0731, 88th percentile of maya's web — displayed, not decisive
safety:     bodily exposure → maya must explicitly acknowledge
→ ADVISE STRONG — maya decides.

Two hops of real gifting (Maya↔Amara↔Lena, Maya↔Kim↔Lena, fresh Nyx edge) put a stranger at the 88th percentile of Maya's subjective web. That is the whole thesis working: does the giver's trust web reach this person? — yes, measurably.

Scene 2 — Jonas, zero history, claims the same couch.

tier gate:  jonas tier 0 < 2 → GATED — paths shown: caseworker credential, or tier≥2 vouches

Scene 3 — Jonas claims a crisis shelter bed.

→ ADMIT — survival category: receiving is never trust-gated. Need is the qualification.

Same person, same graph, opposite outcome — because the gate protects against exploitation, never against being in need.

Scene 4 — Spam (1 real exchange + 20 sybils at 3× weight) claims Nyx's cargo bike.

tier gate:  spam tier 1 < 2 → GATED  (his only real counterparty is tier 1 — sybil ring counts for nothing)
context:    from nyx's seed — spam=0.0447 · all 20 sybils together=0.0878 · honest lena=0.1004

The instructive detail we're not hiding: in Maya's raw view, Spam's score (0.0493) actually edges above peripheral-honest Rio (0.0622 vs 0.0493 — close), because fresh 3×-weighted ring edges trap walks. The tier gate is what stops him — he can't convert score into access without three real tier≥2 humans completing exchanges with him. That's the defense-in-depth argument in one row of numbers: layered checks, because no single number — not even yours — should be trusted alone.

Maya's full subjective view (what trust_score(maya) returns today):

identityscorerelation
nyx0.1805direct, fresh + genesis web
amara0.1333direct
kim0.1140direct
lena0.0764never met — 2 hops, 3 paths
rio0.0622direct (hosted), single path
spam0.0493attacker — score inflated by ring, caught by tier gate
all 20 sybils combined0.0988≈ one honest acquaintance, split 20 ways
jonas0 — unreachableno path exists

Validation

05 The proof — a real sybil attack

Not a claim — a simulation, deterministic, 7/7 checks pass (mcp/test_meritrank.py). An attacker earns 2 legitimate gifts, then mints N fake accounts with dense fake mutual gifting. What happens to the attacker+sybil region's total reputation, seen from an honest seed:

Fake accounts mintedAttacker+sybil region scoreStrongest single sybil
00.1260.000
100.4690.034
500.4820.007
2000.4850.002

Growing the sybil ring from 50 → 200 fake accounts changes the region's total reputation by 0.6%. Minting identities is useless — exactly the property the paper promises.

For you specifically

06 Honest engineering notes

Where trust math stops

07 Boundaries we keep sovereign

MeritRank measures reliability. It does not — and no algorithm can — measure two things we hold outside it:

The bigger picture

08 An open hand

Intersubjective builds decentralized trust infrastructure. EPI builds a planetary gift economy. It is the same worldview from two directions: trust as intersubjective and earned, not granted by a central authority.

Concretely, two ways this could grow together:

This page is the introduction. The rest is a conversation. 🌊