Skip to content

Latest commit

 

History

History
1240 lines (965 loc) · 27.5 KB

File metadata and controls

1240 lines (965 loc) · 27.5 KB

Stellar Portfolio Rebalancer — API Reference

Complete reference for the Stellar Portfolio Rebalancer HTTP API. All endpoints return JSON and are relative to the base URL.

Base URL

Environment Base URL
Development http://localhost:3001
Production Your deployed backend URL

All paths below are relative to the base URL.

API Versioning

Namespace Purpose
/api/v1/* Current stable version. New clients should use this namespace. Responses do not include deprecation headers.
/api/* Legacy compatibility. May include Deprecation, Sunset, and Link headers per RFC 8594. Migrate to /api/v1/*.
/api/auth/* Authentication endpoints (not versioned). See Authentication.
  • Frontend uses /api/v1 by default via VITE_API_VERSION in frontend/src/config/api.ts.
  • Set VITE_USE_LEGACY_API=true only for emergency rollback.

Version Lifecycle

  • Stable: /api/v1/* is the current stable API.
  • Deprecation: Legacy /api/* routes may be deprecated. When deprecated, responses include Sunset header with the retirement date.
  • Migration: New features are added to /api/v1/* first. Backward-compatible fixes may appear in both namespaces until legacy is retired.

Authentication

JWT authentication is optional and disabled by default. Enable it by setting JWT_SECRET in the backend environment.

Wallet-based Authentication Flow

  1. Request challenge:

    POST /api/auth/challenge
    {
      "address": "GALPHABET..."
    }
  2. Sign challenge with your Stellar wallet private key (Ed25519).

  3. Login:

    POST /api/auth/login
    {
      "address": "GALPHABET...",
      "signature": "base64-encoded-signature"
    }
  4. Use access token in subsequent requests:

    Authorization: Bearer <access_token>
  5. Refresh token when access token expires:

    POST /api/auth/refresh
    { "refreshToken": "<refresh_token>" }
  6. Logout:

    POST /api/auth/logout
    Authorization: Bearer <access_token>
    { "refreshToken": "<refresh_token>" }

Protected Endpoints

Most endpoints are public. JWT is required for:

  • /api/auth/* endpoints
  • Managing other users' data
  • Admin endpoints (/api/admin/*, /api/debug/*)
  • Some portfolio operations when auth is enabled and ALLOW_PUBLIC_USER_PORTFOLIOS_IN_DEMO is false

Demo Mode

When DEMO_MODE=true, the API operates in a read-only/simulated mode suitable for testing without real Stellar transactions.

Error Responses

All errors follow this structure:

{
  "success": false,
  "data": null,
  "error": {
    "code": "ERROR_CODE",
    "message": "Human-readable description",
    "details": {}
  },
  "timestamp": "2025-01-01T00:00:00.000Z"
}

Error Codes

Code HTTP Status Description Remediation
VALIDATION_ERROR 400 Invalid request body or parameters Check request schema
UNAUTHORIZED 401 Missing or invalid JWT Obtain valid token
FORBIDDEN 403 Insufficient permissions Verify user role/consent
NOT_FOUND 404 Resource doesn't exist Verify ID/path params
CONFLICT 409 Idempotency conflict or state conflict Retry with same key or resolve state
RATE_LIMITED 429 Rate limit exceeded Implement exponential backoff
SERVICE_UNAVAILABLE 503 Downstream service unavailable Retry later
INTERNAL_ERROR 500 Unexpected error Contact support

Rate Limiting

Tier Limit Window
Public reads 100 req 1 minute
Authenticated 200 req 1 minute
Admin writes 50 req 1 minute

Rate limited responses include Retry-After header (seconds).

Idempotency

Write endpoints support Idempotency-Key header (1–255 chars, e.g., UUID). The server caches the first successful response for 24 hours.

  • Same key + same body: Returns cached response with Idempotency-Replayed: true
  • Same key + different body: Returns 409 CONFLICT
  • Empty/invalid key: Returns 400 VALIDATION_ERROR

Supported endpoints: POST /api/portfolio, POST /api/portfolio/:id/rebalance, POST /api/rebalance/history, POST /api/notifications/subscribe, POST /api/consent, POST /api/admin/assets, PATCH /api/admin/assets/:symbol

Common Response Envelope

Success responses:

{
  "success": true,
  "data": { /* response payload */ },
  "error": null,
  "timestamp": "2025-01-01T00:00:00.000Z",
  "meta": { /* pagination, counts, etc. */ }
}

