Status: v1 IMPLEMENTED in
backend/src/scoring/independence.ts (per-payer w_i = age · diversity · not_funded, concentration/HHI penalty, coarse temporal organicity → R_eff). Proven against the §1.3 attack catalog by npm run test:independence (honest passes; A1–A4 caught/discounted; A7 collusion is the documented gap, §5). Supersedes the MVP min-counterparty gate. This is the defensible core of TrustLine. zkTLS and on-chain indexing prove revenue is real; this model is what argues revenue is independent. The two are different problems and independence is the open, fundable one.0. Why this is the moat
Anyone can indexgetEvents and read an agent’s USDC inflows. That is not a business — it is a SELECT statement. The hard, defensible question is:
Is this revenue from genuine, independent counterparties, or is the operator paying themselves to manufacture a credit score?If the answer can be faked cheaply, the protocol is insolvent by construction: an attacker fabricates revenue, draws an uncollateralized credit line against it, and walks. Every uncollateralized-credit protocol lives or dies on this. The minimum-counterparty rule (
MIN_COUNTERPARTIES = 3) shipped in backend/src/scoring is a floor, not a moat — it is defeated by spinning up three wallets and paying yourself. This document replaces that binary gate with a continuous, economically-grounded independence model.
1. Threat model
1.1 Attacker goal
MaximizecreditLimit(agent) per dollar of real economic cost, then draw and default. The protocol is safe iff faking the revenue that unlocks a credit line costs the attacker more than the line is worth (see §2).
1.2 Attacker capabilities (assume all of these)
- Can create unlimited Stellar accounts at negligible cost.
- Can move their own USDC freely between their own accounts.
- Can stand up a real Stripe account and charge their own cards.
- Can wait (age accounts), and can script organic-looking activity.
- Cannot (cheaply) obtain other people’s genuine, independent economic activity.
1.3 Attack catalog
2. The reframe: independence is an economic-security property
You cannot prove independence. You can only make faking it expensive. State the protocol invariant we are underwriting toward:value_unlocked = limitMultiple(tier) × R minus expected recovery. Because limitMultiple is ≤3× (revenue_math), the model must ensure that manufacturing 3** of at-risk capital they could otherwise abscond with. Everything below is in service of pricing cost_to_fake upward until the inequality holds. This framing — not a particular heuristic — is the thing to defend in a fundraise: it is a clear, falsifiable security target with a roadmap of levers.
3. Independence-v1 scoring model (on-chain)
Replace the binaryonchainCounts gate with a continuous effective independent revenue R_eff and an independence score in [0,1]. All inputs are derivable from the payment graph the indexer already builds (from → agent, amount, ledger), plus cheap per-account lookups.
3.1 Per-payer independence weight w_i ∈ [0,1]
For each distinct payer i contributing revenue r_i:
- age_factor —
min(1, account_age_days / AGE_FULL). A wallet younger thanAGE_FULL(e.g. 30 days) is discounted; one born this week counts ~0. Defeats A2. - diversity_factor — function of the payer’s own out-degree: how many other, distinct parties it transacts with besides this agent. A payer that pays only this agent looks like a puppet → ~0; a payer embedded in real economic activity → 1. Defeats A1/A2.
- not_funded_factor —
0if payeriwas funded (directly, or withinKhops) by the agent or a known agent-controlled address; else1. Defeats A3.
3.2 Fund-flow loop detection
Build the directed value graph over the lookback window. Run a boundedK-hop (default K=3) reverse search from each payer back toward the agent’s own funding sources. Any payer whose inbound funds originate from the agent is circular and its r_i is excluded (its not_funded_factor = 0). This is the primary defense against the cheapest realistic attack (A3).
3.3 Concentration cap
Cap how much any single payer can contribute to counted revenue, and penalize concentration via a normalized Herfindahl index:3.4 Temporal organicity (lightweight v1)
Real revenue is irregular; scripted loops are periodic. v1 uses a coarse signal: coefficient of variation of inter-payment intervals and amount entropy, combined intoorganicity_factor ∈ [0.5, 1] (never below 0.5 — this is a soft signal, not a gate). Flags A5 for review without hard-failing legitimate steady revenue.
3.5 Composition
R_eff (not raw on-chain revenue) feeds computeScore exactly where onchainUsdc does today. The existing MIN_COUNTERPARTIES rule becomes a cheap pre-filter, not the decision. zkTLS off-chain revenue keeps its higher weight but is itself subject to §4.
4. Off-chain independence (Stripe) — partial in v1
Self-charged Stripe (A6) is harder because we don’t see cardholder identities. v1 ships signals, not a solution, and weights them into the off-chain figure:- Stripe account age and payout history (provable via additional zkTLS fetches against Stripe’s balance/payout endpoints).
- Refund / dispute / chargeback rate — a self-charge ring tends to have an anomalous (often zero) dispute profile.
- Payment-method diversity as exposed by Stripe’s own aggregate metrics (count of distinct cards / customers), proven via zkTLS without revealing PII.
5. What v1 does NOT solve (be honest in the pitch)
Path beyond v1: payer staking / skin-in-the-game, cross-protocol identity and reputation (reusing thestellar8004_identity interface), ZK reputation proofs, and per-agent credit ramps (limits grow with repayment history, so a cold attacker can never jump straight to a large draw — this caps value_unlocked directly and is the cheapest, highest-leverage lever to add next).
6. Implementation map
- Data: extend the indexer to retain the payment graph (
from, to, amount, ledger) and expose cheap per-account lookups (age via first-activity ledger; out-degree via counterparties). This is the same persistent-indexer→DB upgrade the on-demand scanner needs anyway — independence and scale share one dependency. - Engine: new
backend/src/scoring/independence.tscomputingR_effandindependence_score;computeScoreResultconsumesR_effin place of rawonchainUsdc. Surface every factor in the API so the borrower dashboard’s “Score Breakdown” can show why (turns a black box into a trust feature). - Contracts: unchanged. Independence is an off-chain underwriting concern; the on-chain
score_registrystill just stores the signed result.
7. Parameters (initial, all tunable)
All defaults are starting points to be calibrated against real + adversarial data; the calibration methodology is itself part of the v1 deliverable.