docs / scoring

Scoring

OC Attest returns three metrics with every verification. They come from live chain state — the signer doesn't control them at signing time.

The three metrics

MetricMeaningHow it's derived
sats_bondedInteger satoshisSum of confirmed, unspent UTXO values at the address — or, if the bond: extension is present in the signed message, exactly that declared value (surplus ignored)
days_unspentInteger daysDays since the earliest confirmation time among the bonded UTXOs
scoreReference score_v0 (number)Formula below; advisory only

These are recomputed from live chain state on every verify call. The signed canonical message does NOT carry these values authoritatively — if it did, a signer could lie.

score_v0 — the reference algorithm

score_v0 = round(ln(1 + sats_bonded) * (1 + days_unspent / 30) * 100) / 100

Two examples:

sats_bondeddays_unspentscore_v0
125,0004730.12
50,0001215.15
100,0009046.05

score_v0 is the only scoring algorithm the protocol registers. It's a coarse, protocol-registered default that's useful for UI badges and cross-service comparability. It is NOT the right answer for gating.

Why score is advisory

The score collapses two independent signals (capital committed, time committed) into one number with a specific opinion about how to weight them. That opinion is fine for a "platinum / gold / silver" badge; it's wrong for gating decisions that actually depend on capital vs. time differently.

Gate on raw metrics instead. Your policy layer knows what you need:

// For a forum: any commitment at all.
const ok = sats_bonded >= 10_000 && days_unspent >= 30;

// For an airdrop: serious capital.
const ok = sats_bonded >= 1_000_000 && days_unspent >= 90;

// For a governance vote: weight by time-weighted capital.
const weight = sats_bonded * Math.min(days_unspent, 365);

score_v0 is returned in every response so UIs can render "43" without reimplementing the formula. Gates should ignore it.

Tier labels (UI convenience)

The reference SDKs expose a computeTier() helper that returns a coarse-grained label for visual display:

TierMinimum satsMinimum days
platinum10,000,000365
gold1,000,000180
silver100,00090
bronze10,00030
noneotherwise

Tier thresholds are a suggestion, not protocol. A platform's "Gold" doesn't necessarily mean another platform's. Don't gate governance weight on tier labels.

Edge cases

  • sats_bonded == 0 → status code bond_zero. The signature is still valid cryptographically; the chain state just shows no bond right now. Default verifier behavior returns ok: true with bond_zero in the codes — policy layers decide what zero bond means.
  • Unconfirmed depositsbond_pending. Unconfirmed UTXOs never count toward sats_bonded.
  • Spent UTXOs → not counted. When the signer spends, the proof's sats_bonded drops on the next verify.
  • Missing bond: on a large wallet → confirmed balance is used. Signers holding more than they want to "bond" should use the bond: extension to pin intent.
  • Multiple UTXOs, one old and one newdays_unspent is computed from the oldest-first greedy selection against the declared bond (or over the full confirmed set if no bond is declared).

Your own algorithm

If your use case needs a custom score — e.g., time-weighted capital with a logarithmic cap — compute it from the raw metrics. Do NOT try to "register" it as a new protocol score. The protocol deliberately ships one reference algorithm and lets every RP pick their own policy.

See also