Testnet Examples

All cURL examples below use the v1 API. Replace http://localhost:3001 with your testnet backend URL. For demo mode, use any valid Stellar testnet address (e.g., from Stellar Laboratory).


Health & System

Health Check

GET /api/v1/health

Response:

{
  "status": "healthy",
  "timestamp": "2025-01-01T00:00:00.000Z"
}

System Status

GET /api/v1/system/status

Response:

{
  "system": {
    "status": "operational",
    "uptime": 12345,
    "timestamp": "2025-01-01T00:00:00.000Z",
    "version": "1.0.0"
  },
  "portfolios": { "total": 42, "active": 42 },
  "rebalanceHistory": { "total": 128 },
  "riskManagement": { "circuitBreakers": {}, "enabled": true },
  "autoRebalancer": { "status": { "isRunning": true } },
  "services": { "priceFeeds": true, "riskManagement": true },
  "featureFlags": { "demoMode": false }
}

Strategies

GET /api/v1/strategies

Response:

{
  "strategies": [
    { "id": "threshold", "name": "Threshold", "description": "..." },
    { "id": "periodic", "name": "Periodic", "description": "..." }
  ]
}

Portfolio

Create Portfolio

POST /api/v1/portfolio
Content-Type: application/json
Idempotency-Key: <uuid>

{
  "userAddress": "GALPHABET...",
  "allocations": { "XLM": 40, "BTC": 30, "USDC": 30 },
  "threshold": 5,
  "slippageTolerance": 1,
  "strategy": "threshold"
}

Response (201):

{
  "success": true,
  "data": {
    "portfolioId": "portfolio-abc123",
    "status": "created",
    "mode": "onchain"
  }
}

Validation:

  • allocations must sum to 100%
  • threshold: 1–50%
  • slippageTolerance: 0.1–5% (optional, default: 1)
  • strategy: threshold | periodic | volatility | custom (optional, default: threshold)

Bulk Import Portfolio

Create a new portfolio by uploading allocations from a CSV or JSON file. This is the primary endpoint consumed by the frontend BulkPortfolioImport.tsx component.

POST /api/v1/portfolio/import
Content-Type: application/json | text/csv

Accepted Formats

JSON — wrapped object:

{
  "allocations": [
    { "asset": "XLM", "allocation_pct": 40 },
    { "asset": "USDC", "allocation_pct": 35 },
    { "asset": "BTC", "allocation_pct": 25 }
  ],
  "userAddress": "GALPHABET...",
  "name": "My Portfolio",
  "description": "Optional description"
}

JSON — bare array:

[
  { "asset": "XLM", "allocation_pct": 40 },
  { "asset": "USDC", "allocation_pct": 35 },
  { "asset": "BTC", "allocation_pct": 25 }
]

When using a bare array, userAddress must be included as a top-level field of the request or authenticated via JWT.

CSV — raw text with required headers:

asset,allocation_pct
XLM,40
USDC,35
BTC,25

Send with Content-Type: text/csv. Headers must include asset and allocation_pct.

Request Schema

Field Type Required Notes
allocations AllocationInputRow[] Yes Array of { asset, allocation_pct }. May also be the top-level body for JSON.
userAddress string Yes Stellar public key. Required in body or via JWT auth.
name string No Portfolio display name.
description string No Portfolio description.

Each AllocationInputRow:

Field Type Constraints
asset string Required, normalized to uppercase. Must exist in the asset registry, be enabled, and not quarantined.
allocation_pct number Required, finite, 0–100. Duplicate assets are merged by summing percentages.

