Node.js oracle server for the Heliobond platform. It simulates IoT sensor data for solar panel and satellite readings, computes impact scores from that data, and submits update_impact_score transactions to the Soroban ProjectRegistry contract on Stellar. An hourly cron job keeps on-chain scores current automatically; the same logic is exposed over REST for on-demand updates.
flowchart TD
subgraph Client
A[Browser / External caller]
end
subgraph Express["Express (src/index.ts)"]
H[GET /health]
IOT[iot.ts\nGET /api/iot/solar/:id\nGET /api/iot/satellite/:id]
ADMIN[admin.ts\nPOST /api/admin/update-scores\n— Bearer token required]
CRON[node-cron\nhourly @ :00]
end
subgraph Lib["lib modules"]
SC[scoring.ts\npure computation\ncomputeScores]
ST[stellar.ts\nRPC client\nsignAndSubmit]
REG[registry.ts\ncontract calls\nupdateImpactScore\ngetTotalProjects]
end
subgraph Stellar
RPC[Stellar RPC\nsoroban-testnet.stellar.org]
CONTRACT[Soroban\nProjectRegistry\ncontract]
end
A -->|HTTP| H
A -->|HTTP| IOT
A -->|HTTP + Bearer| ADMIN
IOT -->|read-only sim| SC
ADMIN --> SC
ADMIN --> REG
CRON --> SC
CRON --> REG
REG --> ST
ST --> RPC
RPC --> CONTRACT
Data flow for a score update (admin route or cron):
getSolarData(id)andgetSatelliteData(id)produce deterministic, hourly-seeded sensor readings.computeScores({ solar, satellite })derivescredit_qualityandgreen_impact(pure, no I/O).updateImpactScore(id, cq, gi)inregistry.tsbuilds and prepares a Soroban transaction.signAndSubmit(xdr, keypair)instellar.tssigns, submits, and polls until the transaction is confirmed.
Full request/response details, validation rules, and error codes are in API.md.
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/health |
— | Liveness + uptime and last cron run |
GET |
/api/iot/solar/:id |
— | Simulated solar panel reading for project id |
GET |
/api/iot/satellite/:id |
— | Simulated satellite / vegetation reading for project id |
GET |
/api/projects |
— | Paginated list of projects with scores (?limit=&cursor=) |
GET |
/api/projects/:id |
— | Single project detail |
GET |
/api/portfolio/:address |
— | Indexed deposit/withdraw history for an address |
POST |
/api/admin/update-scores |
Bearer token | Submit impact score update(s) to the Soroban contract |
Errors return a consistent { "error": "<code>", "message": "<detail>" } JSON
shape (never a stack trace). All /api/* routes are rate limited and return
429 with a Retry-After header once the limit is exceeded.
{ "status": "ok" }{
"power_output_kw": 742.15,
"efficiency_pct": 74.21,
"max_power_kw": 1000,
"timestamp": 1718150400000
}Readings are deterministic per (project_id, hour) — the same id returns the same values within a given clock hour.
{
"forest_density_pct": 68.44,
"ndvi_score": 0.684,
"timestamp": 1718150400000
}Headers: Authorization: Bearer <ADMIN_API_KEY>
Body (optional):
{ "project_ids": [1, 2, 3] }Omit project_ids (or send an empty array) to update every project registered on-chain (fetched via getTotalProjects()).
Response:
{
"updated": 2,
"results": [
{
"project_id": 1,
"tx_hash": "abc123...",
"credit_quality": 74,
"green_impact": 69
}
],
"errors": []
}Soroban does not support multi-call batching; transactions are submitted sequentially.
Both output values are integers in [0, 100].
credit_quality = clamp(efficiency_pct, 0, 100)
green_impact = clamp(
(power_output_kw / max_power_kw) * 50
+ (forest_density_pct / 100) * 50,
0, 100
)
credit_quality reflects how efficiently the solar array is operating.
green_impact is a 50/50 blend of power production ratio and vegetation health.
Create a .env file (see .env.example):
| Variable | Required | Default | Description |
|---|---|---|---|
STELLAR_NETWORK |
No | testnet |
testnet or mainnet — selects the network passphrase |
ADMIN_SECRET_KEY |
Yes | — | Stellar secret key (S...) used to sign transactions |
PROJECT_REGISTRY_CONTRACT_ID |
Yes | — | Soroban contract address for the ProjectRegistry |
RPC_URL |
No | https://soroban-testnet.stellar.org |
Stellar RPC endpoint |
PORT |
No | 3001 |
HTTP port the server listens on |
FRONTEND_URL |
No | http://localhost:3000 |
Origin allowed by CORS |
ADMIN_API_KEY |
No | — | Bearer token for /api/admin/*. If unset, auth is skipped (dev only) |
RATE_LIMIT_WINDOW_MS |
No | 60000 |
Public rate-limit window (ms) |
RATE_LIMIT_MAX |
No | 100 |
Public max requests per IP per window |
RATE_LIMIT_ADMIN_WINDOW_MS |
No | RATE_LIMIT_WINDOW_MS |
Admin rate-limit window (ms) |
RATE_LIMIT_ADMIN_MAX |
No | 20 |
Admin max requests per IP per window |
Prerequisites: Bun (curl -fsSL https://bun.sh/install | bash).
# 1. Install dependencies
bun install
# 2. Configure environment
cp .env.example .env
# Edit .env — ADMIN_SECRET_KEY and PROJECT_REGISTRY_CONTRACT_ID are required to
# start the server; the rest have sensible defaults.
# 3. Development (ts-node + hourly cron + 5-min indexer)
bun run dev # -> Heliobond backend listening on port 3001
# Verify it's up
curl http://localhost:3001/health
# Production
bun run build && bun start
# Quality gate
bun run build # tsc type-check
bun run test # jest suite| Layer | Technology |
|---|---|
| Runtime | Node.js 20 |
| Language | TypeScript |
| HTTP framework | Express 5 |
| Stellar SDK | @stellar/stellar-sdk v15 |
| Scheduler | node-cron v4 |
| Package manager / test runner | Bun |
| Test framework | Jest + ts-jest + Supertest |