Last reviewed: 2026-08-26
Every quote, fill, failure, and settlement latency an anchor produces is recorded as an outcome. Outcomes aggregate into a public, user-verifiable score. The goal is carrot, not stick: an anchor earns a track record it can point to.
Source of truth: lib/reputation/,
types/reputation.ts, the
/api/reputation/* routes, and the Soroban contract in
contracts/reputation/ (see
docs/ORACLE_SPEC.md).
Defined in lib/reputation/composite.ts:
score = fillRate × (1 − slippage) ÷ (settleSeconds / NORM_SETTLE_SECONDS)
fillRate — fraction of quotes that settled, [0, 1].slippage — fractional gap between quoted and delivered value, [0, 1].settleSeconds — median settlement time; floored at MIN_SETTLE_SECONDS (1).NORM_SETTLE_SECONDS = 300 — the "baseline fast" reference.A score of 1.0 = perfect fill, zero slippage, settled at exactly the 300 s reference. > 1.0 = faster than reference. Higher is better.
lib/reputation/bands.ts maps a raw score to a band
via SCORE_THRESHOLDS (getScoreBand / getBandLabel) so the UI can render
confidence labels rather than raw floats.
The reputation store is pluggable (lib/reputation/store.ts):
lib/reputation/sqlite.ts).lib/reputation/postgres.ts).Aggregation, bucketing, reconciliation, locking, and PII redaction live alongside
(aggregate.ts, buckets.ts, reconcile.ts, lock.ts, redact.ts). Migrations
are in lib/reputation/migrations/.
| Method & path | Purpose |
|---|---|
GET /api/reputation/leaderboard?corridor=… |
Ranked anchors (optionally per-corridor). |
GET /api/reputation/[anchor] |
Current score + bands for one anchor. |
GET /api/reputation/[anchor]/history?window=… |
Historical score series. |
POST /api/reputation/append |
Append a signed outcome tuple. |
POST /api/reputation/dispute |
File a dispute against an outcome. |
POST /api/reputation/reconcile |
Reconcile aggregates (maintenance). |
POST /api/reputation/refresh |
Refresh materialized aggregates. |
GET /api/reputation/sdf-export |
Candidate export for SDF's Anchor Directory — see docs/ANCHOR_DIRECTORY_CONTRIBUTION.md. |
Outcomes are signed and replayable, so a dispute resolves on evidence, not
opinion. Admin-only review is gated by ADMIN_SECRET_KEY via
/api/admin/disputes.
The same outcomes are written to the Soroban reputation contract for permissionless
reads. The contract interface (submit_outcome, anchor registry, admin) is
specified in docs/ORACLE_SPEC.md. Mainnet deployment is a
roadmap gate (see docs/ROADMAP.md, Wave 2.1).
Terminal-state rows expose a "flag incorrect outcome" path. A dispute records the contesting party and the disputed outcome id; because every outcome carries the user's signature and is replayable from the ledger, adjudication is evidence-based.
When an anchor is first onboarded to the fleet, it has no transaction history. This section explains how the reputation system handles this cold-start period.
Source of truth:
lib/reputation/thresholds.ts,
lib/reputation/aggregate.ts,
lib/reputation/bands.ts, and the
ScorecardCard component.
On onboarding, a new anchor has no composite score — the system does not
assign a synthetic seed value. Instead, the scorecard enters an
insufficient_data state, and the UI displays a "Collecting Data" notice
that tells consumers the anchor is still being evaluated.
During bootstrap:
compositeScore is null — no score is computed or displayedstate field is "insufficient_data" (see
Scorecard type)ScorecardCard)Example bootstrap API response (GET /api/reputation/[anchor]):
| Field | Value |
|---|---|
state |
"insufficient_data" |
sampleSize |
0 |
compositeScore |
null |
scoreBand |
(not assigned) |
As the anchor processes transactions, each terminal outcome (completed,
partial, refunded, expired, or error) is appended to the outcome log
(types/reputation.ts). Rolling scorecards
aggregate these outcomes over 7-, 30-, and 90-day windows.
The composite score formula
(lib/reputation/composite.ts) combines the
three factors as:
composite = fillRate × (1 − slippage) ÷ (settleSeconds / 300)
A score of 1.0 means a perfect fill, zero slippage, at exactly the 300-second
reference settle time; values above 1.0 indicate faster-than-reference
settlement. Aggregation over each window uses a flat window — every
transaction in the window contributes with equal weight, with no exponential
decay or recency bias (lib/reputation/aggregate.ts).
Progression toward live status:
| Outcomes | Scorecard state |
Score Band | Phase |
|---|---|---|---|
| 0 | insufficient_data |
(none) | bootstrap |
| 1–29 | insufficient_data |
(none) | bootstrap |
| 30+ | ok |
green / amber / red | live |
The threshold of 30 outcomes is defined by MIN_OUTCOMES_THRESHOLD in
lib/reputation/thresholds.ts and can be
overridden with the NEXT_PUBLIC_MIN_OUTCOMES environment variable.
An anchor graduates to live status when it has accumulated at least
MIN_OUTCOMES_THRESHOLD (default 30) terminal outcomes within the
scorecard window. At that point:
The scorecard state becomes "ok" and exposes full metrics: fillRate,
settleMs (p50 / p95), and slippage (p50 / p95)
A composite score is computed and mapped to a score band:
| Band | Score range | Label |
|---|---|---|
| green | ≥ 95 | Excellent |
| amber | 80 – 94 | Needs Improvement |
| red | < 80 | Critical |
The reputation score is fully evidence-based and trusted by downstream consumers for routing and risk decisions
If your integration reads anchor reputation scores, inspect the scorecard
state field before acting on the score:
"insufficient_data" — the anchor is still in bootstrap. Treat it as
unscored; apply wider risk tolerances or defer high-value routing decisions
until enough outcomes have been recorded."ok" — the anchor has a live, evidence-based score. Use the
compositeScore and score band with standard confidence.You can also call
hasEnoughData(outcomesCount) and
estimateTimeToThreshold(outcomesCount)
to programmatically check readiness and display a progress indicator in your
UI, as the built-in
ScorecardCard does.