Validation Rules

  • Allocations must sum to 100% (tolerance: 0.01%).
  • Maximum 10 distinct assets.
  • Maximum 5 000 rows.
  • allocation_pct must be a finite number between 0 and 100 inclusive.
  • Asset codes are validated against the internal asset registry; unknown, disabled, or quarantined assets are rejected.
  • Duplicate asset rows are merged (percentages summed) before validation.

Sample cURL — JSON

curl -X POST http://localhost:3001/api/v1/portfolio/import \
  -H "Content-Type: application/json" \
  -d '{
    "allocations": [
      { "asset": "XLM", "allocation_pct": 60 },
      { "asset": "USDC", "allocation_pct": 40 }
    ],
    "userAddress": "GALPHABET...",
    "name": "Imported Portfolio"
  }'

Sample cURL — CSV

curl -X POST http://localhost:3001/api/v1/portfolio/import \
  -H "Content-Type: text/csv" \
  --data-binary "asset,allocation_pct
XLM,60
USDC,40"

Success Response (201)

{
  "success": true,
  "data": {
    "portfolioId": "portfolio-abc123",
    "status": "created"
  },
  "error": null,
  "timestamp": "2025-01-01T00:00:00.000Z"
}

Validation Error Response (400)

Returned when the payload fails validation. The errors array contains per-row details; meta summarises totals.

{
  "error": "VALIDATION_ERROR",
  "message": "Bulk import validation failed",
  "code": "VALIDATION_ERROR",
  "errors": [
    { "row": 2, "field": "asset", "message": "Invalid or unknown asset code: FAKE" },
    { "row": 3, "field": "allocation_pct", "message": "allocation_pct must be a number" },
    { "row": 0, "field": "allocation_pct", "message": "Allocations must sum to 100% (received 85%)" }
  ],
  "meta": {
    "totalRows": 3,
    "validRows": 1
  }
}

Each error object:

Field Type Description
row number 1-based row index (header = 1 for CSV). 0 indicates a payload-level error (e.g., sum check).
field string Field that failed (asset, allocation_pct, header, csv_or_json, rows, json).
message string Human-readable description of the failure.

meta fields:

Field Type Description
totalRows number Total data rows received.
validRows number Rows that passed all validation checks.

Other Error Responses

HTTP Status Code Condition
400 VALIDATION_ERROR Missing userAddress in body or JWT.
500 INTERNAL_ERROR Unexpected server error during portfolio creation.

Get Portfolio

GET /api/v1/portfolio/{portfolioId}

Response:

{
  "portfolio": {
    "id": "portfolio-abc123",
    "userAddress": "GALPHABET...",
    "totalValue": 10000.00,
    "allocations": [
      { "asset": "XLM", "target": 40, "current": 38.5, "amount": 3500, "balance": 9752, "price": 0.3589 }
    ],
    "needsRebalance": false,
    "lastRebalance": "2025-01-01T00:00:00.000Z",
    "threshold": 5,
    "slippageTolerancePercent": 1,
    "dayChange": 1.2
  },
  "riskHeatmap": { /* risk metrics per asset */ }
}

List User Portfolios

GET /api/v1/user/{address}/portfolios

Response:

{
  "portfolios": [ /* array of portfolio objects */ ]
}

Multi-Portfolio Dashboard Summary

Summarises every portfolio for one address in a single request, so a dashboard listing N portfolios costs one call rather than N. Prices are resolved once from the oracle cache and shared across the whole response.

GET /api/v1/portfolios/summary?userAddress={address}

Response:

{
  "success": true,
  "data": {
    "portfolios": [
      {
        "id": "portfolio-abc123",
        "name": "Core holdings",
        "total_value_usd": 10000,
        "drift_status": "warning",
        "last_rebalanced": "2026-01-02T00:00:00.000Z"
      }
    ]
  },
  "error": null,
  "timestamp": "2026-01-02T12:00:00.000Z"
}

data.portfolios is an empty array when the address has no portfolios.

drift_status compares the largest allocation drift against that portfolio's own threshold, so each portfolio is judged against the tolerance its owner chose:

