docs/checks.md is the contract. This is the worked example it
promised: the same prose path docs/adding-a-corridor.md
takes for a corridor, walked once for a check, end to end.
Checked against the code at commit 35d7d8d, 2026-09-25. Every claim about
the code below was read from the tree at that commit, not from README prose.
The one measurement (a table of live stellar.toml fetches) carries its source
and the date it was taken. Nothing here describes a capability the repository
does not have.
A check is a small, self-contained contribution: one fact about a counterparty, observed and evidenced. It cannot move a verdict or an integrity state, so it cannot break a published measurement — see the composition rule.
One fact. Not a judgement, not a score, not a threshold.
| A check answers | A metric answers | |
|---|---|---|
| Shape | does this fact hold? | what is this quantity? |
| Result | determined + passed, or not determined |
a decimal.Decimal and a unit |
| Example | "the declared SEP-10 endpoint returns a challenge" | "the bid/ask spread is 128.8% of mid" |
If your contribution is a number rather than a fact, stop and read adding-a-metric.md instead.
The third result state is the whole point of the contract, so it is worth restating in your own terms before you write code:
DETERMINED + PASSED the check ran and the fact holds
DETERMINED + not PASSED the check ran and the fact does not hold
NOT DETERMINED the check could not establish either way — not a failure
Determined is a separate field from Passed in Go and on the wire. There is
no way to express unknown as a zero, a default or a false, and every
undetermined result carries a Reason.
type Check interface {
Describe() Descriptor
Run(ctx context.Context, s Subject) CheckResult
}| Piece | File | What it gives you |
|---|---|---|
Descriptor |
checks/checks.go |
identity, scope, cost, severity, and the two prose fields |
Subject |
checks/checks.go |
what is being examined, already resolved |
Observation, Evidence |
checks/checks.go |
the recorded fact and where it was seen |
CheckResult, Pass, Fail, Undetermined |
checks/checks.go |
the result constructors |
checks.Run |
checks/checks.go |
validates the descriptor, recovers a panic, repairs missing identity |
checks.Runner |
checks/runner.go |
resolves the anchor once, runs every check, aggregates |
checks.GuardedClient |
checks/transport.go |
the HTTP client safe to point at an audited party's URL |
| Wire form | checks/wire.go |
CheckJSON, FindingsJSON |
Read checks/checks.go once, in full, before writing anything. Every rule this
document describes is enforced there, so the code is the authority and this
page is the tour.
The existing default set is the worked-example library. Read the ones nearest
your fact before writing a new one — they are all in checks/:
| ID | File | Scope | Cost | Severity |
|---|---|---|---|---|
toml.anchor-asset-iso4217 |
toml_anchor_asset.go |
asset | free | notice |
sep10.endpoint-responds |
sep10_endpoint.go |
anchor | one-request | warning |
sep24.info-lists-asset |
sep24_info_lists_asset.go |
asset | one-request | notice |
sep38.quote-server-published |
sep38_quote_server.go |
anchor | free | notice |
issuer.auth-flags |
issuer_auth_flags.go |
asset | one-request | critical |
issuer.auth-immutable |
issuer_flag_immutability.go |
asset | one-request | info |
toml.home-domain-roundtrip |
toml_home_domain_roundtrip.go |
asset | expensive | notice |
Those seven are what Runner.Default() returns, so they run on every corridor
sweep. checks/issuer_drift.go also implements Check (issuer.drift) but is
not in the default set, so it does not run — and its Run re-reads the
issuer's auth flags without comparing them to any earlier observation, so it
cannot report drift today. It is a file to read, not a shape to copy.
IDs are <source>.<fact>, lower-case, hyphenated. <source> names where
the fact comes from — toml, sep10, sep24, sep38, issuer — and <fact>
says what is being observed. The ID is the stable handle a reader correlates
findings by across runs, so it must not encode a result: issuer.auth-flags,
not issuer.no-clawback.
Choose Cost honestly. CostFree means derivable from data already
fetched (that is what Subject.Profile is for); CostOneRequest is a single
round trip; CostExpensive is several. The descriptor exists so a scheduler can
run cheap checks often without knowing what any of them do, and a check that
understates its cost breaks that.
Choose Severity carefully, and remember it carries no arithmetic weight.
It orders what a reader sees first:
| Severity | Use it for |
|---|---|
Critical |
a failure that can cost the user their funds — clawback enabled, authorization revocable |
Warning |
a failure that makes the route less reliable |
Notice |
a discrepancy worth knowing — declared behaviour differing from observed |
Info |
context; not a problem |
Subject is typed, and the runner populates what it can before your check runs
(checks/runner.go, ForAsset):
| Field | Populated when | Note |
|---|---|---|
Domain |
the asset has a verified home domain in asset/known.go |
a guessed domain would send your check at somebody else's document |
Profile |
that domain's stellar.toml resolved |
shared, read-only, and the reason a CostFree check costs nothing |
Asset |
always, for ScopeAsset |
code and issuer; the code alone identifies nothing |
Send, Receive |
only for corridor-scoped work | never populated by Runner.ForAsset today |
Integrity, Underlying, ReferenceAgreement |
when the caller has one to give | corridor metrics, not checks |
Missing input is not evidence of a problem with the subject. A field your
check needs but does not have is an Undetermined result with a reason, never
a Fail. The rule and its reasoning:
A check must state what it needs in its descriptor and return an undetermined result — never a failure — when the field it needs is absent.
checks/toml_anchor_asset.go is the model: a nil Profile produces "no
stellar.toml has been resolved for this asset, so nothing was declared to
check", and an entry with no anchor_asset produces another undetermined
result, because a token that declares no peg has not said something wrong — it
has said nothing, and those differ.
Subject.Profile is shared, not owned. The runner resolves it once per
corridor and hands the same pointer to every check, so treat it as read-only. A
mutation through it reaches the rest of the sweep.
Both prose fields are mandatory, and checks.Run calls Descriptor.Validate()
before your Run — a descriptor missing Title, CanDetermine or
CannotDetermine produces an undetermined result rather than a broken sweep.
func (MyCheck) Describe() Descriptor {
return Descriptor{
ID: "toml.my-fact",
Scope: ScopeAnchor,
Cost: CostFree,
Severity: SeverityNotice,
Title: "One line naming what is checked",
CanDetermine: "...",
CannotDetermine: "...",
}
}CannotDetermine is the most valuable thing you will write here. The
likeliest way this system misleads is not a wrong result — it is a correct
result read as answering more than it does. issuer.auth-flags can prove a
clawback flag is set; it cannot prove the issuer will ever use it, and the
reader is told so in the descriptor.
Write it by answering: what would a reader wrongly conclude from a pass? For
sep10.endpoint-responds the answer is threefold — the challenge may be
mis-signed, the anchor may be down now rather than permanently, and one probe is
a moment rather than a pattern.
A check must not declare a Venue. Venue is a metric field
(Descriptor.ValidateAsMetric rejects a venue on a corridor-less metric and is
not called for checks); a check that set one would suggest it publishes a market
figure it does not produce.
The shape is always the same: read the subject, refuse if the input is absent, observe, evidence, and return.
func (c MyCheck) Run(ctx context.Context, s Subject) CheckResult {
d := c.Describe()
at := time.Now().UTC()
if s.Profile == nil {
return Undetermined(d, s, "no stellar.toml resolved for this anchor, so nothing was declared to check")
}
observed := strings.TrimSpace(s.Profile.TOML.SomeField)
ev := Evidence{
Source: anchor.TOMLURL(s.Profile.Domain) + " → SOME_FIELD",
Observed: quoteOrAbsent(observed),
ObservedAt: at,
}
...
return Pass(d, s, "one line for a reader", ev)
}A handful of rules come up every time.
Evidence or it did not happen. Every determined result names what was
observed and where: a URL, an account ID, or a TOML field path, plus the value
seen verbatim where practical — ObservedAt non-zero, Source and Observed
non-blank. On this path that obligation is the author's: checks.Run validates
the descriptor, not the evidence. (The metric path does enforce it mechanically
— checks.RunMetric replaces a result whose evidence is incomplete with an
undetermined one. Nothing stops the same guard being added for checks; until it
is, an evidenced result is a convention you keep, not one the runner can
check.)
Whose failure is it? A transport error means different things depending on who published the address, and getting this backwards is easy and quiet:
| Situation | Result | Why |
|---|---|---|
| Horizon is unreachable while reading issuer flags | undetermined | our own data source failed; that says nothing about the issuer |
A declared WEB_AUTH_ENDPOINT does not respond |
determined failure | the anchor published that address, and it does not answer |
The domain serving a stellar.toml refuses us |
undetermined | the document did not arrive; see the worked example below |
Absence and wrongness are different facts. An anchor that publishes no
WEB_AUTH_ENDPOINT offers no programmatic authentication, which is a legitimate
position and not a failure. Fail on a discrepancy, not on silence.
Say why you are not failing. Every undetermined branch is a sentence a
reader can act on: which document, which field, which endpoint, and what was
seen. undetermined: "could not fetch" is an assertion, not an observation.
Network I/O goes through the HTTPClient seam so your check is replayable from a
snapshot and testable with no network at all. Take the client as a struct field
with a guarded default, exactly as sep10_endpoint.go does:
func (c MyCheck) client() *http.Client {
if c.HTTPClient != nil {
return c.HTTPClient
}
return GuardedClient(15 * time.Second)
}GuardedClient refuses loopback, private, link-local and unique-local addresses
on every hop, including redirects. That is not paranoia here: a stellar.toml
is published by the party being audited, so an anchor could otherwise point
every deployment at its own cloud metadata service.
No new dependencies, and decimal.Decimal for every quantity. Never
float64. If your check needs a bounded read of a response body, use
maxErrorBody (checks/transport.go) rather than reading it whole.
Tests run with no network in CI, inside a network namespace. If your check
touches the wire, it must be driven by a recorded transport or an in-process
httptest server, and snapshot.Replayer returns ErrNotRecorded rather than
falling through to the network — a test that reaches out fails rather than
passing intermittently.
The patterns already in the package:
| What to test | Existing example |
|---|---|
| A stub check that returns a chosen result, to test the runner | runner_test.go, stubCheck |
| A recorded HTTP transport | runner_test.go, recordedTransport |
| A check's own branches against fixtures | checks_test.go, TestAnchorAssetISO4217 |
| The three states staying distinct | checks_test.go, TestSEP10ThreeStatesAreDistinct |
| A transport failure on a source we chose | checks_test.go, TestIssuerAuthFlagsUnreachableIsUndetermined |
| A declared endpoint that is dead | checks_test.go, TestSEP10DeclaredButDeadIsAFailure |
| A nil profile | checks_test.go, TestSEP10NoProfileIsUndetermined |
| The runner's default wiring | runner_test.go, TestRunnerHandsTheClientToDefaultChecks |
For a new check, the cases worth a table test are:
profile is nil -> undetermined, reason names the missing document
the field is absent -> undetermined or failed, and you can say which and why
the field holds a value -> passed, with evidence
the field holds a wrong-shaped value -> failed, with evidence
the upstream returns a transport error -> undetermined (not failed) for a source you chose
the endpoint the subject published fails -> failed
go test ./checks/ -run TestMyCheck -count=1
make fmt vet test racemake race matters here: the runner runs checks sequentially today
(RunAll, deliberately), but a check that starts a goroutine must still be race
clean. CI runs go test -race ./... on every push.
A check that is not in Runner.Default() is dead code. That is the one line
that makes your work reach a reader:
func (r *Runner) Default() []Check {
return []Check{
AnchorAssetISO4217{},
SEP10EndpointResponds{HTTPClient: r.client()},
// ...
MyCheck{HTTPClient: r.client()}, // <- your check
}
}Add it with the client (or HorizonURL) the runner already threads through, so
one transport — and therefore one snapshot — covers the whole sweep. Results
compose through route.WithFindings, which is the only composition point
and branches on nothing:
Checks qualify the headline. They never move it.
No result, at any severity, may change integrity or a verdict. That is
enforced by Findings having no path back into the engine, not by asking
callers to behave. Adding your check to the default set is a change to what
every corridor response reports — expect it to be reviewed as one, and check
the affected golden fixtures under server/ and testdata/ if they assert a
finding count.
A complete check, written the way the repository writes them. It is an
illustration, not a registered check — the file it would live in is
checks/toml_network_mainnet.go, and nothing below has been added to
Runner.Default().
The fact. A stellar.toml declares which network the infrastructure in it
operates on via NETWORK_PASSPHRASE. Wayfare measures the public network; every
registered asset's issuer was verified against a document that said so. If the
document now says something else, the asset identity this project relies on is
no longer supported by the issuer's own publication.
package checks
import (
"context"
"strings"
"time"
"github.com/Wayfare-labs/wayfare/anchor"
)
// NetworkMainnet checks that an anchor's stellar.toml declares the public
// Stellar network.
//
// Every registered corridor asset was verified against a document that named
// the public network. A document that now names a different one is a
// discrepancy between the claim Wayfare is scoring and the document it came
// from, which is exactly the class of fact a check records.
//
// Costs nothing: it reads the stellar.toml the runner has already resolved.
type NetworkMainnet struct{}
// Describe implements Check.
func (NetworkMainnet) Describe() Descriptor {
return Descriptor{
ID: "toml.network-mainnet",
Scope: ScopeAnchor,
Cost: CostFree,
Severity: SeverityWarning,
Title: "stellar.toml declares the public Stellar network",
CanDetermine: "Whether the document's NETWORK_PASSPHRASE is the public " +
"network this deployment measures, or names a different one.",
CannotDetermine: "Whether assets from that document are also reachable on " +
"the public network — an asset's issuer is the identity, and only the " +
"ledger can say whether a given account exists there. Nor is the " +
"passphrase proof of anything: it is a declaration, and a document can " +
"declare it while its accounts live elsewhere.",
}
}
// Run implements Check.
func (c NetworkMainnet) Run(_ context.Context, s Subject) CheckResult {
d := c.Describe()
if s.Profile == nil {
return Undetermined(d, s,
"no stellar.toml has been resolved for this anchor, so no network was declared to check")
}
declared := strings.TrimSpace(s.Profile.TOML.NetworkPassphrase)
ev := Evidence{
Source: anchor.TOMLURL(s.Profile.Domain) + " → NETWORK_PASSPHRASE",
Observed: quoteOrAbsent(declared),
ObservedAt: time.Now().UTC(),
}
// Silence is not a discrepancy. This check does not get to invent a
// requirement the repository has never asserted about a registered
// anchor, so an absent passphrase is undetermined rather than failed —
// the same distinction SEP10EndpointResponds draws for an anchor that
// publishes no auth endpoint.
if declared == "" {
return Undetermined(d, s,
"the document declares no NETWORK_PASSPHRASE, so it does not say which network "+
"these assets are on", ev)
}
if declared != anchor.MainnetPassphrase {
return Fail(d, s,
"the document declares \""+declared+"\", not the public network, so the "+
"assets it lists are on a network Wayfare does not measure — every "+
"figure published about this asset would name an issuer whose own "+
"document no longer claims to be on that network", ev)
}
return Pass(d, s, "the document declares the public network", ev)
}The section after the next one is what that check would say about the six issuer domains in the registry today.
anchor.Profile exposes a Mainnet boolean, and it is true only when
t.NetworkPassphrase == anchor.MainnetPassphrase (anchor/anchor.go,
profileFrom). Absent and wrong are the same value in that boolean. A check
that needs to distinguish them — to say "the document declares no network"
rather than "the document declares another network" — has to read the raw
field, which is one more reason Subject carries the whole profile rather than
a bag of derived booleans.
Measured 2026-09-25 by fetching each registered asset's home domain directly, at
the well-known path anchor.TOMLURL builds
(https://<domain>/.well-known/stellar.toml, anchor/anchor.go). The home
domains come from asset/known.go (registry).
| Asset | Home domain | HTTP | NETWORK_PASSPHRASE |
|---|---|---|---|
| NGNC, GHSC, KESC | ngnc.online |
200 | Public Global Stellar Network ; September 2015 |
| NGNT | cowrie.exchange |
200 | Public Global Stellar Network ; September 2015 |
| USDZ, ZARZ | zeam.money |
200 | Public Global Stellar Network ; September 2015 |
| EURMTL | mtl.montelibero.org |
200 | Public Global Stellar Network ; September 2015 |
| PYUSD | token-metadata.paxos.com |
403 | — |
| USDC | circle.com |
301 → 404 at www.circle.com |
— |
Three things follow, and the third is the reason this section exists.
- Four of the six domains pass. No determined failure was observed on 2026-09-25.
- Two are undetermined, not failed.
token-metadata.paxos.comanswered HTTP 403 from a CloudFront distribution refusing the request's origin country;circle.comredirects towww.circle.com, which answers 404 at the well-known path. Neither is a statement about the issuer's document — the document did not arrive — so a check that failed here would be blaming a subject for a request that never reached it. - The registry has its own, separate question.
circle.comis theHomeDomainrecorded for USDC while the recordedSourceURLishttps://www.circle.com/usdc/.well-known/stellar.toml, which also answers 404 as of the same date. Whether the primary USDC issuer can be verified from its own publication at all is tracked as #6 and is not something this check decides. The README's verification table already records the USDC issuer as not yet verified againstcircle.com. This section and the README agree, and neither is evidence that the issuer is anything other than what it appears to be: absence of a document is not a finding about an issuer.
This is what "a negative or inconclusive finding is a valid result" looks like in a check: today it would report four passes and two unknowns, which is honest and is not a defect in the check.
- The fact is one fact, and the ID is
<source>.<fact> -
CostandSeverityare declared honestly, and noVenueis set -
CanDetermineandCannotDetermineboth say something a reader can act on - Missing input produces
Undeterminedwith a reason, neverFail - A transport failure reaching a source we chose is undetermined; one reaching an address the subject published is a failure
- Every determined result carries
Source,Observedand a non-zeroObservedAt - Network I/O goes through an injectable
HTTPClientwith aGuardedClientdefault - No new dependency; no
float64 - Tests cover the undetermined branch, the pass branch and the fail branch, offline
- The check is added to
Runner.Default()with the runner's client -
make fmt vet test raceis clean
- docs/checks.md — the contract this document walks through
- docs/adding-a-metric.md — the other shape, for quantities rather than facts
- docs/adding-a-corridor.md — the same style of walkthrough for a corridor
- docs/maintainer-owned-areas.md — why the check engine is maintainer-owned while individual checks are open contribution
- docs/offline-testing.md — why a test must not reach the network, and how CI enforces it
- docs/glossary.md — the three-valued result and severity levels