Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 11 additions & 17 deletions book/src/metrics/README.md
Original file line number Diff line number Diff line change
@@ -1,27 +1,21 @@
# Metrics

Everything in this section is computed directly from Nostr events the node itself has
published — nothing here is self-reported by the node operator, and nothing is a
subjective score. Each event is first scoped to the node being queried: its pubkey must
match as the event's author, the event's `z` tag must match what that kind expects
(order, dispute, dev-fee-payment, or instance info), and its `y` tag must read
`mostro`. Any event that fails that scoping, or belongs to any other kind entirely, is
silently excluded before a single metric is computed — so a report never mixes in
another node's activity, or another application's use of the same Nostr kinds.
Every metric here is computed from Nostr events the node itself has published — nothing
is self-reported or a subjective score. Each event is first scoped to the queried node
(pubkey as author, expected `z` tag, `y=mostro`); anything failing that scope, or
belonging to another kind, is silently excluded before any metric is computed.

Every metric follows the same rule when there isn't enough underlying data to compute
it: it reports its absence explicitly (`N/A` in console/plain text, `null` in JSON)
rather than a fabricated zero. A `0` you see in the report is always a real, computed
answer — for example, a node can genuinely have zero disputes on a healthy trade
history — never a stand-in for missing data. Each page below explains, for its own
metrics, exactly what triggers the not-applicable case.
A missing value always reports as not applicable (`N/A` in console/plain text, `null`
in JSON), never a fabricated zero — a `0` you see is always a real computed answer.
Each page states the exact rule for its own metrics.