Status Condition
critical Largest drift is past the threshold. This is the same comparison the auto-rebalancer uses to decide a portfolio has drifted.
warning Largest drift is at or above half the threshold, but not past it.
ok Anything below that, including a portfolio holding nothing.

name is null for a portfolio that was never named, and an asset with no available price contributes zero to total_value_usd rather than being assumed.

Get Rebalance Plan

  • POST /api/portfolio — Create portfolio (userAddress, allocations, threshold, optional slippageTolerance). Allocations must sum to 100%; threshold 1–50%. Supports Idempotency-Key.
  • GET /api/portfolio/{id} — Get portfolio by ID.
  • GET /api/portfolios — List all portfolios (optional query parameter: userAddress).
  • POST /api/portfolio/{id}/clone — Clone an existing portfolio (optional body: { name }). Supports Idempotency-Key.
  • GET /api/user/{address}/portfolios — List portfolios for a Stellar address. When JWT auth is enabled, the token subject must match :address (otherwise 403). In demo mode, public-by-address listing is allowed only when ALLOW_PUBLIC_USER_PORTFOLIOS_IN_DEMO is enabled.
  • GET /api/portfolios/summary — Dashboard summary of every portfolio for one address in a single request (query: userAddress, required). Returns id, name, total_value_usd, drift_status (ok/warning/critical), and last_rebalanced per portfolio; empty array for an unknown address. Prices are read once from the oracle cache for the whole response. Same ownership rules as GET /api/user/{address}/portfolios.
  • GET /api/portfolio/{id}/rebalance-plan — Get full read-only rebalance plan (per-asset buy/sell amounts, estimated fees, estimated slippage, projected allocations, prices).
  • POST /api/portfolio/{id}/rebalance/dry-run — Dry-run rebalance; returns the same response schema as rebalance-plan without DB writes, contract calls, or trade execution.
  • POST /api/portfolio/{id}/rebalance — Execute rebalance (body optional: { options: { simulateOnly, ignoreSafetyChecks, slippageOverrides } }). Supports Idempotency-Key.
  • GET /api/portfolio/{id}/analytics — Analytics time series (query: days, default 30).
  • GET /api/portfolio/{id}/performance-summary — Performance summary.
  • GET /api/portfolio/tax-report — Realized gain/loss tax report computed with FIFO cost basis (query: year optional, defaults to current year; format json (default) or csv).

Response:

{
  "portfolioId": "portfolio-abc123",
  "totalValue": 10000.00,
  "maxSlippagePercent": 1,
  "estimatedSlippageBps": 100,
  "prices": { "XLM": { "price": 0.3589, "change": -0.5 } },
  "priceFeedMeta": { /* feed metadata */ }
}

Execute Rebalance

POST /api/v1/portfolio/{portfolioId}/rebalance
Content-Type: application/json
Idempotency-Key: <uuid>

{
  "options": {
    "simulateOnly": false,
    "ignoreSafetyChecks": false,
    "slippageOverrides": { "XLM": 0.5 }
  }
}

Response:

{
  "result": {
    "status": "completed",
    "txHash": "abc123...",
    "trades": 3,
    "gasUsed": "50000"
  }
}

Rebalance Estimate

GET /api/v1/portfolio/{portfolioId}/rebalance-estimate

Response:

{
  "estimatedGas": "55000",
  "estimatedCost": "0.05",
  "canExecute": true
}

Drafts

Create Draft

POST /api/v1/portfolio/draft
Content-Type: application/json
Idempotency-Key: <uuid>

{
  "userAddress": "GALPHABET...",
  "allocations": { "XLM": 50, "USDC": 50 },
  "threshold": 3,
  "label": "My conservative draft"
}

Response (201):

{
  "draftId": "draft-xyz789",
  "status": "draft_created"
}

Get Draft

GET /api/v1/portfolio/draft/{draftId}

Update Draft

PATCH /api/v1/portfolio/draft/{draftId}
Content-Type: application/json
Idempotency-Key: <uuid>

{
  "allocations": { "XLM": 60, "USDC": 40 },
  "threshold": 4
}

