Express API for StellarYield. It indexes on-chain data, stores it in PostgreSQL, and exposes REST endpoints for vault, user, and yield data.
- Node.js 20+
- npm
- Docker and Docker Compose
From this backend/ directory:
docker compose up --buildThe API is available at http://localhost:3000.
Check health:
curl http://localhost:3000/healthRun database migrations once:
docker compose --profile migrate run --rm db-migrateStop services:
docker compose downRemove the local PostgreSQL volume:
docker compose down -vCreate a local environment file:
cp .env.example .envInstall dependencies, build, migrate, and start:
npm ci
npm run build
npm run db:migrate
npm startFor development with file watching:
npm run devnpm run build- compile TypeScript todist/.npm start- runnode dist/index.js.npm run dev- run the API withtsx watch.npm run lint- lint files undersrc/.npm test- run the Vitest suite.npm run db:migrate- applysrc/db/schema.sqlto PostgreSQL.npm run indexer- run the event indexer.npm run operator-expiry-task- run operator expiry background task.
| Name | Required | Default | Description |
|---|---|---|---|
PORT |
No | 3000 |
HTTP server port. |
NODE_ENV |
No | development |
Runtime environment. |
DATABASE_URL |
Yes | none | PostgreSQL connection string. |
STELLAR_NETWORK |
No | testnet |
Stellar network name. |
STELLAR_RPC_URL |
No | Soroban testnet RPC | Stellar RPC endpoint. |
STELLAR_NETWORK_PASSPHRASE |
No | Testnet passphrase | Network passphrase. |
VAULT_FACTORY_CONTRACT_ID |
Recommended | empty | Vault factory contract ID. Required for event indexing. If empty, the indexer will skip event polling and only update indexer_state, logging a warning at startup. |
ZKME_VERIFIER_CONTRACT_ID |
No | empty | zkMe verifier contract ID. |
INDEXER_START_LEDGER |
No | 0 |
Ledger to begin indexing from. |
INDEXER_POLL_INTERVAL_MS |
No | 5000 |
Indexer polling interval. |
WEBHOOK_SECRET |
No | empty | Optional webhook signing secret. |
ADMIN_API_KEY |
No | empty | Admin API authentication key. |
Docker Compose reads .env.example and overrides DATABASE_URL so the backend
connects to the postgres service.
GET /health- service and database health check.GET /api/v1/vaults- list vaults.GET /api/v1/vaults?q=bond- search vaults by name or symbol using full-text search.GET /api/v1/vaults/count- return the total number of vaults.GET /api/v1/vaults/factory/:factoryId- list vaults for a factory.GET /api/v1/vaults/:contractId- get a vault by contract ID.GET /api/v1/vaults/:contractId/operators- list active operators (filters expired).GET /api/v1/vaults/:contractId/positions- list vault positions.GET /api/v1/vaults/:contractId/holders?page=&pageSize=&sort=- list active vault holders, sorted bysharesordeposited.GET /api/v1/vaults/:contractId/holders/count- return the active holder count for a vault.GET /api/v1/vaults/:contractId/holders/export.csv- export active vault holders as a protected CSV attachment.GET /api/v1/vaults/:contractId/early-redemption-fee?shares=- preview the early redemption fee breakdown for a share amount.GET /api/v1/vaults/:contractId/export.csv- export vault data as a CSV attachment.GET /api/v1/users/:address- get a user by Stellar address.GET /api/v1/users/:address/kyc?vaultId=:contractId- live-read on-chain KYC status for a vault.GET /api/v1/users/:address/portfolio- get a user's portfolio.GET /api/v1/users/:address/yield-history- user yield history.GET /api/v1/users/:address/deposits- user deposits.GET /api/v1/users/:address/share-history?vaultId=:contractId- get epoch-ordered share balance history; omitvaultIdto aggregate across vaults by epoch.POST /api/v1/users/portfolios/batch- batch-fetch portfolios for up to 50 addresses ({ addresses: string[] }).GET /api/v1/yields/:contractId/epochs- list vault yield epochs.GET /api/v1/yields/:contractId/pending/:userAddress- get pending yield.
Require X-API-Key header with admin key.
POST /api/v1/admin/indexer/replay- replay events for a ledger range.GET /api/v1/admin/vaults/:contractId/audit- audit log.GET /api/v1/admin/events- indexed events.
- Webhook Documentation - Webhook payload schemas and verification
- Indexer Architecture - Polling loop, event dispatch, dedup, and backfill
- Indexer Event Reference - Every event parser, its DB effect, and its webhook
The vault lifecycle consists of the following states:
| State | Description | Triggered By |
|---|---|---|
Funding |
Initial state. Vault accepts deposits and aims to meet funding target before deadline. | Vault creation via factory |
Active |
Funding target met before deadline. Vault is operational and distributes yield. | Operator calls activate_vault after funding deadline passes with target met |
Matured |
Funding period ended. No new deposits accepted; yield distribution and redemptions continue. | Operator calls mature_vault |
Cancelled |
Funding deadline passed without meeting target. Depositors can withdraw refunds. | Operator calls cancel_funding (via cancel_funding event) |
Closed |
Vault fully wound down. All shares redeemed or refunded. | Operator action |
To fetch all cancelled vaults via the API:
GET /api/v1/vaults?state=CancelledExample response:
{
"data": [
{
"id": 1,
"contractId": "CDLZFC3SYJYHZDQA6M57EYUC2XBDA6LQF3M6KFRDZ7TXJYJL2K3B",
"state": "Cancelled",
"totalAssets": "0",
"totalSupply": "0",
...
}
],
"total": 1,
"page": 1,
"pageSize": 20
}The API supports PostgreSQL full-text search for efficient vault discovery by name or symbol. The search uses:
- GIN index on a generated
tsvectorcolumn for fast indexed queries - Relevance ranking via
ts_rank()to return most relevant results first - English language dictionary for stemming and stop-word removal
Use the q parameter with GET /api/v1/vaults to search:
GET /api/v1/vaults?q=bondThis returns vaults containing "bond" in their name or symbol, ranked by relevance.
- Empty or missing
q: Returns all vaults with standard sorting - With
q: Filters by search match and ranks by relevance (ts_rank) - Combines with filters: Works alongside
state,page,pageSize, etc.
Search for "bond" vaults:
curl "http://localhost:3000/api/v1/vaults?q=bond"Search active "treasury" vaults:
curl "http://localhost:3000/api/v1/vaults?q=treasury&state=Active"Search with pagination:
curl "http://localhost:3000/api/v1/vaults?q=yield&page=1&pageSize=10"- Search column:
search_vector tsvector GENERATED ALWAYS AS (to_tsvector('english', COALESCE(name, '') || ' ' || COALESCE(symbol, ''))) STORED - Index:
CREATE INDEX idx_vaults_search_vector ON vaults USING GIN (search_vector) - Query:
WHERE search_vector @@ plainto_tsquery('english', $q) - Ranking:
ORDER BY ts_rank(search_vector, plainto_tsquery('english', $q)) DESC