The pages are grouped by what they help you evaluate:
Each metric page follows the same structure: **What it is**, **Source** (the Nostr
event kind/tag it's computed from), and **How to read it**.

- [Longevity and liveness](longevity-liveness.md) — how long the node has run, and
whether it's still active.
- [Trade size and consistency](trade-size-consistency.md) — how much it trades, and
how uniform those trades are.
- [Trade size and consistency](trade-size-consistency.md) — how much it trades, and how
uniform those trades are.
- [Disputes and bond policy](disputes-bond-policy.md) — how its trades have gone wrong,
and what protection it offers traders.
- [Fiat, payment method, and premium](context-signals.md) — what kind of trading it
Expand Down
82 changes: 36 additions & 46 deletions book/src/metrics/activity-grid.md
Original file line number Diff line number Diff line change
@@ -1,67 +1,57 @@
# Activity grid

The activity grid is the one part of the report that shows change over time instead of
a single lifetime summary: a table with one row per time bucket, and for each bucket,
how many successful trades happened, how much volume they moved, and the median trade
size within that bucket. Where [cumulative performance](trade-size-consistency.md) tells
you the totals, the activity grid tells you *when* that activity happened — steadily
across the node's history, concentrated in one burst, or trailing off recently.
### What it is

It's built from the same qualifying successful orders used everywhere else in the
report (see [Trade size and consistency](trade-size-consistency.md#what-counts-as-a-qualifying-order)),
bucketed by each order's timestamp.
A time-bucketed table of successful trades, volume, and median trade size per bucket —
where [cumulative performance](trade-size-consistency.md#cumulative-performance) shows
totals, this shows *when* that activity happened.

### Source

The same qualifying successful orders used throughout the report (see
[Trade size and consistency](trade-size-consistency.md#what-counts-as-a-qualifying-order)),
bucketed by timestamp.

## Range

By default, the grid spans the node's own observed lifetime: from its earliest
qualifying order to its latest. `--since` and `--until` narrow that to an explicit
window instead, and once given, that window is authoritative — even if it turns out to
contain zero orders. In that case the grid still renders every bucket across the
requested range, each showing zero trades, rather than collapsing to the empty/null
result that's reserved specifically for a node with no successful orders at all. That
distinction matters: an empty grid over a requested range tells you "nothing happened
here," which is different information from "this node has no order history to
report."
`--since`/`--until` set an explicit window; without them, the range is inferred from
the node's own earliest/latest qualifying order.

An explicit range always wins, even with zero orders inside it: the grid still renders
every bucket across that range showing zero trades, rather than the empty/null result
reserved for a node with no successful orders at all.

## Granularity and its threshold

Each row in the grid represents a day, a month, or a year, depending on the
granularity. `--view` lets you force one explicitly; without it, `mostro-score` picks
automatically based on how wide the requested (or inferred) range is:
`--view` forces `daily`, `monthly`, or `yearly`. Without it:

| Range | Granularity |
|---|---|
| ≤ 90 days | daily |
| ≤ 730 days (~2 years) | monthly |
| beyond that | yearly |

The reasoning behind these two boundaries is practical, not statistical: a daily grid
over a two-year range would produce over 700 rows, which is unreadable in a terminal
table, so the tool switches to coarser buckets before that happens. The 90-day and
730-day cutoffs were chosen by thinking through what a usable table size looks like,
not derived from a formula.
**Source of the boundaries:** practical, not statistical — reasoned from usable
terminal-table row count (a daily grid over 2 years would produce 700+ rows), not
measured or derived from a formula.

Because a `--view` override can still force daily granularity over a much wider range
than the automatic rule would ever choose on its own, `mostro-score` prints a stderr
warning whenever that happens, naming the exact number of rows the result will have.
The warning reuses the same 90-day boundary the automatic selection uses, so the two
can never disagree about what counts as "too wide."
If `--view` forces daily granularity over a range wider than 90 days anyway,
`mostro-score` prints a stderr warning naming the resulting row count, using the same
90-day boundary so the warning and the automatic rule never disagree.

One more detail worth knowing if you inspect `range_start`/`range_end` closely: even a
range you didn't set explicitly still gets aligned to the chosen granularity's
boundaries. A grid forced to monthly view snaps its displayed range to the first and
last day of the calendar month, not to the raw timestamp of whichever order happened to
be first or last.
A defaulted range still snaps to the chosen granularity's boundaries — e.g. a forced
monthly view snaps to the first/last day of the calendar month, not a raw timestamp.

## Progress indicator threshold

Unrelated to the grid itself, but worth documenting here since it's the report's other
numeric threshold: while fetching data from relays, `mostro-score` prints a "still
fetching" message to stderr if the fetch takes longer than **3 seconds**, so you're not
left wondering whether the tool has stalled. Unlike the granularity boundaries above,
this number came from direct measurement rather than reasoning: three real
connect-and-fetch round trips against the default relay
(`wss://relay.mostro.network`) took 2.06s, 1.96s, and 1.69s. Normal single-relay
operation sits around two seconds, so three seconds sits comfortably above that normal
variance while still catching a fetch that's genuinely running slow. Pass `--quiet` to
suppress it along with the tool's other transient status messages.
### What it is

A "still fetching" message printed to stderr when a relay fetch runs past **3
seconds**, so a slow fetch doesn't look like a stall. Suppressed by `--quiet`.

### Source

Direct measurement, not reasoning: 3 real connect-and-fetch round trips against
`wss://relay.mostro.network` took 2.06s/1.96s/1.69s. Normal operation sits around 2s,
so 3s sits comfortably above that variance while still catching a genuinely slow
fetch.
90 changes: 43 additions & 47 deletions book/src/metrics/context-signals.md
Original file line number Diff line number Diff line change
@@ -1,63 +1,59 @@
# Fiat, payment method, and premium

These three signals are less about whether a node is trustworthy and more about what
kind of trading it actually does — which currencies it settles in, how traders pay,
and how its pricing compares to the market rate. All three read tags on the same
qualifying successful orders described in
These three describe what kind of trading a node does, not whether it's trustworthy.
All three read tags on the same qualifying successful orders described in
[Trade size and consistency](trade-size-consistency.md#what-counts-as-a-qualifying-order),
and all three compare their values byte-for-byte: no trimming whitespace, no case
folding. `"USD"` and `"usd"` are different values here, and so are `"Cash"` and
`" Cash"` — because the Mostro protocol itself doesn't normalize these values before
publishing them, and silently merging them could hide a real formatting bug in a
node's own software.
and all compare values byte-for-byte: no trimming, no case folding. `"USD"`/`"usd"` and
`"Cash"`/`" Cash"` are distinct values, since the protocol itself doesn't normalize them.

## Fiat currency breakdown

This shows which fiat currencies the node's trades settle in, and how much of its
volume each one represents — a currency's order count and its percentage share,
ranked from most to least common (ties are broken alphabetically, so the ranking is
always deterministic). It's built from the `f` tag on each qualifying successful
order.
### What it is

If no qualifying order carries a non-empty `f` value, there's no distribution to build,
and the field reports as not applicable rather than an empty list.
Which fiat currencies the node's trades settle in, ranked by share of orders,
descending (ties broken alphabetically).

**How to read it:** this tells you what to expect if you trade with the node — a node
that mostly settles in EUR isn't necessarily a bad fit if you want USD, but it's useful
context before you start.
### Source

The `f` tag on each qualifying successful order. Not applicable when no order carries
a non-empty value.

### How to read it

Tells you what to expect before trading — a EUR-heavy node isn't a bad fit for USD
trading, but it's useful context up front.

## Payment method breakdown

Similar in shape to the fiat breakdown, but built from the `pm` tag, which records the
payment methods (bank transfer, cash, a specific app) buyers and sellers have used.
Unlike most tags in this report, `pm` can carry more than one value per order — a
single order might list several accepted methods — so this breakdown counts every
individual *mention* across all qualifying orders, not one count per order. A node
where every order lists three payment methods will show three times as many mentions
as orders, and that's expected, not a bug.
### What it is

Ranked distribution of payment methods used, by mention count, not order count.

### Source

The `pm` tag, a multi-value Nostr tag: one order can mention several methods, and each
mention counts individually. An order listing 3 methods contributes 3 mentions, not 1.
Not applicable when there are no mentions at all.

It reports as not applicable when there are no `pm` mentions at all across the node's
qualifying orders.
### How to read it

**How to read it:** use this to gauge whether the node typically supports the payment
method you plan to use, before you commit to a trade.
Use it to check whether the node typically supports your preferred payment method.

## Premium signal

Mostro orders are usually priced at some premium or discount relative to the market
rate, expressed as a signed percentage in the `premium` tag (a negative value means a
discount, a positive one a markup). This signal reports two numbers computed from that
tag across the node's qualifying successful orders: `premium_baseline_percent`, the
median premium the node has actually charged, and `premium_dispersion_percent`, the
population standard deviation around that median — how much the premium tends to swing
from order to order.

Both numbers need at least two data points to mean anything, so they report as not
applicable whenever fewer than two qualifying orders carry a valid, parseable `premium`
value.

**How to read it:** the baseline tells you roughly what premium to expect from this
node on a typical trade. The dispersion tells you how much that can vary — a low
dispersion means the node prices consistently near its baseline, while a high one means
premiums swing widely between orders, so the baseline alone is a less reliable
predictor of what you'll actually be offered.
### What it is

`premium_baseline_percent` (median) and `premium_dispersion_percent` (population
standard deviation) of the node's pricing premium/discount versus market rate.

### Source

The `premium` tag, a signed integer percentage (negative = discount, positive =
markup). Both fields need at least 2 qualifying orders with a valid `premium` value;
otherwise not applicable.

### How to read it

The baseline is the premium to expect on a typical trade. The dispersion tells you how
reliable that expectation is — high dispersion means premiums swing widely between
orders.
83 changes: 37 additions & 46 deletions book/src/metrics/disputes-bond-policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,52 +2,43 @@

## Dispute signals

A dispute means a trade broke down badly enough that a third party had to step in and
resolve it. This section reports how many disputes a node has had, how they were
resolved, and how that compares to the node's trade volume — because a handful of
disputes on a node with thousands of trades reads very differently from the same
handful on a node with only a dozen.

The source is the dispute event, kind `38386`, scoped to the node. Like orders, a
dispute is republished every time its status changes, so `mostro-score` deduplicates by
the dispute's `d` tag and keeps only its most recent state. That final state's `s` tag
is then classified into one of three buckets: `resolved` (`settled`,
`seller-refunded`, or `released` — the dispute concluded), `active` (`initiated` or
`in-progress` — it's still open), or `unknown` (any other value, or a missing one). A
dispute lands in `unknown` rather than being dropped, because the event itself proves a
real dispute happened, even if its final outcome can't be classified from the tag.

The report also computes disputes per 100 successful trades, a normalized rate that
lets you compare nodes of very different sizes on equal footing. This rate is not
applicable only when the node has zero successful trades at all — there's no
denominator to divide by. A node with disputes but zero trades in the denominator
(possible if every trade failed or was canceled) reports the rate as not applicable
too, for the same reason. A node with trades but *zero* disputes reports the rate as a
real `0.0`, which is a meaningful, favorable number, not a placeholder for missing data.

**How to read it:** look at the rate, not the raw count, when comparing nodes. A high
rate is a real warning sign; a low one, especially alongside a long trade history, is a
positive signal. `unknown`-status disputes are worth a second look on their own — they
mean something happened that the node's own data doesn't fully explain.
### What it is

How many disputes the node has had, how they resolved, and disputes per 100 successful
trades — a normalized rate for comparing nodes of different sizes.

### Source

Dispute events (kind `38386`), deduplicated by `d` tag to each dispute's latest state.
The final `s` tag classifies it: `resolved` (`settled`/`seller-refunded`/`released`),
`active` (`initiated`/`in-progress`), or `unknown` (anything else, or missing) — still
counted once toward the total, since a real dispute event exists regardless of status.

### Not-applicable rule

`disputes_per_100_trades` is not applicable only when successful trades are zero —
there's no denominator. Zero disputes with trades present is a real `0.0`, not
not-applicable.

### How to read it

Compare the rate, not the raw count, across nodes. `unknown`-status disputes are worth
a second look — something happened that the node's own data doesn't fully explain.

## Bond policy

Some Mostro nodes require traders to lock a small bond before entering a trade, as a
deterrent against bad-faith behavior. Bond policy reports whether this node does:
`enabled`, `disabled`, or `unknown`.

The source is the instance-status event, kind `38385`, which a Mostro node republishes
periodically with its own operational settings, including a `bond_enabled` tag.
`mostro-score` selects the node's single most recent instance-status event and reads
that tag directly: `true` maps to `enabled`, `false` maps to `disabled`. Anything
else — a missing instance-status event entirely, or a `bond_enabled` value that isn't
recognizably `true`/`false` — maps to `unknown`. `unknown` is deliberately never
collapsed into `disabled`: not knowing whether a bond is required is a different, more
uncertain situation than confirming one isn't, and the report is written to keep that
distinction visible rather than picking a side.

**How to read it:** this metric is descriptive, not a verdict. The report will never
tell you `enabled` is safer than `disabled` or vice versa, because that depends on
context this tool doesn't have — a bond requirement raises the cost of trading but also
raises the cost of bad-faith behavior on both sides. Treat it as one more fact to weigh
alongside the node's dispute history and trade record, not a pass/fail check.
### What it is

Whether the node requires traders to lock a bond before a trade: `enabled`,
`disabled`, or `unknown`.

### Source

The node's most recent instance-status event (kind `38385`), reading its
`bond_enabled` tag. `true`/`false` map directly; a missing event or an unparseable
value maps to `unknown` — never collapsed into `disabled`.

### How to read it

Descriptive, not a verdict: the report never implies which status is safer. Weigh it
alongside dispute history and trade record, not as a pass/fail check.
Loading
Loading