Publish Draft

POST /api/v1/portfolio/draft/{draftId}/publish
Idempotency-Key: <uuid>

Response (201):

{
  "portfolioId": "portfolio-abc123",
  "status": "published"
}

Delete Draft

DELETE /api/v1/portfolio/draft/{draftId}

List User Drafts

GET /api/v1/user/{address}/drafts

Portfolio Export (GDPR Data Portability)

Start Export Job

GET /api/v1/portfolio/{portfolioId}/export?format=json
# or format=csv, format=pdf

Response (202):

{
  "jobId": "job-123456",
  "status": "processing"
}

Get Export Status/Result

GET /api/v1/portfolio/{portfolioId}/export/status/{jobId}

Response (processing):

{
  "status": "processing",
  "state": "waiting"
}

Response (completed): Returns the file directly with appropriate Content-Type and Content-Disposition headers.


Analytics

Portfolio Analytics

Returns daily portfolio values and key performance metrics computed from stored price snapshots and rebalance history.

GET /api/v1/portfolio/{portfolioId}/analytics?days=30
GET /api/v1/portfolio/{portfolioId}/analytics?from=2025-01-01T00:00:00Z&to=2025-06-01T00:00:00Z

Query params:

  • days (optional): Number of days to look back. Default: 30. Ignored if from/to are provided.
  • from (optional): ISO 8601 start date. Must be before to. Future dates rejected.
  • to (optional): ISO 8601 end date. Future dates rejected.

Response:

{
  "portfolioId": "portfolio-abc123",
  "dailyValues": [
    { "timestamp": "2025-01-01T00:00:00.000Z", "totalValue": 10000, "allocations": { "XLM": 60, "USDC": 40 } }
  ],
  "metrics": {
    "totalReturnPercent": 5.2,
    "maxDrawdownPercent": 3.5,
    "sharpeRatio": 1.8
  },
  "dataPoints": 30
}

Returns empty dailyValues: [] and zeroed metrics for portfolios with no history.

Performance Summary

GET /api/v1/portfolio/{portfolioId}/performance-summary

Response:

{
  "portfolioId": "portfolio-abc123",
  "totalReturn": 5.2,
  "annualizedReturn": 12.5,
  "sharpeRatio": 1.8,
  "maxDrawdown": -3.5,
  "volatility": 8.2
}

Risk Diagnostics

GET /api/v1/portfolio/{portfolioId}/risk-diagnostics

Response:

{
  "riskHeatmap": { /* per-asset risk scores */ }
}

Rebalance History

List History

GET /api/v1/rebalance/history?portfolioId=portfolio-abc123&limit=50&source=onchain

Query params:

  • portfolioId (optional): Filter by portfolio
  • limit (optional): 1–500, default: 50
  • offset (optional): Pagination offset
  • source (optional): offchain | simulated | onchain
  • startTimestamp, endTimestamp (optional): ISO 8601
  • syncOnChain (optional): true to sync on-chain first

Response:

{
  "history": [
    {
      "id": "event-1",
      "portfolioId": "portfolio-abc123",
      "timestamp": "2025-01-01T00:00:00.000Z",
      "status": "completed",
      "trigger": "manual",
      "trades": 3,
      "gasUsed": "50000"
    }
  ],
  "pagination": { "limit": 50, "offset": 0, "count": 1 }
}

Record Rebalance Event

POST /api/v1/rebalance/history
Content-Type: application/json
Idempotency-Key: <uuid>

{
  "portfolioId": "portfolio-abc123",
  "trigger": "auto",
  "trades": 2,
  "gasUsed": "45000",
  "status": "completed",
  "isAutomatic": true
}

Sync On-Chain History

POST /api/v1/rebalance/history/sync-onchain

Rebalance Summary

GET /api/v1/rebalance/summary/{portfolioId}

Response:

