Status: implemented API contracts, including GET /api/chain-heads added
2026-09-26. This document does not describe roadmap capabilities.
All endpoints are read-only. The service does not hold funds, issue tokens, sign transactions, or execute payments.
| Method | Path | Purpose |
|---|---|---|
GET |
/api/corridor |
Measure or retrieve one corridor |
GET |
/api/corridor/trend |
Read stored measurements for a corridor |
GET |
/api/chain-heads |
Publish the current hash-chain tip for each stored corridor |
GET |
/api/assets |
List verified assets configured in the binary |
GET |
/api/market-structure |
Identify shared issuer dependencies across corridors |
GET |
/healthz |
Return service health |
GET |
/ |
Serve the embedded single-file UI |
Unsupported methods return 405. Requests beyond the per-client rate limit return 429 with a Retry-After header (seconds) and the error code rate_limited (see Rate limiting). JSON errors have the shape { "error": "...", "code": "..." }.
Query parameters:
from— send asset code; defaults toUSDC.to— receive asset code; defaults toNGNC.sizes— optional comma-separated positive decimal send amounts. The server accepts at most 24 values. When omitted, the route ladder's default sizes are used.live=1— when history-first mode is enabled, bypass stored history and request a live measurement.
The destination must be a configured asset with a verified fiat peg. Amounts, rates, percentages, and sizes are decimal strings, not JSON numbers.
A successful response contains:
send_asset,receive_asset— asset code, issuer where applicable, and verified fiat peg.integrity—DIRECT,DERIVATIVE,NO-MARKET, orUNKNOWN; this describes corridor structure and is independent of verdict.depends_on— fiat-token dependencies for a derivative corridor.reference_mid,reference_source,reference_pair— the benchmark used for scoring.reference_agreement, optional secondary mid/source/divergence/note, andscored— the two-provider benchmark cross-check.reference_fetched_at— when the benchmark was obtained, when available.floor_loss_pct,floor_size,worst_loss_pct,worst_size— ladder summary values.recommended— the best acceptable quote, or JSONnullwhen no size is recommendable.recommended_size— the send size of the recommendation, when one exists.finding— explanatory prose about the measurement.rungs— one entry per requested size.measured_at— timestamp of the response measurement or stored reading.live—truefor a fresh measurement andfalsefor history.stale— present only whenliveisfalse; containsrecorded_at,age_seconds, andage_human.findings— present only when counterparty checks or metrics were run; findings qualify the headline and do not change integrity or verdict.
Each rung includes send_amount, priced, integrity, optional quote, notes, and an error when that size could not be measured. A priced rung may also include:
marginal_cost— the change in effective receive-asset cost from the previous valid priced rung.marginal_fromandmarginal_to— the valid ladder sizes defining that marginal measurement.cost— the available cost decomposition.
The first valid priced rung has no marginal cost. Missing rungs are skipped, never treated as zero. The current marginal classification is available on the internal ladder result as improving, flat, worsening, or undetermined; fewer than two valid priced points are undetermined.
A live measurement failure is served from the latest stored run when one exists, labelled live: false. If no stored run exists, the request returns an error rather than fabricating a reading.
cURL:
# History-first (fast, may be stale)
curl -s "https://wayfare-cdb9.onrender.com/api/corridor?from=USDC&to=NGNC"
# Live measurement
curl -s "https://wayfare-cdb9.onrender.com/api/corridor?from=USDC&to=NGNC&live=1"
# Custom sizes
curl -s "https://wayfare-cdb9.onrender.com/api/corridor?from=USDC&to=NGNC&sizes=10,100,500&live=1"Example Response (200 OK):
{
"send_asset": {
"code": "USDC",
"issuer": "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN"
},
"receive_asset": {
"code": "NGNC",
"issuer": "GASBV6W7GGED66MXEVC7YZHTWWYMSVYEY35USF2HJZBLABLYIFQGXZY6",
"peg": "NGN"
},
"integrity": "DIRECT",
"depends_on": [],
"reference_mid": "1350.753432",
"reference_source": "exchangerate-api",
"reference_pair": "USD/NGN",
"reference_agreement": "AGREE",
"reference_secondary_mid": "1346.90659134",
"reference_secondary_source": "currency-api",
"reference_divergence_pct": "0.2856",
"scored": true,
"reference_fetched_at": "2026-08-26T14:47:15Z",
"floor_loss_pct": "4.31",
"floor_size": "0.1",
"worst_loss_pct": "97.23",
"worst_size": "5000",
"recommended": {
"description": "USDC -> XRP -> XLM -> NGNC",
"source": "stellar-dex",
"receive_amount": "129.2574648",
"effective_rate": "1292.574648",
"loss_pct": "4.31",
"loss_amount": "5.82",
"verdict": "FAIR",
"warnings": [
"delivers NGNC tokens, not NGN in a bank account; redeeming to fiat is a separate step with its own cost"
]
},
"recommended_size": "0.1",
"live": true,
"measured_at": "2026-08-26T14:47:17Z",
"finding": "Best available: 4.31% below the exchangerate-api mid at 0.1 USDC, graded FAIR. Loss reaches 97.23% at 5000 USDC.",
"findings": {
"checks": [
{
"id": "sep10.endpoint-responds",
"scope": "anchor",
"subject": "NGNC (GASB\u2026)",
"severity": "warning",
"determined": true,
"passed": false,
"summary": "the declared SEP-10 endpoint returned HTTP 403 rather than a challenge",
"evidence": [
{
"source": "https://anchor.ngnc.online/auth?account=GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF",
"observed": "HTTP 403",
"observed_at": "2026-08-26T14:47:17Z"
}
],
"observed_at": "2026-08-26T14:47:17Z"
},
{
"id": "issuer.auth-flags",
"scope": "asset",
"subject": "NGNC (GASB\u2026)",
"severity": "critical",
"determined": true,
"passed": true,
"summary": "the issuer can neither freeze nor claw back this asset",
"evidence": [
{
"source": "https://horizon.stellar.org/accounts/GASBV6W7GGED66MXEVC7YZHTWWYMSVYEY35USF2HJZBLABLYIFQGXZY6 \u2192 flags",
"observed": "auth_required=false auth_revocable=false auth_clawback_enabled=false auth_immutable=false",
"observed_at": "2026-08-26T14:47:17Z"
}
],
"observed_at": "2026-08-26T14:47:18Z"
}
],
"passed": 3,
"failed": 2,
"undetermined": 0,
"worst_severity": "warning"
},
"rungs": [
{
"send_amount": "5000",
"priced": true,
"integrity": "DIRECT",
"quote": {
"description": "USDC -> XLM -> NGNC",
"source": "stellar-dex",
"receive_amount": "186947.8515264",
"effective_rate": "37.38957030528",
"loss_pct": "97.23",
"loss_amount": "6566819.31",
"verdict": "UNUSABLE",
"warnings": [
"delivers NGNC tokens, not NGN in a bank account; redeeming to fiat is a separate step with its own cost",
"thin liquidity: this size gets 96.9% worse pricing than a 10 USDC trade"
]
},
"cost": {
"parts": [
{
"component": "fx_loss",
"amount": "6566819.3084736",
"pct": "97.23194704381251",
"determined": true
}
],
"total_loss_pct": "97.23194704381251"
},
"notes": [
"No viable route. The best of 1 priced route(s) still loses 97.2% against the exchangerate-api mid-market rate. Sending through this corridor at this size is not recommended."
]
}
]
}Query parameters:
from— send asset code; defaults toUSDC.to— receive asset code; defaults toNGNC.limit— positive whole number; defaults to 100 and is capped at 500.
This endpoint reads stored history only; it never measures. It returns 200 with count: 0 and runs: [] for an empty history. Runs are returned oldest first. Each run carries its sequence, timestamp, integrity, dependencies, reference details, ladder summary, finding, and rung loss/verdict values.
integrity_transitions is an always-present array of structural changes
between consecutive runs in that returned window, in chronological order. A
change in integrity state (for example, DIRECT to DERIVATIVE or
DERIVATIVE to NO-MARKET) is included, as is a depends_on change while
remaining DERIVATIVE. A pair involving UNKNOWN is omitted: it cannot
establish that structure changed. An empty array means no such transition was
established in the window. The series is descriptive and does not alter a
verdict or integrity state.
cURL:
curl -s "https://wayfare-cdb9.onrender.com/api/corridor/trend?from=USDC&to=NGNC&limit=30"
curl -s "https://wayfare-cdb9.onrender.com/api/corridor/trend?to=NGNC&limit=7"Example Response (200 OK):
{
"corridor": "USDC-NGNC",
"send_asset": {"code": "USDC", "issuer": "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN"},
"receive_asset": {"code": "NGNC", "issuer": "GASBV6W7GGED66MXEVC7YZHTWWYMSVYEY35USF2HJZBLABLYIFQGXZY6", "peg": "NGN"},
"reference_pair": "USD/NGN",
"count": 1,
"runs": [
{
"seq": 1,
"recorded_at": "2026-08-22T12:09:59Z",
"integrity": "DIRECT",
"reference": {
"mid": "1349.669672",
"source": "exchangerate-api",
"as_of": "2026-08-22T00:02:31Z",
"divergence_pct": "0.0340"
},
"floor_loss_pct": "27.15",
"worst_loss_pct": "97.52",
"finding": "No usable size. Loss is 27.15% at 0.1 USDC...",
"rungs": [
{"send_amount": "0.1", "priced": true, "loss_pct": "27.15", "verdict": "UNUSABLE"},
{"send_amount": "5000", "priced": true, "loss_pct": "97.52", "verdict": "UNUSABLE"}
]
}
]
}Returns the current stored hash-chain tip for every corridor, sorted by corridor key. This endpoint is read-only and does not measure. Each entry contains:
Query parameter: pretty=1 is optional and indents the JSON response for
humans.
Each item in heads contains:
corridor— stable corridor key, such asUSDC-NGNC.seq— sequence number of the current tip.recorded_at— the tip record's UTC timestamp in RFC 3339 form.hash— the full SHA-256 record hash, including itssha256:prefix.
An empty or unconfigured store returns 200 with heads: []. The endpoint
does not sign the response or provide a trusted timestamp: a third party should
retain the returned hash and the time and source at which it observed it. The
hash is a pin for comparing later observations, not proof that the underlying
measurement was correct or that the publisher included every scheduled run.
To establish that a later chain still contains a pinned head, the reader must
obtain the corresponding NDJSON history from the repository's
committed data and verify its links; this endpoint publishes the
tip only and does not return per-record proofs.
At present, the committed store is a rolling window. When rotation trims a corridor, its surviving records are re-sealed from a new window head, so its published tip hash changes and is not a continuation of the previously pinned chain. See ADR 007.
cURL:
curl -s https://wayfare-cdb9.onrender.com/api/chain-headsExample Response (200 OK):
{
"heads": [
{
"corridor": "USDC-NGNC",
"seq": 1,
"recorded_at": "2026-08-22T12:09:59Z",
"hash": "sha256:424b33fcf1202487e493e905a7710247489ccd4d943eb182ce6f0f4f0fb4144f"
}
]
}Returns an assets array. Each entry contains the asset fields and can_be_destination, which is true when the binary has a verified fiat peg for that asset.
cURL:
curl -s https://wayfare-cdb9.onrender.com/api/assetsExample Response (200 OK):
{
"assets": [
{
"code": "NGNC",
"issuer": "GASBV6W7GGED66MXEVC7YZHTWWYMSVYEY35USF2HJZBLABLYIFQGXZY6",
"peg": "NGN",
"can_be_destination": true
},
{
"code": "USDC",
"issuer": "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN",
"peg": "USD",
"can_be_destination": true
},
{
"code": "GHSC",
"issuer": "GASBV6W7GGED66MXEVC7YZHTWWYMSVYEY35USF2HJZBLABLYIFQGXZY6",
"peg": "GHS",
"can_be_destination": true
},
{
"code": "KESC",
"issuer": "GASBV6W7GGED66MXEVC7YZHTWWYMSVYEY35USF2HJZBLABLYIFQGXZY6",
"peg": "KES",
"can_be_destination": true
}
]
}Returns issuer concentrations — cases where a single issuer backs multiple corridor destination tokens. This is a structural fact about the corridor set, not a measurement, and it does not change between runs.
Query parameters:
pretty=1— optional; pretty-print the JSON response.
A successful response contains:
issuer_concentrations— map of issuer account ID to the list of corridor asset codes it issues. Only issuers with two or more corridors are included.corridor_count— total number of registered corridor destinations (assets with a fiat peg, excluding USDC).concentrated_corridor_count— number of corridors that share an issuer with at least one other corridor.generated_at— RFC3339 timestamp when this analysis was produced.
cURL:
curl -s https://wayfare-cdb9.onrender.com/api/market-structure
curl -s https://wayfare-cdb9.onrender.com/api/market-structure?pretty=1Example Response (200 OK):
{
"issuer_concentrations": {
"GASBV6W7GGED66MXEVC7YZHTWWYMSVYEY35USF2HJZBLABLYIFQGXZY6": [
"GHSC",
"KESC",
"NGNC"
]
},
"corridor_count": 8,
"concentrated_corridor_count": 3,
"generated_at": "2026-08-27T12:00:00Z"
}In the current registry, LinkIOIssuer (GASBV6W7GGED66MXEVC7YZHTWWYMSVYEY35USF2HJZBLABLYIFQGXZY6) backs three corridors: NGNC (naira), GHSC (Ghanaian cedi), and KESC (Kenyan shilling). This concentration is a market-structure fact — if that issuer fails, all three corridors fail together.
A healthy service returns status 200 with:
cURL:
curl -s https://wayfare-cdb9.onrender.com/healthzExample Response (200 OK):
{
"status": "ok",
"data": {
"USDC-NGNC": {
"recorded_at": "2026-09-26T08:00:00Z",
"age_seconds": 45,
"age_human": "45s"
}
},
"freshness": {
"chain_head": 55101724,
"newest_record_at": "2026-09-26T08:00:00Z",
"record_count": 431
}
}data maps each corridor that has stored history to its newest run and the
age of that run; a corridor with no stored run is omitted rather than
reported as fresh, and the whole field is null when no history is
configured.
This endpoint checks that the HTTP service is responding. It does not perform a live corridor measurement or validate upstream availability.
freshness describes how current the recorded data is, so a consumer can tell
"is old" apart from "is down":
chain_head: the Stellar core ledger sequence reported by Horizon's root endpoint at request time.newest_record_at: RFC 3339 timestamp of the most recent recorded corridor measurement in the run store (null when the store holds no records).record_count: number of records held by the run store.
Unknown values are rendered as null, never as zero or a synthesised guess.
If Horizon's root is unreachable, chain_head is null while store-derived
fields are still reported; if the store cannot be read, its fields are null
while chain_head is still reported. A 200 with nulls means the check could
not be made, not that data is absent. The freshness block does not change the
endpoint's health verdict: it remains 200 as long as the HTTP service is
answering.
Every route — including /healthz and the UI — passes through a per-client
limiter, because the thing being bounded is the service's cost (upstream
calls, CPU), and that cost is real on every path.
- Sustained rate: 4 requests/second per client (
-rate-limit) - Burst: 10 requests (
-rate-burst) - Client identity: the connecting peer's host, or the client named in
X-Forwarded-Forwhen the request arrives from loopback or a private-range address (the reverse-proxy shape of the hosted deployment). A directly connected public peer cannot choose its own bucket by setting that header. - Off switch:
-rate-limit=0disables limiting entirely — for an operator who already has a limiter in front.
A refused request answers:
HTTP/1.1 429 Too Many Requests
Retry-After: 1
{"error": "rate limit exceeded; ...", "code": "rate_limited"}
Retry-After is rounded up to whole seconds so a client that honours it is
never refused twice for the same bucket. The limit is a fairness mechanism,
not a security boundary: it bounds what one client can cost, and does not
authenticate anyone.
live is not a verdict. It describes where the response came from. A response with live: false is historical, and its stale envelope is authoritative for age. Consumers must not infer freshness from deployment time, request time, or the absence of an error.
Reference rates are never averaged. The response identifies the provider and, when available, the second provider and divergence. NO-MARKET means no path was returned; it is not the same claim as a priced route with an UNUSABLE verdict.
- Reading the API correctly — a worked consumer that
respects
live,scoredand a nullrecommended, with its offline tests - Run store — stored record and hash-chain format
- Snapshot format — recorded upstream bytes
- Checks — tri-state counterparty findings and metrics
- Contributing — invariants for changes