The open-source indexer & data API for Stellar / Soroban: record every contract event, query any moment in history.
Live demo: stardex.onrender.com (browse real testnet events in the explorer).
Public API: stardex-api.onrender.com, try /events?limit=5 or /health.
Stellar's RPC only keeps a short window of history and then prunes it. Stardex captures that history durably and makes it queryable, so any dApp can ask "every payment this user has made," "this contract's daily volume," or "every swap this AMM has emitted," without rebuilding indexing from scratch.
This repo is the engine. The rest of Stardex lives in separate repos in the stardexhq org:
| Repo | What it is |
|---|---|
| stardex (this repo) | Rust ingester, decoders and CLI, plus the Postgres schema in db/migrations |
| stardex-backend | HTTP API over the database |
| stardex-sdk | @stardex/sdk on npm: typed client and shared types |
| stardex-frontend | Web app, including the event explorer |
Stardex is in active development, but the core engine is real and runs against live testnet:
- Live event streaming from Stellar RPC. Pages through a contract's events and polls for new ones, with retry/backoff through transient outages.
- Multi-contract indexing. Register any number of contracts (
stardex add) and index them all at once withstardex run. Each contract runs on its own task with its own cursor, so one contract failing is isolated and retried without stalling the rest. Adding or removing a contract takes effect on a running indexer, no restart needed. - Account watching. Watch a Stellar address (
stardex accounts add G...) andstardex runrecords every payment into it, in any asset, including payments sent to its muxed addresses or with a memo. Each one lands in thepaymentstable with its amount, asset, sender and reference, ready to be matched to an invoice. - Resumable ingestion. The cursor is persisted to Postgres, so a restart continues exactly where it left off (verified end-to-end on testnet).
- Real transfer decoding. Token
transferevents, including classic payments in the CAP-67 unified format, are decoded from XDR into typed{ from, to, amount, asset, to_muxed_id }records.to_muxed_idcarries the payment's muxed ID or memo, which is what lets a payment be matched to an invoice. - Decoded events stored in Postgres. Each event runs through the decoder registry and is written to the
eventstable; events without a decoder yet are kept raw, so nothing is lost. - REST API (stardex-backend).
GET /eventsserves the indexed data with filters (contractId,kind, ledger range) and cursor pagination;GET /healthreports DB connectivity. - Typed SDK (stardex-sdk).
@stardex/sdkon npm wraps the API so apps query indexed events in a few lines. - Web app (stardex-frontend). A multi-page React site that browses, filters, and paginates indexed events through the SDK against the live API.
- Streams (webhooks). Subscribe a URL to a contract or event kind and Stardex posts matching events to it as they are indexed, signed with HMAC-SHA256 and retried with backoff through a durable queue. Push, so apps stop polling.
- In progress. The GraphQL API and more decoders (mint/burn, swaps, payment streams).
$ stardex index CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC
storage: Postgres (events + resumable cursor)
indexing CDLZ... via https://soroban-testnet.stellar.org ...
starting from current ledger 2932199
# events land in Postgres, decoded where a decoder exists:
kind | fields
---------+-------------------------------------------------------------------
transfer | {"from":"GBSOLM...","to":"GCMLUV...","amount":"5000000"}
raw | {"topics":["AAAADwAAAANmZWUA", ...],"data":"AAAACv//..."}
Stellar's RPC is built for recent data. It keeps only a short window of history and then prunes it. That makes the most common questions in any app surprisingly hard to answer:
- "Show me every payment this user has made over the last 6 months."
- "What was this contract's volume, day by day?"
- "List all the streams / swaps / mints this contract has ever emitted."
Today, every Soroban team rebuilds the same indexing plumbing from scratch: a service that watches the chain, copies events into a database, and exposes them for querying. It's duplicated effort across the entire ecosystem, with no canonical open-source tool that does it.
Stardex is that plumbing, built once, as a tool everyone can use and self-host. Point it at a contract and it:
- Watches the Stellar/Soroban chain in real time.
- Decodes each contract's events into clean, typed records.
- Stores them durably in Postgres, so old history is never lost and stays fast to search.
- Serves them through a GraphQL + REST API, a typed TypeScript SDK, and a web dashboard.
Stardex is built on one core idea: a neutral ingestion engine that knows how to read the chain, and pluggable decoders that know what each contract's events mean. New contract support is a new decoder, never a change to the engine.
Stellar RPC
(getEvents)
β raw events (XDR)
βΌ
βββββββββββββββββ βββββββββββββββββ ββββββββββββββββ
β Ingestor β β Decoders β β Postgres β
β (core) ββββββΆβ (per-contract ββββββΆβ store β
β β β translators) β β β
β β’ RPC stream β β β’ token/SAC β β β’ events β
β β’ cursors β β β’ swaps β β β’ cursors β
β β’ retry/ β β β’ streams β β β’ migrations β
β backfill β β β’ your own... β β β
βββββββββββββββββ βββββββββββββββββ ββββββββ¬ββββββββ
Rust Rust β
βΌ
βββββββββββββββββββββββββββ
β API (GraphQL + β
β REST), TypeScript β
βββββββββ¬βββββββββββ¬ββββββββ
βΌ βΌ
@stardex/sdk Dashboard
(typed client) (React)
- Neutral core, pluggable edges. The ingestor is opinion-free about event meaning; all protocol knowledge lives in decoders. Adding a contract never risks the engine.
- Resumable by default. Ingestion persists a cursor every page, so restarts, crashes, and deploys never lose or double-process events.
- Self-hostable, no exotic dependencies. Plain Rust, Postgres, TypeScript, React. No ZK, no forked VMs, no special hardware. Clone it and run it.
- Typed all the way up. Raw XDR to typed Rust records to Postgres to typed API to
@stardex/sdk, so apps consume history in a few lines without re-deriving types.
| Component | Lang | Role |
|---|---|---|
Ingestor (ingestor/crates/core) |
Rust | Streams events from RPC, tracks cursors, handles backfill & reorgs. |
Decoders (ingestor/crates/decoders) |
Rust | Per-contract XDR to typed rows. Where most contributions happen. |
CLI (ingestor/crates/cli) |
Rust | stardex add / run to index many contracts, index for one, plus decoders & backfills. |
Store (db/) |
SQL | Postgres schema, migrations, retention. |
| API (stardex-backend) | TypeScript | REST over the indexed data. |
| SDK / types (stardex-sdk) | TypeScript | @stardex/sdk, the typed client and shared types. |
| Web app (stardex-frontend) | React | Multi-page site to browse, filter, and paginate indexed events. |
A decoder turns a raw event into a typed record. That's the whole extension model:
impl Decoder for TokenDecoder {
fn name(&self) -> &'static str { "token" }
fn decode(&self, event: &RawEvent) -> Option<DecodedEvent> {
// recognize a "transfer", pull out from / to / amount ...
}
}Try the live version, no setup: open the dashboard, or query the hosted API directly:
curl "https://stardex-api.onrender.com/events?limit=5"To run your own instance:
Requires Rust, and Docker (or a local Postgres).
# 1. start Postgres, then bring the schema up to date
docker compose up -d
DATABASE_URL=postgres://stardex:stardex@localhost:5432/stardex scripts/migrate.sh
# 2. register the contracts you want to index, then index them all at once
cd ingestor
export DATABASE_URL=postgres://stardex:stardex@localhost:5432/stardex
cargo run -p stardex-cli -- add <CONTRACT_ID>
cargo run -p stardex-cli -- add <ANOTHER_CONTRACT_ID>
cargo run -p stardex-cli -- runstardex run indexes every registered contract concurrently and keeps following the registry, so you can stardex add or stardex remove a contract in another terminal and the running indexer picks it up within a few seconds. Removing keeps everything that contract already indexed; it just stops following it.
cargo run -p stardex-cli -- contracts list # what is being indexed
cargo run -p stardex-cli -- remove <CONTRACT_ID>To stream a single contract without registering it, use stardex index <CONTRACT_ID> (add --once to catch up to the tip and exit, for scheduled jobs). Without DATABASE_URL the single-contract index still runs; the cursor just stays in memory (won't survive a restart). Stop with Ctrl-C; on the next run it resumes from where it left off.
Instead of a whole contract, you can follow the payments into one address:
cargo run -p stardex-cli -- accounts add <G_ADDRESS> --label "My business"
cargo run -p stardex-cli -- runWatching starts from the ledger that was current when you ran accounts add, so payments sent before the watcher first runs are still recorded.
For a scheduled setup with no always-on worker, stardex run --once catches every stream up to the tip and exits (add --accounts-only to skip contract streams). The hosted demo runs run --once --accounts-only and then reconcile --once every 15 minutes from GitHub Actions.
Every transfer paid to that address is stored as a transfer event, and also recorded as a row in payments with its amount, asset, sender and reference (the muxed ID or memo, when present). Outgoing transfers and payments to itself are not recorded as payments, and each event is recorded only once, even if ingestion replays it.
cargo run -p stardex-cli -- payments list --account <G_ADDRESS>
# 2026-09-17T10:03:52Z 5.0000000 XLM from GDKA... ref id:100042 tx 8a41...Payments sent to any M... address built on it arrive under the base G... address with the ID kept, so there is no need to watch muxed addresses separately. Manage watched accounts with accounts list and accounts remove <G_ADDRESS>.
Create an invoice for a watched account. Each one gets a reference number, and the customer pays with it either as a muxed address or as a memo ID:
cargo run -p stardex-cli -- invoices add <G_ADDRESS> 5 --customer "Acme Ltd"
# invoice INV-100001 for 5.0000000 XLM
# pay to: MCN4...GUEKMQ
# or to: GCN4...LN4LGI with memo ID 100001Run the reconcile engine next to stardex run. It matches each recorded payment to its invoice and keeps invoice status up to date:
cargo run -p stardex-cli -- reconcile # add --once to match what is waiting and exit
cargo run -p stardex-cli -- invoices list
# INV-100002 partial 3.0000000 of 5.0000000 XLM ref 100002 (Globex)
# INV-100001 paid 5.0000000 of 5.0000000 XLM ref 100001 (Acme Ltd)A payment with no usable reference, an unknown reference, the wrong asset, or for a cancelled invoice stays unmatched with a reason. See docs/reconciliation.md for exactly how matching works.
Rather than polling /events, subscribe a URL and Stardex posts each matching event to it as it is indexed:
# subscribe a webhook; leave a filter out to match anything
cargo run -p stardex-cli -- subscriptions add https://your-app.example/hooks/stardex \
--contract <CONTRACT_ID> --kind transfer
# run the dispatcher (its own process, so a slow receiver never stalls indexing)
cargo run -p stardex-cli -- streamsFor a scheduled setup with no always-on worker (the hosted demo indexes on a cron), stardex streams --once drains whatever is due and exits, so delivery runs right after each indexing pass.
Each request carries an x-stardex-signature: sha256=<hmac> header, an HMAC-SHA256 of the exact request body using the secret printed when the subscription was created. Verify it before trusting a request:
const want = "sha256=" + createHmac("sha256", SECRET).update(rawBody).digest("hex");The body mirrors what /events returns:
{
"deliveryId": "8412",
"subscriptionId": "3",
"event": { "id": "1542451", "contractId": "C...", "ledger": 3653428,
"kind": "transfer", "fields": { "from": "G...", "amount": "5000000" },
"closedAt": "2026-07-17T14:31:02.000Z" }
}Delivery is at-least-once, so treat deliveryId as an idempotency key. Any 2xx counts as accepted; anything else is retried with exponential backoff and marked dead after 8 attempts. Subscriptions only receive events indexed from the moment the dispatcher first runs, not a replay of existing history. Manage them with subscriptions list and subscriptions remove <id>.
To serve the indexed data over HTTP, run stardex-backend against the same database, and stardex-frontend for the web app. Each repo's README has its setup steps.
| Layer | Stack |
|---|---|
| Ingestor & decoders | Rust, Stellar RPC, Soroban XDR (stellar-xdr) |
| Storage | PostgreSQL + SQL migrations |
| API | TypeScript, Node (stardex-backend) |
| SDK / types | TypeScript, @stardex/sdk on npm (stardex-sdk) |
| Web app | React + Vite + Tailwind (stardex-frontend) |
| Infra | Docker Compose |
stardex/
βββ ingestor/ # Rust workspace: engine + decoders + CLI
β βββ crates/
β βββ core/ # ingestion engine: RPC stream, cursors, streams
β βββ decoders/ # per-contract event decoders
β βββ cli/ # `stardex` command-line tool
βββ db/migrations/ # Postgres schema + migrations
βββ docker-compose.yml # one-command local Postgres
βββ .github/workflows/ # CI and the scheduled ingest job
- M1: Core ingestion. Stream events from RPC, persist a resumable cursor to Postgres.
- M2: Decoders
- token/SAC transfers
- CAP-67 classic payments with asset, muxed ID and memo
- mint/burn, swaps, payment streams, balances over time
- M3: Storage + API
- store decoded events in Postgres
- REST
/eventswith filters + cursor pagination - GraphQL endpoint and more REST endpoints
- M4: SDK + dashboard. Typed
@stardex/sdkclient and a multi-page React UI to explore indexed events. - M5: Decoder ecosystem. Soroswap & streaming decoders, plus a "write your own decoder" guide.
- M6: Ops. Reorg handling, backfill, retention policy, Docker deploy.
- M8: Streams. Push indexed events to subscriber webhooks.
- subscriptions, durable delivery queue, retries with backoff, HMAC-signed payloads (
stardex streams) - authenticated HTTP API to manage subscriptions, plus SDK and dashboard support
- replay a dead delivery, and filter by account as well as contract/kind
- subscriptions, durable delivery queue, retries with backoff, HMAC-signed payloads (
- M7: Multi-contract indexing service.
- register contracts and index them concurrently, isolated per contract (
stardex add/run) - auto-recover a contract whose cursor falls behind the RPC retention window
- add/remove contracts at runtime without a restart (
stardex add/remove) - watch accounts for incoming payments alongside contracts (
stardex accounts add) - record incoming payments with their memo or muxed ID (
paymentstable,stardex payments list)
- register contracts and index them concurrently, isolated per contract (
- M9: Payment reconciliation.
- invoices with a unique reference, paid by muxed address or memo (
stardex invoices add) - match payments to invoices: paid, partial, overpaid, or unmatched with a reason (
stardex reconcile) - invoice, payment and export endpoints in stardex-backend, plus manual matching
- value each payment in a home currency at the time it arrived
- journal entries and sync to Xero and QuickBooks
- invoices with a unique reference, paid by muxed address or memo (
Stardex is built in the open for the Stellar ecosystem. Contributions are welcome, claim an issue, open a PR, and it ships on merge.
- New here? Look for
good first issue. Most are self-contained decoder, API, UI, or docs tasks. - Highest leverage: write a decoder for a contract you already use; the
tokendecoder is a working reference to copy. - Browse all open work in Issues.
See CONTRIBUTING.md for setup in this repo, and the org contributing guide for the rules that apply to every Stardex repo.