{
  "portfolioId": "portfolio-abc123",
  "readiness": {
    "systemReady": true,
    "canExecute": true,
    "checks": { "database": "ready", "queue": "ready", "workers": "ready" }
  },
  "drift": { "needsRebalance": true, "maxDriftPercent": 6.5, "exceedsThreshold": true },
  "slippage": { "maxSlippagePercent": 1, "estimatedSlippageBps": 100 },
  "risk": { "allowed": true, "overallRiskLevel": "low", "alerts": [] },
  "dataFreshness": { "ageSeconds": 12, "isStale": false }
}

Auto-Rebalancer

Get Status

GET /api/v1/auto-rebalancer/status

Response:

{
  "status": { "isRunning": true, "initialized": true },
  "statistics": { "totalRebalances": 42, "successRate": 0.95 }
}

Start/Stop

POST /api/v1/auto-rebalancer/start
POST /api/v1/auto-rebalancer/stop

Force Check

POST /api/v1/auto-rebalancer/force-check

Dry-Run (Admin)

POST /api/v1/auto-rebalancer/dry-run/{portfolioId}

Auto-Rebalance History

GET /api/v1/auto-rebalancer/history?portfolioId=portfolio-abc123&limit=20

Risk

Risk Metrics

GET /api/v1/risk/metrics/{portfolioId}

Response:

{
  "portfolioId": "portfolio-abc123",
  "riskMetrics": {
    "volatility": 0.15,
    "concentrationRisk": 0.32,
    "liquidityRisk": 0.08,
    "var95": -0.025
  },
  "recommendations": [ "Reduce concentration in XLM" ],
  "circuitBreakers": { "volatility": { "isTriggered": false } }
}

Risk Check

GET /api/v1/risk/check/{portfolioId}

Response:

{
  "portfolioId": "portfolio-abc123",
  "allowed": true,
  "reason": null,
  "riskMetrics": { /* ... */ }
}

Prices & Market

Current Prices

GET /api/v1/prices

Response:

{
  "prices": {
    "XLM": { "price": 0.3589, "change": -0.5, "timestamp": 1706880000, "source": "coingecko" }
  },
  "feedMeta": { "degraded": false, "sources": ["coingecko"] }
}

Enhanced Prices

GET /api/v1/prices/enhanced

Response:

{
  "prices": {
    "XLM": { "price": 0.3589, "change": -0.5, "riskAlerts": [], "volatilityLevel": "low" }
  },
  "riskAlerts": [],
  "feedMeta": { /* ... */ }
}

Market Details

GET /api/v1/market/{asset}/details

Price Chart

GET /api/v1/market/{asset}/chart?days=7

Response:

{
  "asset": "XLM",
  "data": [ { "timestamp": 1706880000, "price": 0.35 } ],
  "timeframe": "7d",
  "dataPoints": 168
}

Assets

List Assets

GET /api/v1/assets?enabledOnly=true&page=1&limit=20&sortBy=symbol&order=asc

Query params:

  • enabledOnly (optional): true to show only enabled assets
  • code / search / q: Search by symbol/name
  • issuer: Filter by issuer address
  • sortBy: symbol | name | enabled
  • order: asc | desc
  • page, limit: Pagination (max 100)

Response:

{
  "assets": [
    {
      "symbol": "XLM",
      "name": "Stellar Lumens",
      "enabled": true,
      "contractAddress": null,
      "issuerAccount": null,
      "coingeckoId": "stellar"
    }
  ],
  "pagination": { "page": 1, "limit": 20, "total": 50 }
}

Get Asset

GET /api/v1/assets/{symbol}

Notifications

  • GET /api/notifications/alerts/thresholds — Get per-asset price alert threshold overrides and the global default for a user (query: userId, required).
  • PUT /api/notifications/alerts/thresholds — Merge per-asset price alert threshold overrides for a user (body: userId, thresholds).
  • DELETE /api/notifications/alerts/thresholds — Remove a single per-asset price alert threshold override (query: userId, asset, both required).

Subscribe

POST /api/v1/notifications/subscribe
Content-Type: application/json
Idempotency-Key: <uuid>

{
  "userId": "GALPHABET...",
  "emailEnabled": true,
  "emailAddress": "user@example.com",
  "webhookEnabled": false,
  "webhookUrl": null,
  "digestMode": false,
  "events": ["rebalance", "priceMovement"]
}

