ServicesReputation

Reputation

One interaction-event stream, two projections. Reputation is the one-to-world projection: domain-scoped trust scores with asymmetric gain/loss physics, lazy exponential decay, and named tiers. Affinity is the one-to-one projection: directed relationship edges between any two entities that climb a configurable state ladder, up to and including a Nemesis suite of rivals that remember. Every example on this page was executed against a live server.

When to reach for it: marketplace seller trust that gates privileges, community karma that fades without contribution, NPC relationships that follow how a player treats them, trust scores for AI agents.

When not to: plain score rankings with no trust physics belong to Leaderboard; earn-and-redeem points ledgers belong to Loyalty.

Concepts

  • Domains are isolated scoring universes (marketplace, tavern_sim). Each has its own config; any operation in a domain without one fails with 404 config_not_found.
  • Entities are kind:id pairs; kinds are auth_user, agent, guild, and ecs_entity. Anything can rate anything.
  • Events are the only write. Each event names an actor, a target, and an event_type that must be declared in the domain config's affinity.event_deltas map (undeclared: 400 unknown_event_type).
  • Reputation delta = rating x gain-or-loss multiplier x weight x repeat-rater falloff: trust is lost faster than earned, and repeat events from the same actor on the same target count geometrically less.
  • Decay is lazy - nothing recomputes in the background. Scores decay by half-life at read time and the tier is re-derived on every read.
  • Roles: reads are open to any authenticated caller; plain users may ingest events only as themselves. Domain config, preference profiles, nemesis resolution, and quarantine status require the reputation_granter capability or a platform admin token.

Configure a domain

Upsert the per-domain rules from a JSON file with reputation and affinity blocks (requires reputation_granter or admin):

snug reputation config set --domain tavern --file ./config.json
snug reputation config get --domain tavern
{
  "reputation": {
    "gain_multiplier": 1.0, "loss_multiplier": 3.0,
    "decay_half_life_days": 30.0, "repeat_rater_falloff": 0.5,
    "min_rater_account_age_days": 0,
    "velocity_cap": { "max_events_per_rater_per_day": 50 },
    "tiers": [ { "name": "untrusted", "min": -1000000.0 },
               { "name": "neutral", "min": 0.0 },
               { "name": "trusted", "min": 5.0 } ]
  },
  "affinity": {
    "neutral_point": 0.0, "decay_toward_neutral_half_life_days": 14.0,
    "event_deltas": { "gift_given": 5.0, "insulted": -8.0 },
    "states": [ { "name": "nemesis", "max": -50.0, "nemesis_suite": true },
                { "name": "rival", "max": -10.0 }, { "name": "stranger", "max": 10.0 },
                { "name": "friend", "max": 1000000.0 } ]
  }
}

Tiers carry inclusive lower bounds, relationship states inclusive upper bounds; empty tier or state lists are rejected with 400 invalid_config. Setting min_rater_account_age_days or the velocity cap below the platform floors requires a full admin token, not just reputation_granter.

Send events

snug reputation event send --domain tavern --type gift_given \
  --actor auth_user:docs-wave --target auth_user:vendor --rating 5
snug reputation event batch --domain tavern --file ./events.json

Verified guardrails: a non-privileged caller whose --actor is not their own auth_user identity gets 403 insufficient_permissions, and so does rating yourself. Admins and reputation_granter holders may submit events for any actor, such as an NPC. Negative ratings need the = form: --rating=-2 (a bare -2 parses as a flag).

The response reports what actually applied: the integrity-damped weight, the reputation delta, and any tier or relationship transition the event caused. A batch (events array, capped at 100 by default, else 400 batch_too_large) is validated up front and rejected wholesale - verified: one unknown type in a two-event batch failed the whole batch and left the target's rating count unchanged.

Read scores

snug reputation reputation get --domain tavern --entity auth_user:vendor
snug reputation reputation leaderboard --domain tavern --tier trusted --limit 25

Over HTTP the score read is enveloped (GET /api/v1/reputation/scores/{domain}/{entity_kind}/{entity_id}), captured live:

{
  "status": 200,
  "msg": "OK",
  "data": {
    "score": {
      "domain": "docs4-tavern",
      "entity": { "kind": "auth_user", "id": "docs4-vendor" },
      "score": 4.999986629123192,
      "raw_score": 5.0,
      "tier": "neutral",
      "rating_count": 1,
      "unique_raters": 1,
      ...
    }
  }
}

That capture is the lazy-decay gotcha in action: the ingest response one second earlier reported a transition to trusted (score 5.0, tier min 5.0), but by read time decay had shaved it to 4.99998 and the tier re-derived as neutral. Set tier bounds with decay in mind.

An entity nobody has rated is 404 score_not_found - there is no implicit zero score. --at <RFC3339> recomputes the decayed score as of any timestamp; verified: a 7.5 score read 30 days out (one half-life) returned 3.75. The leaderboard is delegated to the Leaderboard service and re-ranked by decayed value at read time.

Anti-abuse

Every event passes integrity checks before it lands: a per-rater-pair velocity cap, target burst detection, and rater-cluster (collusion) detection. Tripping a check damps the event rather than rejecting it - verified with a cap of 2/day: the third event came back "damped": true, "damping_factor": 0.5, "reasons": ["velocity_cap"] with its delta quartered by damping and repeat-rater falloff combined. Enough distinct reasons quarantines the target: events apply at weight zero and a Human-in-the-Loop review ticket is opened for the reputation_arbiter approver.

snug reputation quarantine status --domain tavern --entity auth_user:vendor

Quarantine status requires reputation_granter or admin; a plain-user call is 403 insufficient_permissions (verified). Raters below the domain's min_rater_account_age_days are rejected with 403 rater_too_young.

Tier and relationship transitions

Crossing a tier or relationship boundary fans out (best-effort) to PubSub on channel reputation.transitions.{domain}.{kind} and to the durable Timeline stream reputation_transitions_{domain}, where kind is tier_changed, relationship_changed, or nemesis. Both fanouts were captured live; subscribing to the channel is subject to the PubSub channel-capability rules.

Affinity and the Nemesis suite

Directed edges, the state ladder, preference profiles, and nemesis promotion/resolution have their own page: Affinity and nemeses.

Limits and configuration

Batch size (100), edge-index and nemesis-memory caps, anti-abuse windows and thresholds, and the platform floors are tunable via the REPUTATION_* variables in the Reputation CONFIG reference.

Reference

On this page