Docs / Guides
Getting started for app builders
Everything LOPW does on-chain is public and indexed. The read-only API serves it as JSON for third-party apps, mobile clients and agents: no wallet, no node, no key.
Conventions
- Versioned paths with the network in the path:
/v1/{network}/..., wherenetworkistestnetormainnet. - JSON only,
snake_casefields matching the contract types; amounts as decimal strings (i128 does not fit a JSON number); timestamps as Unix seconds; 32-byte values as lowercase hex. - Errors are RFC 9457
application/problem+jsonwith a stable machine-readablecode. - Cursor pagination (
cursor,limit1 to 200, default 50), never offset pages, so clients can resume. - Open CORS on read endpoints; no cookies; no auth for public reads.
- Cache-friendly:
ETagand304 Not Modified; rate limits advertised withRateLimit-*headers and429plusRetry-After. - Real time:
GET /v1/{network}/streamis a Server-Sent Events feed of contract events. - Self-describing: the OpenAPI 3.1 document is served at
/v1/openapi.json; a copy lives at /docs/api/openapi.json.
First calls
GET /v1/testnet/stats
// { "services": 2, "active_services": 2, "total_stake": "4000000000", "claims_paid": 9, "claims_rejected": 1, "total_paid": "320000000", "indexed_ledger": 5021999 }
GET /v1/testnet/services?state=Active&limit=50
// { "data": [ { "service_id": "…64 hex…", "seller": "G…", "state": "Active", "stake": "2000000000", "min_stake": "1000000000",
// "pending_withdrawal": "0", "terms_hash": "…", "key_version": 1, "claims": { "paid": 3, "rejected": 0, "total_paid": "150000000" } } ],
// "next_cursor": null }
GET /v1/testnet/services/{service_id}/claims?outcome=paid
// { "data": [ { "id": "…", "service_id": "…", "predicate_id": 6, "outcome": "paid", "reason": null, "buyer": "G…", "amount": "50000000",
// "payment_reference": "…", "transaction": "…", "ledger": 5021500, "occurred_at": 1790991420 } ], "next_cursor": null }const API = "https://api.lopw.com";
const res = await fetch(`${API}/v1/testnet/services?state=Active`);
const { data, next_cursor } = await res.json();
for (const s of data) {
const stakeUsdc = Number(BigInt(s.stake)) / 1e7; // decimal string -> number for display only
console.log(s.service_id.slice(0, 8), s.state, stakeUsdc, "USDC", s.claims.paid, "claims paid");
}Field values shown here are illustrative shapes, not live numbers.
Live events
const es = new EventSource("https://api.lopw.com/v1/testnet/stream");
es.addEventListener("claim_paid", (e) => console.log("claim paid", JSON.parse(e.data)));
es.addEventListener("state_changed", (e) => console.log("state changed", JSON.parse(e.data)));
// Each message has id (event ID), event (kind) and data (an Event). Resume with the Last-Event-ID header or ?after=<id>.Event kinds mirror the contract events: service_registered, deposited, slashed, state_changed, claim_paid, claim_rejected, and the admin events (claims_change_proposed, validator_change_proposed, claims_contract_set, validator_set). The full list is in the registry and claims references.
Reading the chain directly
The API is a convenience; the chain is the source of truth. Any app can read the same data with the protocol package’s binding, which simulates read-only contract calls from a throwaway account and never signs anything:
import { StellarBinding, getNetwork } from "@lopw/protocol";
const binding = new StellarBinding(getNetwork("testnet"));
const service = await binding.getService(serviceId); // { seller, state, stake, min_stake, terms_hash, key_version, max_claim_window, pending_withdrawal }
const terms = await binding.getTerms(serviceId, service.terms_hash); // { predicates, claim_window, max_data_age, allowed_status, max_response_bytes, oracle }
const key = await binding.getKey(serviceId, service.key_version); // { public_key, registered_at, retired_at }Next: the complete HTTP API reference, or read one service’s bond from its own discovery endpoint.