Base URL (local): http://localhost:3001
Frontend integration: set
NEXT_PUBLIC_API_URLto the versioned base and include/v1, e.g.http://localhost:3001/v1. The frontend calls${NEXT_PUBLIC_API_URL}/projects; a base without/v1resolves to the deprecated/apipaths (or404).
All REST responses default to JSON unless an export format (e.g. format=csv) is explicitly requested.
Errors follow a consistent structure:
{
"error": {
"code": "bad_request",
"message": "project id must be a positive integer"
}
}code is a stable, machine-readable identifier for programmatic handling; message provides human-readable detail.
| Status | error.code |
When |
|---|---|---|
400 |
bad_request |
Invalid parameters, body validation error, or malformed JSON |
400 |
invalid_telemetry |
Telemetry payload matches neither accepted schema |
401 |
unauthorized |
Missing or invalid authentication credentials / bearer token |
403 |
forbidden |
Client IP not whitelisted or role insufficient |
404 |
not_found |
Resource or unknown route does not exist |
413 |
payload_too_large |
Request body exceeds the configured size limit |
429 |
too_many_requests |
Rate limit exceeded (check Retry-After header) |
500 |
server_misconfigured |
Admin endpoint called without ADMIN_API_KEY configured |
500 |
internal_error |
Unexpected server error |
The platform supports multiple authentication schemes depending on the endpoint category:
- Admin Bearer Token: Required for administrative operations under
/v1/admin/*. Pass via headerAuthorization: Bearer <ADMIN_API_KEY>. - Role-Based Access Control (RBAC): Required for
/v1/roles/*endpoints. Pass user identifier viaX-User-Id: <user_id>. Valid roles:admin,operator,viewer. - Consumer API Keys: External consumers authenticate with keys generated via
/v1/admin/api-keys. Pass viaAuthorization: Bearer <key>orX-API-Key: <key>. - IP Whitelisting: Certain sensitive management endpoints (
/v1/admin/*,/v1/roles,/v1/webhooks,/v1/panels,/v1/metadata,/v1/email,/v1/scoring/formulas,/v1/chains,/v1/satellite-sources) restrict access according to configured IP whitelist ranges when enabled.
Endpoints enforce rate limiting per client IP or authenticated API key. Standard rate limit headers are included in responses:
RateLimit-Limit: Maximum requests permitted per windowRateLimit-Remaining: Remaining requests in current windowRateLimit-Reset: Seconds until quota resetsRetry-After: Included on429 Too Many Requestsresponses
Configurable environment variables (see .env.example):
- Public tier:
RATE_LIMIT_WINDOW_MS,RATE_LIMIT_MAX - Admin tier:
RATE_LIMIT_ADMIN_WINDOW_MS,RATE_LIMIT_ADMIN_MAX - Telemetry tier (
POST /v1/telemetry):TELEMETRY_RATE_LIMIT_WINDOW_MS,TELEMETRY_RATE_LIMIT_MAX
All current routes are mounted under the /v1 prefix.
Legacy unversioned /api/* routes are deprecated and maintained for backward compatibility until 2027-01-01. Responses on /api/* include Deprecation: true and sunset warning headers.
Replace the /api prefix with /v1 (e.g. /api/projects → /v1/projects). Notes:
- Auth is not identical between the two mounts (for example
/api/portfoliorequires an API key while/v1/portfoliodoes not; admin routes on/v1additionally require request signing). Follow the/v1requirements. - Legacy traffic is counted in the Prometheus metric
legacy_api_requests_total{path}; check it to find remaining callers. - On and after 2027-01-01T00:00:00Z every
/api/*request returns410 Gonewith aLink: </v1/...>; rel="successor-version"header and a JSON body pointing at the/v1equivalent. The mounts are then deleted; see docs/LEGACY_API_REMOVAL.md.
| Route Group | Base Path | Auth Requirement | Rate Limit Tier | Description |
|---|---|---|---|---|
| System & Health | /health, /ready, /metrics, /docs |
Public | None / Default | Liveness, readiness, Prometheus metrics, and OpenAPI/Swagger documentation |
| Telemetry (IoT) | /v1/iot |
Public / Consumer Key | Public | Simulated solar panel readings and satellite NDVI vegetation telemetry |
| Telemetry (Client) | /v1/telemetry |
Public | Telemetry | Frontend error-report and Web Vitals beacon ingest with PII scrubbing |
| Projects | /v1/projects |
Public / Consumer Key | Public | Paginated, filterable, and sortable project registry and detail |
| Score History | /v1/projects/:id/history |
Public / Consumer Key | Public | Historical score logs and score direction trend evaluation |
| Aggregation | /v1/projects/aggregate |
Public / Consumer Key | Public | Portfolio-level aggregate score calculations by category and region |
| Portfolio | /v1/portfolio |
Public / Consumer Key | Public | Investor deposit/withdrawal history, token shares, and valuation |
| Metadata | /v1/metadata |
IP Whitelist | Admin | Descriptive project metadata, geolocation, coordinates, and tags |
| Panels Hardware | /v1/panels |
IP Whitelist | Admin | Solar panel technical specifications and effective capacity calculation |
| Dashboard | /v1/dashboard |
Public / Consumer Key | Public | Portfolio summaries, top/bottom performers, score distributions, and CSV export |
| Comparison | /v1/comparison |
Public / Consumer Key | Public | Side-by-side multi-project metric comparisons and ranked lists |
| Benchmarking | /v1/benchmarking |
Public / Consumer Key | Public | Standard/custom industry benchmark definitions, percentiles, and alerts |
| Financial Modeling | /v1/financial |
Public / Consumer Key | Public | NPV, discounted payback, cost-benefit analysis, and parameter sensitivity |
| Forecasting | /v1/forecast |
Public / Consumer Key | Public | Time-series forecasting (ARIMA, smoothing, regression), weather adjustments, accuracy |
| Maintenance | /v1/maintenance |
Public / Consumer Key | Public | Predictive failure modeling, work order tasks, scheduling calendar, maintenance logs |
| Anomaly Detection | /v1/anomaly |
Public / Consumer Key | Public | Z-score anomaly detection on live IoT telemetry with configurable thresholds |
| Investor Reports | /v1/investor |
Public / Consumer Key | Public | High-level executive summaries, ESG compliance reports, and custom PDF/JSON reports |
| Webhooks | /v1/webhooks |
IP Whitelist | Admin | Webhook registration, HMAC secret configuration, and retry management |
| Email Digests | /v1/email |
IP Whitelist | Admin | Digest subscriptions, unsubscribe tokens, alert thresholds, and email templates |
| Multi-Chain | /v1/chains |
IP Whitelist | Admin | Multi-blockchain network management and cross-chain score broadcasting |
| Satellite Sources | /v1/satellite-sources |
IP Whitelist | Admin | Satellite imagery provider priorities, adapter health checks, and fallback routing |
| Scoring Formulas | /v1/scoring/formulas |
IP Whitelist | Admin | Custom impact scoring formulas, metric weighting, and A/B score preview |
| Oracle & Admin | /v1/admin |
Bearer Token / IP Whitelist | Admin | Soroban smart contract oracle score updates and immutable audit logging |
| Batch Operations | /v1/admin/batch |
IP Whitelist | Admin | Asynchronous multi-project score update batch jobs with concurrency controls |
| Consumer API Keys | /v1/admin/api-keys |
Bearer Token / IP Whitelist | Admin | Generation, usage monitoring, scheduled rotation, and revocation of client API keys |
| RBAC Roles | /v1/roles |
RBAC (X-User-Id) |
Admin | User role assignment (admin, operator, viewer) and authorization policies |
| System Operations | /v1/admin/*, /v1/traces |
IP Whitelist / Admin | Admin | DB migrations, secret rotation, dynamic log levels, compression stats, OpenTelemetry traces |
| GraphQL API | /graphql |
Bearer / API Key | Public / Admin | Flexible GraphQL query and mutation endpoint with GraphiQL playground |
| gRPC Service | localhost:50051 |
Metadata Auth | RPC | High-performance unary and streaming gRPC interface |
Liveness check and cron job execution status. Not rate limited.
Response 200
{
"status": "ok",
"uptime_seconds": 3712,
"started_at": "2026-06-26T18:00:00.000Z",
"last_cron_run": {
"name": "score-update",
"status": "success",
"at": "2026-06-26T19:00:00.123Z"
}
}Readiness probe for load balancers and container orchestrators.
Response 200 / 503
{
"status": "ready",
"checks": {
"database": "connected",
"stellar_rpc": "available"
}
}Standard Prometheus format metrics endpoint for scraping.
Interactive Swagger UI explorer (/docs) and raw OpenAPI 3.0.3 specification (/docs.json).
Aggregated operational metrics dashboard (request counts, latency percentiles, error rates).
Response 200
{
"requests_total": 14205,
"error_rate_pct": 0.04,
"avg_latency_ms": 18.2,
"uptime_seconds": 3712
}Exports distributed trace spans collected via OpenTelemetry.
| Param | In | Type | Description |
|---|---|---|---|
correlation_id |
query | string | Optional correlation ID filter |
limit |
query | int | Max spans to return (default: 100, max: 500) |
since |
query | int | Unix ms timestamp filter |
Response 200
{
"summary": {
"total_spans": 240,
"active_traces": 4
},
"spans": [
{
"trace_id": "9a12b48fe3...",
"span_id": "7c88d12...",
"name": "calculateNPV",
"duration_ms": 1.4,
"timestamp": 1718150400000
}
]
}Ingest frontend error reports and Web Vitals measurements (#770). The browser
sends the payload with navigator.sendBeacon as a text/plain Blob containing
a JSON string; application/json is also accepted. No authentication, API key,
or CSRF token is required — the route carries no cookies/session and browser
beacons cannot set custom headers. The route is rate limited per client IP.
Body size is bounded by TELEMETRY_BODY_SIZE_LIMIT (default 64kb) for
text/plain; application/json also remains bounded by BODY_SIZE_LIMIT.
ErrorReport
| Field | Type | Required | Notes |
|---|---|---|---|
kind |
string | Yes | Metric label; [A-Za-z0-9_.:-]{1,64} |
message |
string | Yes | 1–8192 chars; PII-scrubbed before use |
stack |
string | No | Up to 16384 chars |
url |
string | No | Up to 2048 chars |
userAgent |
string | No | Up to 1024 chars |
contractErrorCode |
number | string | No | Soroban error code (e.g. 22) |
contractErrorName |
string | No | Metric label; [A-Za-z0-9_.:-]{1,64}; defaults to none |
timestamp |
number | No | Client epoch-ms |
WebVitalReport
| Field | Type | Required | Notes |
|---|---|---|---|
name |
string | Yes | One of CLS, FCP, INP, LCP, TTFB, FID |
value |
number | Yes | Finite, >= 0 |
rating |
string | Yes | One of good, needs-improvement, poor |
id |
string | No | Measurement id |
delta |
number | No | Delta since previous report |
navigationType |
string | No | Navigation type |
timestamp |
number | No | Client epoch-ms |
Security / PII
A second scrub pass rejects/strips anything resembling a Stellar address
(G...), secret seed (S...), contract id (C...), XDR/base64 blob, or e-mail
address. Reports are never persisted; only validated label fields reach the
Prometheus registry. A redacted value used as a metric label fails validation
and returns 400.
Metrics
frontend_errors_total{kind, contract_error_name}— counterfrontend_web_vital{name, rating}— histogram (exposed asfrontend_web_vital_bucket/_sum/_count)
Responses
| Status | Body / meaning |
|---|---|
204 |
Accepted; empty body |
400 |
invalid_telemetry — malformed JSON or neither schema matches |
413 |
payload_too_large — body over TELEMETRY_BODY_SIZE_LIMIT |
429 |
too_many_requests — per-IP telemetry rate limit exceeded |
curl -X POST http://localhost:3001/v1/telemetry \
-H 'Content-Type: text/plain' \
--data '{"kind":"runtime","message":"boom","contractErrorName":"InvalidScore"}'
# -> 204 No ContentThe unversioned alias
POST /api/telemetryis deprecated like the rest of/api/*.
Frontend follow-up: the frontend lives in a separate repository. Set
NEXT_PUBLIC_ERROR_REPORT_URLto<api-base>/v1/telemetryin the frontend's.env.example. Optionally setTELEMETRY_OTLP_ENABLED=trueon the backend to forward accepted reports to the OTLP exporter.
Simulated solar panel telemetry for project id. Deterministic per (project_id, clock_hour).
| Param | In | Type | Rules |
|---|---|---|---|
id |
path | int | Positive integer (>= 1, <= MAX_PROJECT_ID) |
Response 200
{
"power_output_kw": 742.15,
"efficiency_pct": 74.21,
"max_power_kw": 1000,
"timestamp": 1718150400000
}Simulated satellite / vegetation index reading for project id.
Response 200
{
"forest_density_pct": 68.44,
"ndvi_score": 0.684,
"timestamp": 1718150400000
}Paginated, filterable list of projects with latest scores and telemetry.
| Param | In | Type | Rules | Default |
|---|---|---|---|---|
page |
query | int | 1-based page number (>= 1) |
1 |
pageSize |
query | int | Integer 1..100 |
10 |
limit |
query | int | Alias for pageSize (kept for existing clients) |
10 |
cursor |
query | int | Legacy offset alias. When present it is applied as the offset and takes precedence over page |
— |
min_score |
query | number | Minimum credit quality score filter | — |
max_score |
query | number | Maximum credit quality score filter | — |
min_date |
query | number | Minimum timestamp (ms) | — |
max_date |
query | number | Maximum timestamp (ms) | — |
sort_by |
query | string | One of: id, credit_quality, green_impact, power_output_kw, efficiency_pct, forest_density_pct, ndvi_score, timestamp |
id |
sort_order |
query | string | asc or desc |
asc |
Response 200 (PaginatedProjectsResponse)
{
"projects": [
{
"id": 1,
"credit_quality": 74,
"green_impact": 69,
"power_output_kw": 742.15,
"efficiency_pct": 74.21,
"forest_density_pct": 68.44,
"ndvi_score": 0.684,
"timestamp": 1718150400000
}
],
"total": 50,
"filtered_total": 50,
"page": 1,
"pageSize": 10,
"hasMore": true,
"cursor": 10
}cursor is returned only when another page follows; it carries the next
offset, so cursor clients (?cursor=10&limit=10) keep working unchanged.
Detailed metrics and funding data for a specific project, nested as the
frontend's ProjectWithDetail shape: {project, detail, verifiedMetadata}.
project: identity and impact scores (id,credit_quality,green_impact).detail: telemetry and funding (power_output_kw,efficiency_pct,forest_density_pct,ndvi_score,timestamp,funding).verifiedMetadata: whether the backend holds metadata for the project.
Returns 404 not_found for ids that do not exist on the registry, including
ids that were never issued and projects that were deleted or compacted
(existence is read from the contract's get_project(id) getter).
Response 200
{
"project": {
"id": 1,
"credit_quality": 74,
"green_impact": 69
},
"detail": {
"power_output_kw": 742.15,
"efficiency_pct": 74.21,
"forest_density_pct": 68.44,
"ndvi_score": 0.684,
"timestamp": 1718150400000,
"funding": 482910.55
},
"verifiedMetadata": false
}Historical score logs for project id. Supports CSV export.
| Param | In | Type | Description |
|---|---|---|---|
from |
query | int | Starting Unix ms timestamp |
to |
query | int | Ending Unix ms timestamp |
format |
query | string | json (default) or csv |
Response 200 (JSON)
{
"project_id": 1,
"count": 2,
"entries": [
{
"project_id": 1,
"credit_quality": 74,
"green_impact": 69,
"recorded_at": 1718150400000
}
]
}Evaluates score direction (improving, declining, stable) over time.
Response 200
{
"project_id": 1,
"trend": "improving",
"net_delta": 4.5,
"data_points": 12
}Powers the frontend's price/yield chart (PricePoint[] = {date, price, yield?}).
Values are derived from real on-chain state:
date: ISO date string (YYYY-MM-DD). Daily buckets by default; weekly buckets are aligned to the Monday of the containing ISO week (UTC).yield: percentage yield, computed asrate_bps / 100.rate_bpscomes from the registry'sget_interest_rate(project_id).price: one-period present value of a par-100 bond at that yield:price = 100 / (1 + yield / 100). Rounded to two decimal places.
Timestamps for the series come from get_score_history(project_id) (a 50-entry
ring buffer on the contract). Historical rate is not yet indexed from the
RateUpdated / ScoreChanged events (paired backend work), so every point
currently carries the CURRENT yield and price. When the event indexer lands
the response shape stays the same and only the per-entry rate lookup changes.
| Param | In | Type | Rules |
|---|---|---|---|
id |
path | int | Project id (>= 1) |
from |
query | int | Start timestamp (unix ms). Optional. |
to |
query | int | End timestamp (unix ms). Must be >= from. |
interval |
query | string | day (default) or week |
Responses:
200:{project_id, interval, count, points}withpointsin ascending date order. SetsCache-Control: public, max-age=60.400:bad_requestwhenfrom > toorintervalis notday/week.404:not_foundwhen the project is unknown or archived on the registry.
Response 200
{
"project_id": 27,
"interval": "day",
"count": 3,
"points": [
{ "date": "2026-09-28", "price": 96.15, "yield": 4 },
{ "date": "2026-09-29", "price": 96.15, "yield": 4 },
{ "date": "2026-09-30", "price": 96.15, "yield": 4 }
]
}Portfolio-level aggregated impact and credit quality metrics across projects.
| Param | In | Type | Rules |
|---|---|---|---|
limit |
query | int | Number of projects to aggregate (1..100, default: 20) |
cursor |
query | int | Offset (default: 0) |
category |
query | string | Optional filter: solar, forest, wind |
region |
query | string | Optional filter: north, south, east, west |
Response 200
{
"project_count": 20,
"avg_credit_quality": 78.4,
"avg_green_impact": 72.1,
"total_power_output_kw": 14200.5,
"cursor": 20,
"limit": 20
}Indexed deposit/withdrawal transaction history, share count, and position value for a Stellar account address. The
simulated share price is seeded from the address, so current_value is
deterministic per (address, clock hour) — the same address returns the same
value within a clock hour, and current_value stays between 1.5x and 2.0x
of current_shares.
Response 200
{
"address": "GBBD...24KL",
"current_shares": 42,
"current_value": 71.25,
"events": [
{
"id": "1234-abcdef",
"type": "deposit",
"amount": 500,
"shares": 42,
"timestamp": 1718150400000,
"txHash": "abcdef..."
}
]
}Retrieves project descriptive metadata (name, description, location, geographic coordinates, tags, and attached panel specs).
Response 200
{
"project_id": 1,
"name": "Sahara Sol I",
"description": "High-efficiency utility-scale solar farm",
"location": "North Africa",
"coordinates": { "latitude": 27.12, "longitude": 13.18 },
"tags": ["utility", "clean-energy"],
"panel_config": {
"project_id": 1,
"model": "SunPower Maxeon 6",
"manufacturer": "SunPower",
"panel_count": 2500,
"wattage_per_panel": 400,
"effective_capacity_kw": 1000
}
}Creates, replaces, or updates project metadata.
Request Body
{
"name": "Sahara Sol I",
"description": "Expanded 1.2MW capacity solar array",
"location": "North Africa",
"coordinates": { "latitude": 27.12, "longitude": 13.18 },
"tags": ["solar", "expansion"]
}Retrieves solar panel hardware specs and calculates effective capacity (panel_count * wattage_per_panel / 1000).
Configures solar panel specifications for project id.
Request Body
{
"model": "SunPower Maxeon 6",
"manufacturer": "SunPower",
"panel_count": 2500,
"wattage_per_panel": 400,
"tilt_angle": 25,
"azimuth": 180,
"panel_type": "monocrystalline",
"installation_date": "2024-01-15"
}High-level overview of total projects, average credit scores, average green impact, and total power generation.
Response 200
{
"total_projects": 50,
"avg_credit_quality": 76.5,
"avg_green_impact": 71.2,
"total_power_output_kw": 36500.8
}Ranks top and bottom performing projects across the portfolio.
| Param | In | Type | Rules | Default |
|---|---|---|---|---|
limit |
query | int | Integer 1..50 |
5 |
Response 200
{
"top": [{ "id": 12, "credit_quality": 98, "green_impact": 95 }],
"bottom": [{ "id": 4, "credit_quality": 38, "green_impact": 42 }]
}Generates score distribution histogram data for charts.
| Param | In | Type | Rules | Default |
|---|---|---|---|---|
field |
query | string | credit_quality or green_impact |
credit_quality |
bucket |
query | int | Number of histogram buckets (1..50) |
10 |
Retrieves historical score data points for a specific project. Supports ?from= and ?to= query filters.
Exports the entire portfolio score database in CSV format (Content-Type: text/csv).
Compares up to 20 projects side by side across environmental and financial metrics.
| Param | In | Type | Rules |
|---|---|---|---|
ids |
query | string | Comma-separated list of positive integer project IDs (max 20) |
Response 200
{
"projects": [
{
"id": 1,
"credit_quality": 74,
"green_impact": 69,
"power_output_kw": 742.15,
"efficiency_pct": 74.21,
"forest_density_pct": 68.44,
"ndvi_score": 0.684
},
{
"id": 2,
"credit_quality": 88,
"green_impact": 82,
"power_output_kw": 890.0,
"efficiency_pct": 89.0,
"forest_density_pct": 74.1,
"ndvi_score": 0.741
}
]
}Lists all supported comparison metric keys (credit_quality, green_impact, power_output_kw, efficiency_pct, forest_density_pct, ndvi_score, combined_score).
Ranks a set of projects according to chosen criteria.
| Param | In | Type | Rules | Default |
|---|---|---|---|---|
ids |
query | string | Comma-separated project IDs | Required |
criteria |
query | string | Any valid comparison metric | combined_score |
Exports project comparison or ranked analysis to CSV format.
Lists standard and custom benchmark definitions with evaluation thresholds (poor, fair, good, excellent).
Response 200
{
"benchmarks": [
{
"id": "credit_quality",
"name": "Industry Credit Quality",
"description": "Standardized financial creditworthiness",
"metric": "credit_quality",
"thresholds": { "poor": 40, "fair": 60, "good": 80, "excellent": 90 },
"source": "Standard & Poor's ESG"
}
]
}Registers a new custom benchmark definition.
Request Body
{
"id": "custom_efficiency",
"name": "High-Efficiency Solar Benchmark",
"description": "Benchmark for bifacial tier-1 solar assets",
"metric": "efficiency_pct",
"thresholds": { "poor": 50, "fair": 70, "good": 85, "excellent": 95 },
"source": "NREL 2026 Guidelines"
}Evaluates project id against all registered industry benchmarks.
Calculates project percentile ranking against all projects in the registry.
| Param | In | Type | Rules | Default |
|---|---|---|---|---|
metric |
query | string | Metric key to rank | combined_score |
Returns active alerts for project metrics falling below benchmark thresholds.
Evaluates project performance trajectory relative to a benchmark over time.
Detailed cost-benefit analysis breakdown over the project lifetime.
| Param | In | Type | Description |
|---|---|---|---|
installation_cost |
query | number | Optional override for total capital expenditure |
annual_maintenance_cost |
query | number | Optional override for annual O&M |
electricity_price_per_kwh |
query | number | Price per kWh generated |
discount_rate |
query | number | Annual discount rate (e.g. 0.06 for 6%) |
project_lifetime_years |
query | number | Project lifespan (default: 25) |
Response 200
{
"project_id": 1,
"installation_cost": 250000,
"total_revenue": 680000,
"total_maintenance_cost": 75000,
"net_benefit": 355000,
"roi_pct": 142.0,
"annual_cash_flows": [
{ "year": 1, "revenue": 27200, "maintenance_cost": 3000, "net_cash_flow": 24200 }
]
}Computes simple and discounted payback periods in years.
Response 200
{
"project_id": 1,
"payback_years": 7.4,
"discounted_payback_years": 9.2,
"reaches_payback": true
}Calculates Net Present Value (NPV) based on discounted future cash flows.
Response 200
{
"project_id": 1,
"npv": 118450.25,
"discount_rate": 0.06,
"discounted_cash_flows": [{ "year": 1, "discounted_cash_flow": 22830.19 }]
}Performs sensitivity analysis evaluating how NPV and ROI respond to changes in discount rates, electricity prices, and degradation rates.
Compares ROI, NPV, and payback across multiple projects (?ids=1,2,3).
Generates time-series forecasts for future power generation or efficiency.
| Param | In | Type | Rules | Default |
|---|---|---|---|---|
horizon |
query | int | Hours ahead to forecast (1..8760) |
24 |
field |
query | string | power_output_kw or efficiency_pct |
power_output_kw |
method |
query | string | exponential_smoothing, linear_regression, moving_average, arima_simplified |
exponential_smoothing |
history_hours |
query | int | Historical sample window (4..8760) |
168 |
format |
query | string | json or csv |
json |
Response 200
{
"project_id": 1,
"field": "power_output_kw",
"method": "exponential_smoothing",
"predictions": [
{
"hour_offset": 1,
"predicted_value": 745.2,
"confidence_lower": 710.0,
"confidence_upper": 780.4
}
]
}Forecasts power generation factoring in diurnal solar irradiance and simulated cloud cover patterns.
Decomposes performance into seasonal, diurnal, and periodic cycles.
Backtests forecasting models against historical project telemetry and reports accuracy metrics (mae, rmse, mape).
Returns list of available forecasting model identifiers.
Analyzes efficiency degradation trend and rate of loss over time.
Predictive modeling identifying hardware failure risks, estimated hours/days to critical threshold, severity, and confidence score. Supports CSV export.
Response 200
{
"project_id": 1,
"current_efficiency": 74.21,
"critical_threshold": 50.0,
"estimated_hours_to_threshold": 1420,
"estimated_days_to_threshold": 59.1,
"severity": "medium",
"confidence": 0.88,
"panel_type": "monocrystalline"
}Actionable maintenance recommendations (e.g. panel cleaning, inverter diagnostics, string inspection) based on telemetry patterns.
Optimal scheduled maintenance calendar with recommended target dates and estimated costs.
Consolidated health check including efficiency trends, failure predictions, recommendations, and maintenance schedules.
Creates a maintenance task / work order.
Request Body
{
"project_id": 1,
"title": "Quarterly Inverter Inspection",
"description": "Thermal scan of inverter modules and terminal connections",
"action_type": "inspection",
"priority": "high",
"scheduled_date": "2026-09-15",
"assigned_to": "technician_1",
"estimated_cost": 450
}Lists work orders with query filters: project_id, status (pending, in_progress, completed, cancelled), priority, from_date, to_date, format (json or csv).
Auto-generates recommended work orders for project id from predictive schedule analysis.
Retrieves or partially modifies a specific maintenance task.
Marks a task completed and records actual costs and before/after efficiency delta.
Request Body
{
"actual_cost": 420.0,
"notes": "Replaced faulty connector on string 4",
"efficiency_before": 72.1,
"efficiency_after": 78.4
}Deletes a maintenance task.
Calendar views (daily, weekly, monthly) or custom date ranges (?from=YYYY-MM-DD&to=YYYY-MM-DD) of scheduled maintenance.
Retrieves or manually logs a past maintenance intervention.
Maintenance KPIs: completed task count, total maintenance spend, average resolution time, and efficiency gains.
Runs statistical z-score and moving average anomaly detection on project telemetry.
| Param | In | Type | Rules |
|---|---|---|---|
sensitivity |
query | number | Optional z-score threshold override |
window |
query | number | Moving baseline window size |
Response 200
{
"project_id": 1,
"is_anomaly": true,
"anomaly_score": 3.42,
"flagged_metrics": ["efficiency_pct"],
"details": {
"efficiency_pct": { "value": 41.2, "expected": 74.0, "z_score": -3.42 }
}
}Retrieves or updates anomaly detection configuration (sensitivityZScore, trendWindowSize, trendDeviationPct, minBaseline).
Clears baseline history cache for a specific project or all projects.
Portfolio-wide aggregated dashboard summary and recent audit activities for investor portals.
Response 200
{
"portfolio_summary": {
"total_projects": 2,
"total_power_output_kw": 1150,
"avg_credit_quality": 85,
"avg_green_impact": 75,
"total_portfolio_value": 950000,
"total_carbon_offsets_tonnes": 4312.5
},
"recent_activities": [
{
"id": 1,
"project_id": 1,
"credit_quality": 85,
"green_impact": 75,
"tx_hash": "tx123",
"triggered_by": "api",
"timestamp": 1718150400000
}
]
}Provides actual vs expected performance ratios and operational status (Optimal, Underperforming, Critical) for all portfolio assets.
Aggregates financial KPIs (total installation cost, total NPV, average payback period, average ROI).
ESG compliance scoring, verified carbon credits issued, and immutable audit logs.
Generates customized reports tailored by project ID list and report sections.
Request Body
{
"project_ids": [1, 2],
"sections": ["performance", "scores", "financials", "compliance"]
}Registers an external HTTP endpoint to receive real-time event notifications (e.g. score updates, threshold alerts).
Request Body
{
"url": "https://api.external.com/webhooks/heliobond",
"secret": "super_secret_signing_key_at_least_16_chars",
"max_retries": 3,
"retry_delay_ms": 2000
}Response 201
{
"id": "wh_9f2a48b1",
"url": "https://api.external.com/webhooks/heliobond",
"max_retries": 3,
"retry_delay_ms": 2000,
"created_at": 1718150400000
}Lists registered webhooks or retrieves details for a single webhook (secrets are omitted).
Unregisters and deletes a webhook subscription.
Subscribes an email address to recurring summary digests.
Request Body
{
"email": "investor@example.com",
"frequency": "weekly"
}Public, no admin API key, IP allowlist, or request signature required
(#763). Recipients can click the footer link from any mail client. Rate
limited by the shared publicLimiter.
Query: token=<unsubscribe_token> (URL-encoded).
Responses:
200 {"unsubscribed": true}on success.400when the token is missing.404when the token is unknown or already used.
Public. RFC 8058 List-Unsubscribe=One-Click target. Mail clients
POST here automatically when the recipient uses the built-in Unsubscribe
button surfaced from the List-Unsubscribe header.
Body: { "token": "<unsubscribe_token>" } (or ?token= in the query
string; either is accepted).
Alert and digest emails carry:
- Absolute footer:
Unsubscribe: ${PUBLIC_API_URL}/v1/email/unsubscribe?token=.... - Headers:
List-Unsubscribe: <${PUBLIC_API_URL}/v1/email/unsubscribe?token=...>andList-Unsubscribe-Post: List-Unsubscribe=One-Click.
Tokens are HMAC-SHA256 derived from the subscriber's email plus
EMAIL_UNSUBSCRIBE_SECRET. They are deterministic per subscriber and are
not stored as raw UUIDs; rotating the secret invalidates every
outstanding link.
Lists current subscribers with optional ?frequency=daily|weekly filter.
Retrieves or updates alerting thresholds for automatic email notifications.
Manages markdown/HTML templates for digest emails (name, subject, body).
Triggers an on-demand digest dispatch to subscribers.
Lists configured blockchains (Stellar, Polygon, Ethereum, etc.) and active RPC
status. Public read: no authentication required. The chain registry does
not expose secret material and is also surfaced via /v1/status/oracle.
Updates configuration for a specific chain network (enabled, rpcUrl,
contractAddress, name). Admin only: requires IP whitelist, admin
bearer token, and HMAC request signing (ipWhitelist + requireAdminBearer +
requestSigning), matching sibling admin routers like /v1/satellite-sources
and /v1/admin/api-keys.
Payload is validated before any config is applied: rpcUrl must be an
http(s) URL that does not resolve to private, loopback, link-local, or
metadata address ranges (defense-in-depth SSRF check via
lib/ssrf.validatePublicUrl). contractAddress must be a valid Stellar
contract strkey. enabled must be a boolean. name must be a non-empty
string of at most 200 characters.
Broadcasts computed project impact scores to Stellar. Admin only: same
auth stack as PATCH /v1/chains/:id — this endpoint signs and submits a
Soroban transaction with ADMIN_SECRET_KEY, so unauthenticated access would
let any caller spend the oracle signer's XLM.
EVM chains (Ethereum, Polygon) do not have a submission implementation yet:
requesting one returns 501 Not Implemented rather than a synthetic
transaction hash.
On success the response contains per-chain results with a status of
submitted, failed, or not_implemented. txHash is only populated for
real submitted results.
Request Body
{
"chains": ["stellar"]
}Lists configured satellite data providers (e.g. Sentinel-2, Landsat-9, MODIS, Planet) with priority and health status.
Health check status and consecutive failure counts per data source provider.
Enables/disables a source or alters its priority in the failover cascade ({ "enabled": true, "priority": 1 }).
Registers a custom external satellite data adapter endpoint (name, priority, fetchUrl).
Fetches satellite NDVI data from the highest-priority available source with automatic fallback failover.
Lists all scoring formula definitions and identifies the currently active formula ID.
Creates a custom formula assigning weights to telemetry metrics.
Request Body
{
"id": "solar_heavy_v2",
"name": "Solar Focused Impact Formula",
"description": "Increases weighting of solar efficiency over vegetation",
"weights": {
"efficiency_pct": 0.6,
"power_output_kw": 0.2,
"ndvi_score": 0.2
}
}Retrieves or deletes a custom scoring formula definition.
Activates a custom formula platform-wide for subsequent score computations.
Validates weight distribution and sum normalization without saving.
Simulates and previews score changes for a project using the custom formula vs the default formula (A/B testing).
Computes and submits update_impact_score transactions to the Soroban smart contract oracle.
Headers
| Header | Required | Value |
|---|---|---|
Authorization |
Yes (when ADMIN_API_KEY set) |
Bearer <ADMIN_API_KEY> |
Request Body (optional)
{
"project_ids": [1, 2, 3]
}Response 200
{
"updated": 2,
"results": [
{ "project_id": 1, "tx_hash": "abc123...", "credit_quality": 74, "green_impact": 69 }
],
"errors": [],
"skipped": []
}Immutable audit log of all on-chain score updates with query filters (?project_id=, ?from=, ?to=, ?format=json|csv).
Starts an asynchronous background batch score update job with concurrency controls.
Request Body
{
"project_ids": [1, 2, 3, 4, 5],
"concurrency": 3
}Response 202 Accepted
{
"batch_id": "job_9a8f21c4",
"status": "running",
"total": 5,
"concurrency": 3
}Polls progress, completion status, results, and errors for a batch job.
Generates a new consumer API key with rate limits and rotation intervals.
Request Body
{
"consumer_name": "Acme Partner Service",
"rate_limit": 100,
"rotation_interval_days": 90
}GET /v1/admin/api-keys, POST /v1/admin/api-keys/:id/rotate, DELETE /v1/admin/api-keys/:id, GET /v1/admin/api-keys/:id/usage
Full lifecycle management for consumer API credentials.
Role-Based Access Control (RBAC) user assignment (admin, operator, viewer).
Checks encryption secret rotation status and schedule.
Database migration status and execution controls.
Inspects or dynamically updates the runtime logger level (debug, info, warn, error).
Reports response compression efficiency and byte savings.
GET /v1/admin/flags, GET /v1/admin/flags/:name, POST /v1/admin/flags/load, POST /v1/admin/flags/merge, GET /v1/admin/flags/analytics
Feature flag management and evaluation context analytics.
Two WebSocket feeds are exposed, targeting different consumers:
| Path | Auth | Frames | Intended consumer |
|---|---|---|---|
/ws |
Bearer | Binary (score) | Internal / server-to-server admin subscribers. |
/ws/events |
None | JSON (vault) | Browser tabs (the VaultEventStream in the app UI). |
Both share the same HTTP server; the upgrade handler routes by URL pathname.
Browsers cannot set headers on a WebSocket, so this feed accepts a bare
new WebSocket(url) with no headers and no subscribe frame. The
frontend's VaultEventStream is the canonical consumer.
-
Endpoint:
ws://localhost:3001/ws/events(dev) orwss://your-domain.com/ws/events(production). -
Auth: none, this endpoint serves public chain data. Follow-up work (#655) may layer an optional
Sec-WebSocket-Protocoltoken. -
Optional filter:
?contract=<contractId>, only events whosecontractIdfield matches are delivered to that client. Omit to receive every broadcast. -
Frame shape: the fixture at
src/__tests__/fixtures/vault-event.jsonis the shared contract with the frontend:{ "type": "ScoreChanged", "contractId": "CDLZ...CYSC", "ledger": 1234567, "topic": ["AAAA...", "AAAA..."], "value": { "project_id": 27, "credit_quality": 82, "green_impact": 74 }, "id": "1234567-0" } -
Connection limits:
WS_EVENTS_MAX_PER_IP(default 8) concurrent sockets per source IP. The 9th connection is closed with code1013 too many connections from this IP. -
Heartbeat: the server sends a WebSocket
pingevery 30s; sockets that fail topongwithin 60s are terminated. BrowserWebSocketanswers pings automatically. -
Producers: any backend module can push an event via
broadcastVaultEvent(evt)fromsrc/lib/vaultEvents.ts. The full end-to-end wiring from a Soroban indexer is tracked in the paired issue referenced from #767.
Unchanged from before #767. Provides real-time binary score updates with low latency and minimal bandwidth usage.
- Endpoint:
ws://localhost:3001/ws(dev) orwss://your-domain.com/ws(production) - Protocol: Binary frames for score updates, JSON for control messages
Authentication is required in production and optional in dev/test environments. Send the bearer token via the Authorization header during the WebSocket upgrade:
const ws = new WebSocket('ws://localhost:3001/ws', {
headers: {
'Authorization': 'Bearer YOUR_WS_AUTH_TOKEN'
}
});Important: Query string authentication (?token=...) is not supported for security reasons (tokens in URLs leak into logs). WS_AUTH_TOKEN does not fall back to ADMIN_API_KEY.
Close codes:
1008 Unauthorized: Missing or invalid bearer token (production only)
Clients send JSON control messages to manage subscriptions:
{
"action": "subscribe",
"project_ids": [1, 42, 100]
}{
"action": "subscribe",
"all": true
}Or alternatively:
{
"action": "subscribe",
"project_ids": "all"
}{
"action": "unsubscribe",
"project_ids": [1, 42]
}{
"action": "unsubscribe",
"all": true
}After each subscription change, the server confirms with:
{
"type": "subscribed",
"all": false,
"project_ids": [1, 42, 100]
}{
"type": "error",
"message": "invalid JSON control frame"
}Or:
{
"type": "error",
"message": "unknown action: invalid_action"
}Score updates are sent as compact 15-byte binary frames for efficiency:
| Offset | Length | Type | Field | Description |
|---|---|---|---|---|
| 0 | 1 | uint8 | message_type | Always 0x01 (score update) |
| 1 | 4 | uint32 | project_id | Big-endian project identifier |
| 5 | 1 | uint8 | credit_quality | 0–100 (clamped) |
| 6 | 1 | uint8 | green_impact | 0–100 (clamped) |
| 7 | 8 | float64 | timestamp | Big-endian Unix timestamp (ms) |
ws.on('message', (data) => {
if (data instanceof Buffer && data.length === 15) {
const messageType = data.readUInt8(0);
if (messageType === 0x01) {
const projectId = data.readUInt32BE(1);
const creditQuality = data.readUInt8(5);
const greenImpact = data.readUInt8(6);
const timestamp = data.readDoubleBE(7);
console.log({
project_id: projectId,
credit_quality: creditQuality,
green_impact: greenImpact,
timestamp: new Date(timestamp).toISOString()
});
}
} else {
// JSON control frame (acknowledgement or error)
const message = JSON.parse(data.toString());
console.log('Control message:', message);
}
});import struct
import json
from datetime import datetime
def decode_score_update(data: bytes):
if len(data) == 15:
msg_type, project_id, cq, gi, timestamp = struct.unpack('>BIBBD', data)
if msg_type == 0x01:
return {
'project_id': project_id,
'credit_quality': cq,
'green_impact': gi,
'timestamp': datetime.fromtimestamp(timestamp / 1000)
}
else:
return json.loads(data.decode('utf-8'))WebSocket connections remain open indefinitely. Clients should implement reconnection logic for network interruptions. The server does not send ping/pong heartbeats; rely on TCP keep-alive or application-level health checks.
const WebSocket = require('ws');
const ws = new WebSocket('ws://localhost:3001/ws', {
headers: {
'Authorization': 'Bearer YOUR_WS_AUTH_TOKEN'
}
});
ws.on('open', () => {
console.log('Connected');
// Subscribe to specific projects
ws.send(JSON.stringify({
action: 'subscribe',
project_ids: [1, 2, 3]
}));
});
ws.on('message', (data) => {
if (data instanceof Buffer && data.length === 15) {
const messageType = data.readUInt8(0);
if (messageType === 0x01) {
const projectId = data.readUInt32BE(1);
const creditQuality = data.readUInt8(5);
const greenImpact = data.readUInt8(6);
const timestamp = data.readDoubleBE(7);
console.log('Score update:', {
project_id: projectId,
credit_quality: creditQuality,
green_impact: greenImpact,
timestamp: new Date(timestamp).toISOString()
});
}
} else {
const message = JSON.parse(data.toString());
console.log('Control message:', message);
}
});
ws.on('error', (error) => {
console.error('WebSocket error:', error);
});
ws.on('close', (code, reason) => {
console.log(`Connection closed: ${code} ${reason}`);
// Implement reconnection logic here
});- HTTP Endpoint:
/graphql(POST requests) - GraphiQL Playground:
/graphql-playground(GET request in browser)
GraphQL documents are validated before execution. A request that exceeds a
limit is rejected with a GRAPHQL_* error code and no resolvers (DataLoader,
database, or RPC work) run.
| Limit | Default | Environment variable |
|---|---|---|
| Maximum field-selection depth per operation | 12 | GRAPHQL_MAX_DEPTH |
| Maximum field selections per operation (complexity) | 1000 | GRAPHQL_MAX_COMPLEXITY |
| Maximum root fields/aliases per operation | 20 | GRAPHQL_MAX_ROOT_FIELDS |
Fragments and inline fragments are expanded when calculating these limits, so wrapping selections in fragments does not bypass them. Repeated aliases of the same field each count towards complexity and the root-field cap.
Introspection policy: __schema and __type introspection is disabled
when NODE_ENV=production and enabled in other environments. Set
GRAPHQL_ALLOW_INTROSPECTION=true to allow introspection in production (for
example, for a trusted internal schema registry) or false to disable it
everywhere. __typename is always allowed.
query GetProjectsWithSolar {
projects(limit: 5) {
id
credit_quality
green_impact
solar {
power_output_kw
efficiency_pct
max_power_kw
}
financials {
npv
roi_pct
payback_period_years
}
}
}mutation UpdateProjectScore {
updateProjectScores(id: "1", creditQuality: 90, greenImpact: 85) {
id
credit_quality
green_impact
}
}High-performance gRPC service listening on port 50051. Authenticates callers via gRPC metadata headers (authorization or x-api-key).
syntax = "proto3";
package heliobond;
service HeliobondService {
rpc GetProjectScore(ProjectRequest) returns (ProjectResponse);
rpc StreamProjectScores(StreamRequest) returns (stream ProjectResponse);
rpc ChatProjectScores(stream ProjectRequest) returns (stream ProjectResponse);
}
message ProjectRequest {
int32 project_id = 1;
}
message StreamRequest {
repeated int32 project_ids = 1;
}
message ProjectResponse {
int32 project_id = 1;
double credit_quality = 2;
double green_impact = 3;
double power_output_kw = 4;
double efficiency_pct = 5;
int64 timestamp = 6;
}