Get Preferences

GET /api/v1/notifications/preferences?userId=GALPHABET...

Unsubscribe

DELETE /api/v1/notifications/unsubscribe?userId=GALPHABET...&reason=no-longer-needed

Notification Logs

GET /api/v1/notifications/logs?userId=GALPHABET...

Get Price Alert Thresholds

GET /api/v1/notifications/alerts/thresholds?userId=GALPHABET...

Returns per-asset price alert threshold overrides and the user's global default:

{
  "success": true,
  "data": {
    "thresholds": { "XLM": 7, "BTC": 3 },
    "defaultThreshold": 5
  },
  "timestamp": "2025-01-01T00:00:00.000Z"
}

Set Price Alert Thresholds

PUT /api/v1/notifications/alerts/thresholds
Content-Type: application/json

{
  "userId": "GALPHABET...",
  "thresholds": { "XLM": 7, "BTC": 3 }
}

Merges the provided per-asset overrides into the user's existing overrides.

Delete a Price Alert Threshold

DELETE /api/v1/notifications/alerts/thresholds?userId=GALPHABET...&asset=XLM

Removes the per-asset override for the given asset so alert evaluation falls back to the user's global default.


Consent (GDPR)

Get Consent Status

GET /api/v1/consent/status?userId=GALPHABET...

Response:

{
  "accepted": true,
  "termsAcceptedAt": "2025-01-01T00:00:00.000Z",
  "privacyAcceptedAt": "2025-01-01T00:00:00.000Z",
  "active": true
}

Grant Consent

POST /api/v1/consent/grant
Content-Type: application/json
Idempotency-Key: <uuid>

{
  "userId": "GALPHABET...",
  "terms": true,
  "privacy": true,
  "cookies": true,
  "documentText": "v2025.01"
}

Revoke Consent

POST /api/v1/consent/revoke
Content-Type: application/json
Idempotency-Key: <uuid>

{
  "userId": "GALPHABET..."
}

Consent Audit Log

GET /api/v1/consent/audit?userId=GALPHABET...

Delete User Data (GDPR Erasure)

DELETE /api/v1/user/{address}/data
Authorization: Bearer <access_token>

Admin

Admin routes require Authorization: Bearer <admin_token> and ADMIN_PUBLIC_KEYS to include your address.

List All Assets (Including Disabled)

GET /api/v1/admin/assets
Authorization: Bearer <admin_token>

Add Asset

POST /api/v1/admin/assets
Authorization: Bearer <admin_token>
Content-Type: application/json

{
  "symbol": "NEW",
  "name": "New Asset",
  "contractAddress": "C...",
  "issuerAccount": "G...",
  "coingeckoId": "new-asset"
}

Update Asset

PATCH /api/v1/admin/assets/{symbol}
Authorization: Bearer <admin_token>
Content-Type: application/json

{
  "enabled": true,
  "quarantined": false
}

Remove Asset

DELETE /api/v1/admin/assets/{symbol}
Authorization: Bearer <admin_token>

Debug

Debug routes are disabled in production (NODE_ENV=production).

Test Notification

POST /api/v1/debug/notifications/test
Authorization: Bearer <admin_token>
Content-Type: application/json

{
  "userId": "GALPHABET...",
  "eventType": "rebalance"
}

Force Fresh Prices

GET /api/v1/debug/force-fresh-prices
Authorization: Bearer <admin_token>

Env Info

GET /api/v1/debug/env
Authorization: Bearer <admin_token>

OpenAPI & Tools

  • Swagger UI: http://localhost:3001/api-docs
  • OpenAPI JSON: http://localhost:3001/api-docs.json
  • Postman: Import from URL above or backend/openapi.json after running npm run openapi:export

Maintenance

  • Authoritative spec: backend/src/openapi/spec.ts
  • Generated artifacts: backend/openapi.json
  • Validate sync: cd backend && npm run api:validate
  • Export: cd backend && npm run openapi:export

CI fails if docs are out of sync.