Human-readable Soroban contract explorer. Decodes raw XDR into plain English:
"Address GABC... swapped 100 USDC → 98.7 XLM on StellarSwap at ledger 4521983."
The Octraban Soroban contracts this backend indexes are deployed and verifiable on the Stellar test network:
| Contract | Contract ID | Stellar Explorer |
|---|---|---|
| Explorer / registry | CBKPNRQ4D3KTAAE7MMJ4HL6JNF2J2EBG2PSSRW4YHOMHTRHUU734CFWJ |
View ↗ |
| Ticket | CDX3V6OE72KUIEEJTBLFCQZFXZCAKOYWYXK2KPRM57M6FLZFAVUSVL42 |
View ↗ |
Network:
Test SDF Network ; September 2015· RPC:https://soroban-testnet.stellar.orgContracts live in the octraban_contract repo.
- Node.js + Express + TypeScript
- PostgreSQL + Prisma ORM
- Stellar SDK — Soroban RPC + XDR decoding
- Docker Compose — one-command setup
src/
├── index.ts # Express app entry
├── config.ts # Env config
├── db.ts # Prisma client
├── api/
│ ├── router.ts # Route aggregator
│ ├── transactions.ts # GET /transactions
│ ├── events.ts # GET /events
│ ├── contracts.ts # GET/POST /contracts (ABI registry)
│ ├── wallets.ts # GET /wallets/:address
│ └── tokens.ts # GET /tokens (SEP-41)
└── indexer/
├── rpc.ts # Stellar RPC client
├── registry.ts # ABI registry + SEP-41 built-in ABI
├── decoder.ts # XDR → human-readable decoder
├── indexer.ts # Ledger polling loop
└── run.ts # Indexer entry point
cp .env.example .env
docker compose upcp .env.example .env
# edit .env with your DB URL and RPC endpoint
npm install
npx prisma migrate dev
npm run seed # seed known contracts (StellarSwap etc.)
npm run dev # start API server
npm run index # start indexer (separate terminal)| Method | Path | Description |
|---|---|---|
| GET | /api/v1/transactions |
List transactions (filter: contract, account, status) |
| GET | /api/v1/transactions/:hash |
Transaction detail + events |
| GET | /api/v1/events |
List events (filter: contract, type) |
| GET | /api/v1/events/:id |
Event detail |
| GET | /api/v1/contracts |
List registered contracts |
| GET | /api/v1/contracts/:address |
Contract detail + recent txs/events |
| POST | /api/v1/contracts |
Register contract ABI metadata |
| GET | /api/v1/wallets/:address/transactions |
Wallet transaction history |
| GET | /api/v1/wallets/:address/events |
Wallet event history |
| GET | /api/v1/tokens |
List SEP-41 tokens |
| GET | /api/v1/tokens/:address |
Token detail |
| GET | /api/v1/tokens/:address/transfers |
Token transfer history |
| GET | /health |
Health check |
curl -X POST http://localhost:3000/api/v1/contracts \
-H "Content-Type: application/json" \
-d '{
"address": "CXXX...",
"name": "MyDEX",
"abi": {
"functions": [{
"name": "swap",
"inputs": [
{ "name": "from", "type": "address" },
{ "name": "amount_in", "type": "i128" },
{ "name": "amount_out", "type": "i128" }
],
"humanTemplate": "{from} swapped {amount_in} → {amount_out} on MyDEX"
}]
}
}'The Octraban backend requires an RS256 key pair to sign and verify JWT tokens securely.
In production, you must supply your own keys. Generate them using OpenSSL:
# Generate a 2048-bit RSA private key
openssl genrsa -out private.pem 2048
# Extract the public key
openssl rsa -in private.pem -pubout -out public.pemSet these in your environment variables JWT_PRIVATE_KEY and JWT_PUBLIC_KEY. Note that multi-line PEM strings must have their newlines replaced with literal \n characters in some environments (like .env files).
The JWT_KEY_ID uniquely identifies the active key. This supports zero-downtime key rotation.
Because the server caches keys, a typical rotation involves:
- Generating a new key pair and assigning it a new
JWT_KEY_ID(e.g.,key_2). - Updating the environment variables to use the new keys.
- Restarting the service. New tokens are signed with
key_2.
Note: Never log or commit the JWT_PRIVATE_KEY to version control. The server performs a startup validation check and will crash if the keys are missing or malformed when NODE_ENV=production.
| Variable | Default | Description |
|---|---|---|
DATABASE_URL |
— | PostgreSQL connection string |
STELLAR_NETWORK |
testnet |
testnet or mainnet |
STELLAR_RPC_URL |
testnet RPC | Soroban RPC endpoint |
HORIZON_URL |
testnet Horizon | Horizon API endpoint |
NETWORK_PASSPHRASE |
testnet | Network passphrase |
INDEXER_START_LEDGER |
0 |
Ledger to start indexing from |
INDEXER_POLL_INTERVAL_MS |
5000 |
Polling interval |
INDEXER_BATCH_SIZE |
100 |
Ledgers per batch |
ADMIN_SECRET |
— | Bearer token required by every /api/admin/* route (indexer). See docs/ADMIN_AUTH.md. |
Tracing is off by default and never starts unless you explicitly enable it. A missing or unreachable collector is handled gracefully — it logs a warning and the service continues without tracing.
| Variable | Default | Description |
|---|---|---|
TRACING_ENABLED |
(unset — tracing is off) | Set to true or 1 to start the OpenTelemetry SDK |
OTLP_ENDPOINT |
http://localhost:4318 |
OTLP collector base URL. Setting this variable alone also enables tracing. |
SERVICE_NAME |
octraban |
Service name reported in traces |
# Start a local Jaeger all-in-one (OTLP HTTP on :4318)
docker run -d -p 16686:16686 -p 4318:4318 jaegertracing/all-in-one:latest
# Enable tracing in your .env
TRACING_ENABLED=true
OTLP_ENDPOINT=http://localhost:4318Spans are flushed cleanly on SIGTERM and SIGINT.
STELLAR_NETWORK=mainnet
STELLAR_RPC_URL=https://mainnet.stellar.validationcloud.io/v1/<API_KEY>
HORIZON_URL=https://horizon.stellar.org
NETWORK_PASSPHRASE=Public Global Stellar Network ; September 2015This repository is the backend tier of the Octraban stack, split across three repos:
- octraban_frontend — Vite/React explorer UI. Reads from the indexer via
VITE_INDEXER_URL(defaulthttp://localhost:3001). - octraban_backend (this repo) — the API service plus the indexer (under
indexer/) that ingests on-chain data.- API service:
PORT=3000 - Indexer API:
PORT=3001(what the frontend queries)
- API service:
- octraban_contract — Soroban smart contracts (explorer, ticket) deployed to testnet.
- Start the indexer (
indexer/) → serves on:3001. - Start the API (root) → serves on
:3000. - Point the frontend at the indexer:
VITE_INDEXER_URL=http://localhost:3001. - Set
TESTNET_RPC_URL=https://soroban-testnet.stellar.orgfor chain access.
octraban_backend ships two independent, separately-runnable Node processes. They are not
interchangeable — new contributors should read this section before deciding what to run.
| API service | Indexer service | |
|---|---|---|
| Entrypoint | src/index.ts (httpServer.listen(config.port, ...)) |
indexer/src/index.js → indexer/src/api.js (server.listen(PORT, ...)) |
| Run with | npm run dev / npm start (root) |
cd indexer && npm run dev / npm start |
| Port | 3000 (PORT env, root .env) |
3001 (PORT env, indexer/.env) |
| Responsibility | Registers contract ABIs, serves GraphQL/billing/WebSocket features, and runs an in-process copy of the TS indexer (src/indexer/) that also writes to the same Postgres DB. |
Polls Soroban RPC for ledgers/events, decodes them, persists them to Postgres, and serves the REST/GraphQL API that the frontend actually calls. |
| Depends on | PostgreSQL (Prisma, DATABASE_URL), optional Redis (rate-limit store), Soroban RPC |
PostgreSQL (pg, DATABASE_URL), optional Redis (REDIS_URL, caching/pub-sub), Soroban RPC (SOROBAN_RPC_URL) |
The frontend (
octraban_frontend) queries the indexer service on:3001, not the API service on:3000.src/index.tsis a second, largely independent API — running it is optional for a working explorer UI, but the two processes share the same Postgres database.
The indexer is still plain JavaScript, unlike the root API's TypeScript. It's being brought
under type checking incrementally (npm run typecheck in indexer/, wired into CI) — see
Indexer Type Checking in CONTRIBUTING.md for the
approach and which modules are covered so far.
Both processes migrate the same database with two independent migration systems (Prisma for
the API, a small SQL runner for the indexer). See
docs/database-ownership.md for which tables each one owns and
for npm run db:bringup, the single command that brings a fresh database to a working state
for both.
Soroban RPC → indexer (indexer/src/index.js) → PostgreSQL → indexer API :3001 → frontend
↑
src/index.ts (API :3000, optional, same DB)
# 1. Indexer service — what the frontend needs (:3001)
cd indexer
cp .env.example .env # set DATABASE_URL, SOROBAN_RPC_URL
npm install
npm run dev # or: npm start
# 2. API service — optional, ABI registry / GraphQL / billing (:3000)
cd ..
cp .env.example .env # set DATABASE_URL, TESTNET_RPC_URL, etc.
npm install
npx prisma migrate dev
npm run dev # start API server
npm run index # start the in-process TS indexer poller (separate terminal)The octraban_frontend nginx configuration proxies all /api/ traffic to a Docker host
named indexer on port 3001:
location /api/ { proxy_pass http://indexer:3001; }This repository's docker-compose.yml defines per-network services named
indexer-testnet, indexer-mainnet, and indexer-devnet. To make the frontend's proxy
target resolve correctly without renaming the services, each indexer service declares a
Docker network alias of indexer on the default Compose network:
indexer-testnet:
networks:
default:
aliases:
- indexerThis means a frontend container joined to the same Compose network can always reach the
active indexer at http://indexer:3001, regardless of which network profile is active.
# Copy and configure environment variables
cp .env.example .env
# Set at minimum: POSTGRES_PASSWORD
# Start the testnet stack (db + api + indexer)
docker compose up
# The indexer is now reachable as both:
# http://indexer-testnet:3001 (service name)
# http://indexer:3001 (alias — what the frontend uses)For mainnet or devnet, activate the corresponding profile:
docker compose --profile mainnet up
docker compose --profile devnet upNote for frontend contributors: Set
VITE_INDEXER_URL=http://localhost:3001when running the frontend outside Docker. Inside a shared Compose network, usehttp://indexer:3001.