morphit/docs/API.md
Morphit Team 7c1c3bb332
Some checks failed
morphit-ci / TypeScript typecheck (sweep all workspaces) (push) Has been cancelled
morphit-ci / apps/web svelte-check (svelte-kit sync + svelte-aware tsc) (push) Has been cancelled
morphit-ci / Integration tests (real Postgres 16) (push) Has been cancelled
morphit-ci / ansible-lint (playbook quality gate) (push) Has been cancelled
morphit-ci / Smoke suite (run-smokes.sh, triple-pulse) (push) Has been cancelled
morphit-release / Build + publish release tarball (push) Has been cancelled
v1.11.1 — registration self-heal + data-fs disk health + offline-install RPC softening + node-health unification (cp707-cp710)
2026-08-11 17:38:41 -07:00

914 lines
31 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Morphit public HTTP API
A documented, stable contract over the indexer's `/v1/*` endpoints.
Any Morphit instance you can reach exposes these endpoints — the
hostname changes, the contract doesn't.
This is what powers block explorers, federation aggregators, third-
party clients (CLI tools, mobile apps), and anyone who wants to
read Morphit data without going through the web frontend.
## What we promise
**Stable shape.** Once an endpoint is documented here, response
shapes don't break. Fields can be ADDED (back-compatible) but not
RENAMED or REMOVED without a version bump (`/v2/...`).
**Stable URL.** Documented endpoints stay at the documented path.
We won't move `/v1/orderbook` to `/v1/orders/list` without leaving
a redirect or deprecation notice in place for 90 days.
**Public + free.** No API key, no signup, no rate-limit-by-account,
no payment. Just hit the URL.
**Operator-tunable rate limits** (see "Rate limits" below). Every
operator runs their own instance and sets their own caps; the
defaults below are what you'll find on most instances.
**Read-only.** Every documented endpoint is `GET`. Writes happen
on the Blurt chain via `custom_json` ops, not via this API. If
your tool needs to write data, it broadcasts directly to a Blurt
RPC node — see `apps/web/src/lib/blurt/sign.ts` for the pattern.
## What we DON'T promise
**Specific instance availability.** Any individual Morphit instance
can go down, get blocked in a region, run an old version, or
intentionally rate-limit a noisy client. Don't pin to one
hostname; instead, query `/v1/instances` to discover the federation
and round-robin against multiple operators.
**Real-time freshness.** The indexer polls Blurt with ~3s block
time but applies blocks in batches. Your data is at most a few
seconds behind chain head, sometimes more under load. Endpoints
that need real-time deliver via SSE — see "Streaming endpoints"
below.
**Backwards-compatible operator overrides.** Operators can disable
endpoints (e.g. RSS feeds, SSE streams) per their threat model.
A 404 from one instance might mean "deliberately disabled"; try
another.
---
## Quick start
Pick a Morphit instance. You can find a directory at any
instance's `/instances` page; for this example we'll use
`https://morphit.example.com` as the base URL.
```bash
# Live orderbook — most popular endpoint
curl 'https://morphit.example.com/v1/orderbook?asset=XMR&side=sell&limit=20'
# Health check
curl 'https://morphit.example.com/v1/health'
# Federation directory — find more instances
curl 'https://morphit.example.com/v1/instances'
# An account's reputation
curl 'https://morphit.example.com/v1/accounts/alice/feedback'
```
All responses are JSON unless explicitly noted (RSS endpoints
return `application/rss+xml`).
## Authentication
None. Every documented endpoint is open to the public internet.
If you need higher rate limits than the operator's default, the
intended path is **run your own indexer**. The cost (~$5/month
VPS, see `RUN-A-MORPHIT-NODE.md`) is much lower than negotiating
allowlists with a federation of independent operators, and you
get the data closer to source with no rate limits at all.
## Rate limits
Per-IP, per-minute, enforced by the indexer's middleware:
| Tier | Default | Endpoints |
|---|---|---|
| `resource` | 600 req/min | Single-record lookups, fee quotes, health |
| `list` | 120 req/min | Listings, search, history pagination, RSS |
Defaults are operator-tunable via `MORPHIT_INDEXER_LIST_RATE_PER_MIN`
and `MORPHIT_INDEXER_RESOURCE_RATE_PER_MIN`. An instance running
behind a CDN or reverse proxy may also enforce upstream rate limits
that are stricter.
When you hit the limit, the response is `HTTP 429 Too Many Requests`
with a `Retry-After` header (seconds). Back off.
For aggregator/explorer use cases that do polling, **respect the
list tier of 120/min by polling no more often than every ~500ms
average per endpoint per instance**. Spread load across multiple
instances if you want higher aggregate throughput.
## Versioning
The path `/v1/...` is the stable contract. Breaking changes will
introduce `/v2/...` with `/v1/...` remaining available for at
least 12 months after `/v2/...` ships.
The indexer reports its version in `GET /v1/health` (`version`
field) so clients can warn users on stale instances.
---
## Endpoints
### Health & metadata
#### `GET /v1/health`
Tier: `resource`
Liveness check — also exposes block lag and indexer version.
```json
{
"status": "ok",
"version": "1.11.1",
"uptime_sec": 3742,
"chain_head_block": 17234569,
"indexed_block": 17234567,
"lag_blocks": 2,
"lag_blocks_note": "030 is normal (~90s behind; Blurt makes a block every 3s)",
"stale": false,
"rpc_endpoints_healthy": 4,
"rpc_endpoints_total": 4,
"price_feed": {
"enabled": true,
"blurt_fiat": 0.00130526,
"denomination_fiat": "USD",
"source": "coingecko",
"stale": false
}
}
```
`status` is `"ok"` (lag below configured threshold) or
`"degraded"` (lag exceeds threshold but indexer is still
responsive). If the indexer's database is unreachable, the
endpoint itself returns 503 instead of a body.
`stale` is a boolean mirror of the same threshold check, exposed
as a separate field for clients that just want a yes/no without
parsing the status enum.
`uptime_sec` is seconds since the indexer process started.
`chain_head_block` is the most recent block the indexer has seen
on the Blurt RPC pool; `indexed_block` is the most recent block
the indexer has fully written to its database. `lag_blocks` is
the difference. `lag_blocks_note` is a human-readable hint for
operators eyeballing the endpoint: a healthy indexer trails chain
head by only a handful of blocks, so "normal" is reported as up to
the same threshold the `stale` flag uses (default 30 blocks ≈ 90s
at Blurt's 3-second block time).
`rpc_endpoints_healthy` and `rpc_endpoints_total` report how many
of the operator's configured Blurt RPC endpoints are currently
reachable (out of cooldown) versus configured in total. If
`rpc_endpoints_healthy` reads `0` while the node is behind, the
RPC endpoints — not the indexer — are the problem. Per-endpoint
URLs and detail stay in the operator-opt-in verbose block below.
`price_feed` summarises the BLURT/USD price feed that powers the
UI's fiat echoes (the same number served as `blurt_price_fiat` on
`/v1/listing-fee`, so nothing here is more sensitive than that).
`enabled` is `false` when the operator has
`MORPHIT_INDEXER_PRICE_FEED_ENABLED=false` (the UI then shows
BLURT only). When enabled, `blurt_fiat` is the current 1-BLURT
price, `denomination_fiat` is the fiat ticker it's quoted in
(`MORPHIT_INDEXER_PRICE_FEED_DENOMINATION_FIAT`, default `USD`),
`source` is the upstream currently serving
(`coingecko` or `static_floor`), and `stale` is `true` when no
live upstream has succeeded and the indexer is falling back to the
static floor. The per-upstream forensic detail (drift,
disagreement, peer comparison) stays in the verbose block.
Operators who set `MORPHIT_INDEXER_VERBOSE_HEALTH=1` may also see
a `diagnostics` block in the response with breaker snapshots,
queue depths, etc. This is operator-opt-in because the verbose
data leaks below-threshold signal that a public attacker could
use to time a drain.
The `diagnostics.price` object (present only when the optional price
feed is enabled) reports the live BLURT/USD value, the serving
upstream, and the three price-manipulation defenses (ADR-0039 /
ADR-0041), all surfaced as of cp233: `drift` (defense B — deviation
from a time-decayed moving baseline), `disagreement` (defense C —
`morphit_native` vs the external market price), and `peer` (defense F
— own price vs federation peer median). Each carries an `alert`
boolean that goes true on a sustained breach and is `null` until it
has data. See `docs/OPERATIONS.md` → "Monitoring the
price-manipulation defenses".
Use this endpoint for federation health monitors and uptime
probes.
#### `GET /v1/instance`
Tier: (no rate limit; very small static response)
Per-instance branding and metadata as configured by the operator.
```json
{
"name": "Acme Morphit",
"tagline": "Trade in Acme's community",
"contact_url": "https://acme.example.com/contact",
"alt_networks": {
"tor": "abc123...onion",
"lokinet": "abc123.loki",
"i2p_b32": "abc123.b32.i2p",
"i2p_name": "acme.i2p",
"i2p": null,
"ens": "acme.eth",
"nostr": "npub1..."
},
"fee_recipient": "morphit-fees-acme",
"relay_account": "morphit-relay-acme",
"operator_tag": "acme",
"seo": {
"title": null,
"description": null,
"keywords": null
}
}
```
Fields are nullable when unset. Use this to render an instance's
identity in directories and aggregators.
`alt_networks.i2p_b32` and `alt_networks.i2p_name` are the
preferred fields for I2P addresses (b32 hash and human-readable
name respectively). `alt_networks.i2p` is a deprecated legacy
field kept for one release cycle; it's `null` when either of the
new fields is set.
`alt_networks.ens` is an optional registered ENS `.eth` name
(e.g. `acme.eth`) pointing at the instance, typically via an ENS
contenthash to an IPFS copy of the site. Display-only — the
indexer does not resolve it; frontends render it as a footer pill
linking to an ENS gateway.
`operator_tag` is the operator-attribution tag used for
operator-earnings split. Null on unbranded instances.
`seo.{title,description,keywords}` are optional per-instance SEO
overrides; null means the frontend uses its bundled localized
defaults. Operators only set these if they want to override the
default page metadata for their instance.
---
### Orderbook
#### `GET /v1/orderbook`
Tier: `list`
The live orderbook — every verified, non-expired order across the
federation. This is the most-hit endpoint in the API.
Query parameters (all optional):
| Param | Type | Description |
|---|---|---|
| `asset` | string | Filter to `BTC`, `XMR`, `BLURT`, `USDT`, `USDC`, `DAI`, `BCH`, `LTC`, `DASH`, `DOGE`, `ZEC`, `ARRR`, `DCR`, `SOL`, `ETH`, or `XRP` |
| `asset_network` | string | For multi-network assets: USDT → `erc20`/`trc20`/`spl`/`bep20`; USDC → `erc20`/`spl`/`base`/`polygon`; DAI → `erc20`/`polygon`/`base`/`arbitrum` |
| `side` | string | `buy` or `sell` |
| `fiat_currency` | string | ISO-4217 e.g. `USD`, `EUR` |
| `payment_method` | string | e.g. `bank_transfer`, `paypal`; case-insensitive |
| `location_region`| string | e.g. `US`, `EU` |
| `sort` | string | `recent` (default), `rating`, `trades` |
| `limit` | integer | 1100, default 50 |
| `cursor` | string | opaque cursor from previous response's `next_cursor` |
Response:
```json
{
"items": [
{
"account": "alice",
"permlink": "order-2026-04-25-aaa",
"side": "sell",
"asset": "XMR",
"fiat_currency": "USD",
"amount_min": 100,
"amount_max": 1000,
"price_model": { "kind": "spread", "percent": 5 },
"location_region": "US",
"payment_methods": ["cash_in_person", "zelle"],
"terms": "...",
"fee_method": "blurt",
"feedback_count": 42,
"weighted_rating": 4.7,
"is_new_trader": false,
"engagement_24h": 3,
"created_at": "2026-04-25T14:23:00Z",
"updated_at": "2026-04-25T14:23:00Z",
"expires_at": "2026-05-09T14:23:00Z"
}
],
"next_cursor": "..."
}
```
Notable fields:
- `weighted_rating` excludes feedback flagged by the suspicious-
reciprocity detector — it's the **trustworthy** rating, not raw
average. See `FEES-AND-REWARDS.md` if you need the breakdown.
- `engagement_24h` is the count of distinct accounts who messaged
the order owner about THIS order in the last 24 hours. Useful
for "is this order actually being looked at" signals.
- `is_new_trader` is `feedback_count < 4` — flag for the UI to
badge inexperienced counterparties.
- `fee_method` is one of: `'blurt'` (paid in BLURT — fee split
90/10 operator/treasury), `'waived_first_buy'` (the user's
one-time first-buy waiver per ADR-0011), `'btc'` (paid in
Bitcoin — 100% to treasury), or `'xmr'` (paid in Monero —
100% to treasury).
- `price_model` is one of: `{kind:'fixed', price:N}` (a flat
fiat price per unit), `{kind:'spread', percent:N}` (relative
to current market rate, where `N` is the +/- percentage; 0
means "market price"), or any other shape — unknown `kind`
values pass through as-is for forward compatibility, and
the frontend's `priceModelDisplay.ts` falls back to a
"Custom price" rendering for them. Note: an earlier draft
of this API named these `market_premium`/`premium_pct` and
`unspecified`; those names never shipped — the code paths
use `spread`/`percent` and forward-compat respectively.
#### `GET /v1/orderbook/stream`
Tier: SSE-specific (long-lived; one connection per IP-orderbook-filter)
Server-Sent Events stream of orderbook deltas. Same query params
as `/v1/orderbook` but instead of a snapshot, you get a live feed
of `add`, `update`, and `remove` events as the chain moves.
Use for explorers that need real-time orderbook display without
polling.
#### `GET /v1/orderbook/featured`
Tier: `list`
Featured-slot bidders, top 3 by paid bid amount (`max_slots`, the
indexer's hard cap, is echoed in the response).
Each item is `{ order, bid }`. The `order` is a COMPLETE
`OrderRecord` — identical in shape to a `/v1/orderbook` item,
including the trust signals (`feedback_count`, `weighted_rating`,
the composite `reputation_score`, `is_new_trader` for the 🌱 chip,
`first_trade_at`, `posting_pubkey`), `engagement_24h`,
`asset_network` (a featured USDT order must name its chain) and
`created_at`. Reputation and engagement come from the SAME
sock-puppet-filtered, time-decayed aggregates the orderbook uses
(`apps/indexer/src/api/reputationJoin.ts`), so the numbers rendered
on a featured card can never disagree with the ones on the same
trader's orderbook card. `bid` carries `hours_requested`,
`blurt_paid`, `blurt_per_hour`, `effective_at` and `expires_at`.
#### `GET /v1/orders/:account`
Tier: `list`
All orders for a specific account (live + expired + cancelled).
Useful for "show me alice's complete order history."
#### `GET /v1/orders/:account/:permlink/views`
Tier: `list`
Private viewcount for a specific order — only the order owner can
read this (JWT-gated). Documented for completeness; aggregators
typically don't have access.
---
### Reputation
#### `GET /v1/accounts/:account/feedback`
Tier: `list`
All feedback received by `:account`.
```json
{
"items": [
{
"id": "12345",
"reviewer": "bob",
"subject": "alice",
"rating": 5,
"comment": "Smooth trade, would do again",
"order_permlink": "order-2026-04-20-xyz",
"created_at": "2026-04-22T10:00:00Z",
"source_trx_id": "abc123...",
"suppressed": false,
"has_verified_chat": true,
"responses": [
{
"responder": "alice",
"comment": "Thanks!",
"created_at": "2026-04-22T11:00:00Z"
}
]
}
],
"next_cursor": null
}
```
- `suppressed: true` means the (reviewer, subject) pair is flagged
in `suspicious_reciprocity` or `related_accounts` and excluded
from the headline rating. See ADR-0014 if you need the full
detector spec.
- `has_verified_chat: true` means a real-looking conversation
preceded the review — see `FEES-AND-REWARDS.md` and
`apps/indexer/src/db/schema.sql` (search for the
`verified-chat` marker comments) for the conformance criteria.
#### `GET /v1/accounts/:account/feedback-given`
Tier: `list`
Symmetrical: all feedback authored BY `:account`. Same row shape
as `/feedback` above.
#### `GET /v1/accounts/:account`
Tier: `resource`
Summary of `:account`'s reputation:
```json
{
"name": "alice",
"feedback_count": 42,
"weighted_rating": "4.7",
"by_rating": { "1": 0, "2": 1, "3": 2, "4": 5, "5": 34 },
"first_trade_at": "2025-08-12T...",
"trades_completed": 47
}
```
---
### Federation
#### `GET /v1/operator-blocks/by-blocked/:account`
Tier: `resource`
Whether `:account` is currently operator-blocked **on this
instance**, and if so by whom and why. The frontend banner uses
this to tell a signed-in user that their listings are hidden here.
When there is no block:
```json
{ "account": "alice", "blocked": false }
```
When the operator has an active block:
```json
{
"account": "alice",
"blocked": true,
"operator": "acme-operator",
"reason": "repeated payment-method spam",
"since_block_num": 17234001,
"since_trx_id": "a1b2c3...",
"created_at": "2026-06-01T12:00:00.000Z",
"updated_at": "2026-06-01T12:00:00.000Z"
}
```
#### `GET /v1/operator-blocks/by-operator/:operator`
Tier: `resource`
Every account `:operator` currently has blocked on this instance
(capped at 10,000 rows).
```json
{
"operator": "acme-operator",
"items": [
{
"blocked": "alice",
"reason": "repeated payment-method spam",
"since_block_num": 17234001,
"since_trx_id": "a1b2c3...",
"created_at": "2026-06-01T12:00:00.000Z",
"updated_at": "2026-06-01T12:00:00.000Z"
}
]
}
```
Both endpoints are **unauthenticated** and the data is
**instance-local**. Moderation on Morphit is transparent by
design: a blocked user — and anyone else — can see what an
operator has blocked on their instance and the operator's stated
reason, so an operator cannot censor silently. A block here has
no effect on any other Morphit instance; a user blocked here
remains fully visible everywhere else.
#### `GET /v1/instances`
Tier: `list`
Directory of all known Morphit instances the indexer has probed.
```json
{
"items": [
{
"origin": "https://acme.example.com",
"operator_account": "acmecorp",
"name": "Acme Morphit",
"tagline": "Trade in Acme's community",
"contact_url": "https://acme.example.com/contact",
"alt_networks": { "tor": "...", "lokinet": "...", "i2p": "...", "nostr": "..." },
"registered_at": "2025-12-01T00:00:00Z",
"last_probed_at": "2026-04-30T14:00:00Z",
"last_probe_status": "good",
"indexed_block": 17234567,
"chain_lag_sec": 3
}
]
}
```
- `last_probe_status` is one of: `good` (all checks pass), `quiet`
(live but no recent orders), `stale` (lagging chain), `unreachable`
(probe couldn't connect), `mismatch` (relay account doesn't match
what's recorded on-chain), `never` (never probed).
#### `GET /v1/instances/stream`
Tier: SSE-specific
SSE stream of instance-directory changes. Same shape as
`/v1/instances`, delivered as add/update/remove events.
#### `GET /v1/operators`
Tier: `list`
All registered operators on the chain. Distinct from /instances —
operators are the chain identities, instances are the running
servers. An operator can run multiple instances; an instance can
operate without registering (running unregistered = invisible to
the federation directory).
---
### Discovery & metadata
#### `GET /v1/listing-fee`
Tier: `resource`
Current listing-fee schedule for this instance.
```json
{
"base_fee_blurt": 60,
"feature_fee_blurt_per_hour": 50,
"quote_ttl_seconds": 300,
"base_fee_fiat": 0.12,
"blurt_price_fiat": 0.002,
"denomination_fiat": "USD",
"price_warning": "NOT-AN-ORACLE: For Morphit UI display only. Do NOT use as oracle."
}
```
`base_fee_fiat`, `blurt_price_fiat`, `denomination_fiat`, and
`price_warning` are present iff the operator has price-feed
integration enabled AND the price is fresh. If any are missing,
frontends should display BLURT only.
The `denomination_fiat` field tells you which fiat the `_fiat`
numbers are in. Default `"USD"`; operators in non-USD-native
markets (or hedging against USD erosion) can configure
`"EUR"`, `"GBP"`, `"JPY"`, `"BRL"`, `"CNY"`, `"INR"`, `"RUB"`,
`"AED"`, `"XDR"` (IMF Special Drawing Rights), `"XAU"` (gold
ounces), or any 3-8 character uppercase ticker. See ADR-0040
for the design.
The `price_warning` field carries a loud NOT-AN-ORACLE warning
(cp127 defense H from ADR-0039). Downstream protocols using the
`_fiat` numbers as oracle input do so against this explicit
recommendation; the price is for Morphit UI display only and is
NOT designed to be cryptoeconomically secure as a price feed for
third-party value-bearing systems. Use `/v1/price/morphit-native/receipt`
for the full derivation transparency.
> **cp128 rename**: pre-cp128 the optional fields were
> `base_fee_usd` and `blurt_price_usd` (USD hardcoded). No
> external consumers depend on the old names — the rename
> shipped during pre-launch hardening before any instance went
> live.
`quote_ttl_seconds` is how long a frontend should cache its quote
before re-fetching. The frontend renders fee amounts using this
window; once it elapses, the next page render re-fetches.
The fee-recipient account is NOT on this endpoint; it lives on
`/v1/instance` as the `fee_recipient` field, since it's an
operator-identity property not a fee-schedule property.
#### `GET /v1/chain-fee`
Tier: `resource`
Current Blurt chain `account_creation_fee`. Read from chain
dynamic global properties, with a configured fallback.
```json
{ "account_creation_fee_blurt": 100, "source": "chain" }
```
`source` is `"chain"` if read from a live RPC, `"fallback"` if all
RPCs are unreachable and we returned the configured default.
#### `GET /v1/release`
Tier: `resource`
Latest `morphit_release_v1` op the indexer has seen. Use for
detecting stale instance bundles. When the release carries a
chain-pinned `treasury` block it is surfaced here (BTC/XMR
addresses + amounts and, as of cp372, the BLURT fee base under
`treasury.blurt.base`) — all public information. Any Monero
view key on a legacy row is stripped before the response (it is
never stored or served).
#### `GET /v1/fx`
Tier: `resource`
The indexer's cached USD→fiat rate table (cp372), so a client can
compute the "$1 USD-equivalent" first-order minimum and other fiat
echoes in the user's LOCAL currency without itself calling an FX
provider. Response: `{ base: "USD", rates: { EUR: 0.92, … },
source, stale, updated_at, currency_count }`. The WHOLE table is
served and the client picks its own currency locally — there is
deliberately no per-currency lookup, so the indexer never learns
which fiat any individual user chose (the same privacy posture as
the server-side FX fetch). `404` when the FX feed is disabled on
the instance (`MORPHIT_INDEXER_FX_FEED_ENABLED=false`); clients
then treat amounts as already-USD and the indexer's own floor
still applies.
#### `GET /v1/profiles/:account`
Tier: `list`
Single account's public profile (display name, avatar, BLURT-media
URL, optional Nostr pubkey). 404 if the account has never broadcast
a `morphit_profile_v1` op. Reads cleanly even from a Morphit-naive
account (returns 404, not an error — the account just doesn't have
a Morphit profile yet).
#### `GET /v1/profiles?accounts=alice,bob,carol`
Tier: `list`
Batch profile lookup. Up to 100 accounts per request, comma-
separated. Accounts without a profile row are silently dropped
from the response (no 404 since some-found-some-not is the common
case). Use this for orderbook rows and feedback lists — N+1
single-account lookups are how clients used to flood the API.
Caching: a COMPLETE batch (every requested account resolved) is sent
with `Cache-Control: public, max-age=90, stale-while-revalidate=60`.
A PARTIAL batch — one where any requested account is absent — is sent
with `Cache-Control: no-store`, because an absent account is usually
just indexer lag right after that account's profile broadcast, and
caching the negative result would pin it in the client's HTTP cache
across page refreshes.
#### `GET /v1/operators`
Tier: `list`
The federation's operator directory: every account that has
broadcast `morphit_operator_register_v1` and that this instance
considers active. Useful for federation-health dashboards and
operator-comparison tools.
#### `GET /v1/instance/payment-methods`
Tier: `list`
This instance's payment-method additions (ADR-0021). Operators
extend the global picker with region-specific methods (PromptPay,
PIX, etc.) by broadcasting `morphit_payment_method_addition_v1`
ops; this endpoint returns the active set for the instance you
queried. Different instances will return different sets — that's
the federation working.
```json
{
"additions": [
{
"key": "@instance:promptpay",
"name": "PromptPay",
"description": "Thai instant retail payments…",
"category": "online",
"url": "https://www.bot.or.th/en/our-roles/payment-systems/PromptPay.html"
}
],
"generated_at": "2026-04-29T00:00:00.000Z"
}
```
#### `GET /v1/activity/volume`
Tier: `list`
Aggregate trading-activity stats for the Morphit instance.
```json
{
"trade_count_by_asset_7d": { "BTC": 12, "XMR": 8, "BLURT": 4, "USDT": 6, "USDC": 4, "DAI": 3, "BCH": 3, "LTC": 5, "DASH": 2, "DOGE": 4, "ZEC": 2, "ARRR": 1, "DCR": 1, "SOL": 5, "ETH": 11, "XRP": 7 },
"trade_count_by_asset_30d": { "BTC": 47, "XMR": 31, "BLURT": 19, "USDT": 24, "USDC": 17, "DAI": 13, "BCH": 11, "LTC": 18, "DASH": 9, "DOGE": 15, "ZEC": 8, "ARRR": 4, "DCR": 2, "SOL": 23, "ETH": 42, "XRP": 18 },
"trade_count_by_asset_90d": { "BTC": 132, "XMR": 91, "BLURT": 53, "USDT": 72, "USDC": 51, "DAI": 38, "BCH": 28, "LTC": 47, "DASH": 22, "DOGE": 41, "ZEC": 24, "ARRR": 11, "DCR": 6, "SOL": 67, "ETH": 121, "XRP": 49 },
"volume_estimate_by_asset_30d": {
"BTC": "0.42",
"XMR": "23.0",
"BLURT": "12500",
"USDT": "4200",
"USDC": "3100",
"DAI": "2400",
"BCH": "1.8",
"LTC": "8.5",
"DASH": "3.2",
"DOGE": "1200",
"ZEC": "85.5",
"ARRR": "12.4",
"DCR": "8.7",
"SOL": "180.5",
"ETH": "2580.12",
"XRP": "2.48"
}
}
```
Notes: the asset list is dynamic — new tradable assets added to the
canonical registry appear here automatically. USDT, USDC, and DAI are
each reported as a single rollup; per-network breakdown is not
exposed in this endpoint (see `/v1/orderbook?asset=USDT&asset_network=trc20`
or `/v1/orderbook?asset=USDC&asset_network=base` or
`/v1/orderbook?asset=DAI&asset_network=polygon` for per-network
filtering on the live orderbook).
**Trade count semantics:** unique completed orders that received
feedback from at least one party. An order with feedback from
BOTH parties counts ONCE.
**Volume caveat:** the feedback row carries the order_permlink but
not the actual filled amount. Volume is computed as
`(amount_min + amount_max) / 2` per completed order — clearly
labeled "estimate." Real volume could be anywhere in the
amount-range or even outside it. Don't quote these numbers as
"the volume Morphit did" — quote them as "a midpoint estimate."
#### `GET /v1/attestor-eligibility/:account`
Tier: `list`
Per-account: is this account currently eligible to act as a
third-party fee attestor for the operator-paid-fee scheme?
Returns `{eligible: bool, reason?: string}` where reason is a
machine-readable code (`account_too_young`, `insufficient_stake`,
`recently_attested_too_often`, etc.) when ineligible.
Public read so operator-monitoring tools can verify their
attestor pool stays healthy.
#### `GET /v1/stranger-fee-quote`
Tier: `list`
Quote the stranger-message fee a sender would owe to message a
specific recipient (Finding H layer-2 admission gate). Query
params: `?sender=X&recipient=Y`. Returns the BLURT amount and
the fee-recipient account.
Public read so unauthenticated previews work — a sender about to
write their first message to a stranger needs to know the fee
before any signing happens.
---
### RSS feeds (alternative format)
Same data as `/v1/orderbook`, served as RSS for RSS readers and
news aggregators. Requires an nginx config block to proxy through;
see `OPERATIONS.md §14` and `§24`.
- `GET /rss/orderbook.xml` — full orderbook
- `GET /rss/orderbook/by-asset/:asset.xml` — filtered to one asset
- `GET /rss/orderbook/by-account/:account.xml` — one account's listings
All `application/rss+xml` content type.
### Streaming endpoints
SSE (Server-Sent Events) streams for real-time data:
- `GET /v1/orderbook/stream` — orderbook deltas
- `GET /v1/instances/stream` — federation directory deltas
- `GET /v1/chat/:a/:b/stream` — chat messages between two accounts
(only the two accounts can usefully consume this; ciphertext
delivered as-is)
- `GET /v1/chat-activity/:account/stream` — GLOBAL (all-conversations)
activity pings for one account, so the inbox list + notification
badges update sub-second without per-conversation streams. Emits
`chat_activity` with `{"peer":"<account>"}` ONLY — no ciphertext,
header, or message id (privacy: metadata is on-chain-public; content
stays end-to-end encrypted and is re-fetched same-origin on the ping).
A `ready` event on connect signals the stream is live.
SSE clients must respect `Last-Event-ID` for resume-after-
disconnect. Server emits keep-alive comments every 30s.
---
### Intentionally undocumented endpoints
Several `/v1/*` routes are deliberately omitted from this
document because they require client-side cryptographic context
to be useful:
- **`/v1/chat-identity`**, **`/v1/conversations`**,
**`/v1/chat-read-state`**, **`/v1/chat-admission`** — chat
metadata and ciphertext. You can't decrypt without the
recipient's chat private key, derived from their posting key.
Documented internally in `apps/web/src/lib/chat/`.
- **`/v1/blocks`** — chat blocklist mutations. Each
`morphit_block_v1` op is signed by the blocker; reads are
per-account-self.
- **`/v1/login-pairing`** — the QR-pair handshake endpoint
used by ADR-0022's desktop-mediated paired-readonly sessions.
Pairing is intentionally a closed loop between a desktop
client and its phone; documenting the protocol publicly would
invite confusion about whether arbitrary third parties can
initiate it (they shouldn't).
If you have a genuine third-party use case for any of these,
open an issue and we'll consider promoting it to a documented
endpoint.
---
## Self-hosting
If you're building a serious aggregator or block explorer, run your
own indexer. See `RUN-A-MORPHIT-NODE.md` for the setup walkthrough.
You'll get:
- No rate limits (the public ones are for unknown clients)
- Faster response times (data closer to your application)
- Independence from any single operator's uptime
- Full RSS / SSE access without nginx proxy fiddling
This is genuinely the right answer for high-volume use cases.
## Versioning policy
- **Adding a field** to a response: not breaking. Aggregators
should ignore unknown fields.
- **Removing a field**: breaking. Requires `/v2/...`.
- **Renaming a field**: breaking.
- **Changing a field's type** (string → number, etc.): breaking.
- **Adding a new endpoint**: not breaking.
- **Changing rate-limit defaults**: not breaking, but operators
may notice.
We'll publish breaking-change notices in `morphit_release_v1` ops
on chain (so any indexer can detect that downstream consumers of
the API need to update).
## Reporting issues
API bugs / inconsistencies / docs errata:
- Open an issue at git.agorise.net/agorise/morphit
- Or DM `@agorise:matrix.org` on Matrix
Security issues affecting the API: see `SECURITY.md` for the
disclosure path.