# Stenion configuration. Copy to .env (gitignored) and adjust: # cp .env.example .env # # Real shell/CI env vars win over this file. The indexer validates everything on # startup and refuses to run, listing every problem at once. # --- Required --- # Soroban RPC. The default is the public keyless mainnet endpoint — shared and # rate-limited, so use your own for anything sustained. STENION_RPC_URL=https://mainnet.sorobanrpc.com # Horizon, for admin-account signer/activity data. STENION_HORIZON_URL=https://horizon.stellar.org # Neon *pooled* connection string, pasted exactly as the Neon dashboard gives it — # the db package rewrites sslmode=require to verify-full itself. Migrations use # this same variable. See db/src/env.ts for why the mode is pinned. DATABASE_URL=postgresql://user:password@ep-example-pooler.region.aws.neon.tech/stenion?sslmode=require # Shared secret for POST /api/cron/run-indexer, sent as `Authorization: Bearer `. # The route refuses to run (500) without it, so it is never open. Generate with: # node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" # Only needed where that route runs: the deployed dashboard, or locally to test it. CRON_SECRET=replace-with-a-long-random-string # Adapters own their contract addresses in code — nothing to configure here. # --- Deploying to Vercel (one project = the dashboard) --- # # Set in Production + Preview: DATABASE_URL, STENION_RPC_URL, STENION_HORIZON_URL, # CRON_SECRET (the same value goes on the cron-job.org job's Authorization header). # Worth adding: STENION_ALERT_WEBHOOK_URL, STENION_RATE_LIMIT_SALT. # STENION_INTERVAL_MS / STENION_RUN_ONCE are ignored by the cron route — it always # runs exactly one cycle, and the schedule lives in cron-job.org, not in this repo. # --- Optional: indexer loop (defaults shown) --- # Scoring-cycle interval, ms. #STENION_INTERVAL_MS=300000 # Run one cycle then exit. The `--once` CLI flag does the same and overrides this. #STENION_RUN_ONCE=false # --- Optional: retry and failure alerting --- # # Retries make failures rarer and louder, never hidden: a run that ultimately # fails is still recorded as `failed`. See ARCHITECTURE.md. # Attempts per protocol per cycle, including the first. 1 disables retry. #STENION_RETRY_ATTEMPTS=3 # Delay before the 2nd attempt, ms; doubles for each attempt after. #STENION_RETRY_BASE_DELAY_MS=1000 # Soft cap on a single attempt, ms. Live fetches measure 6–10.5s. "Soft" because it # abandons the in-flight work rather than cancelling it (see ROADMAP.md). #STENION_ATTEMPT_TIMEOUT_MS=15000 # Wall-clock budget for one cycle, ms, split between protocols. # LOAD-BEARING: the cron route runs under Vercel Hobby's maxDuration=60, which cannot # be raised, and a cycle killed mid-flight is worse than one that fails cleanly — it # can leave one protocol scored and another neither scored nor recorded as failed. # Must leave room for cold start, pool connect, upserts, streak queries and the alert # POST. Raise it only against observed cycle durations, never by arithmetic alone. #STENION_CYCLE_BUDGET_MS=42000 # Consecutive failed cycles before a protocol alerts. 4 ≈ 20 min at the 5-min cadence. # One blip must not page anyone; false pages are how people learn to ignore alerts. #STENION_ALERT_THRESHOLD=4 # Webhook for failure and recovery alerts. Unset = alerting off; retries and # failed-run recording carry on regardless. Slack and Discord incoming webhooks both # work as-is, as does anything accepting a JSON POST. # THIS IS A SECRET (the URL is the credential) — set it in Vercel, not here. # Does NOT cover a total database outage: no run row is written, so no streak # advances and no alert fires. That surfaces as the cron route returning 500. # Verify it without waiting for a real outage: # pnpm smoke:alert-webhook # sends both alerts # pnpm smoke:alert-webhook -- --dry-run # print the payload, send nothing #STENION_ALERT_WEBHOOK_URL=https://hooks.slack.com/services/T000/B000/xxxx # --- Optional: public API rate limiting --- # # Per-client token bucket, counted in Postgres (`api_rate_limits`, migration 0005) # because serverless has no shared memory. Only CDN cache MISSES are counted — a # cache hit never reaches the function. The cron route is deliberately exempt. # Nothing here is required: every value has a default, a malformed one falls back to # it, and the limiter fails OPEN if its own query errors. # Full reasoning in ARCHITECTURE.md "Caching and rate limits". # Sustained requests per minute per client. #STENION_RATE_LIMIT_PER_MIN=60 # Burst allowance before the sustained rate binds. #STENION_RATE_LIMIT_BURST=60 # Salt for the client-key hash — an IP is stored as a salted SHA-256 prefix, never # raw. SET THIS IN PRODUCTION: unsalted, the hash is reversible by enumerating IPv4. # Changing it resets every bucket once, which is harmless. # node -e "console.log(require('crypto').randomBytes(16).toString('hex'))" #STENION_RATE_LIMIT_SALT= # Off switch. Only the exact string `true` disables the limiter. #STENION_RATE_LIMIT_DISABLED=false # --- Optional: health endpoint thresholds (/api/v1/health) --- # # Per-protocol run freshness plus one overall status, for an uptime monitor. # Staleness is measured from each protocol's last SUCCESSFUL run — an adapter # failing every cycle has a perpetually fresh lastRunAt. # healthy everything within threshold 200 # degraded some current, some not — probably one broken adapter 503 # down nothing current anywhere — the cron itself looks dead 503 # Full reasoning in ARCHITECTURE.md "The health endpoint"; contract in API.md. # How stale a protocol's last successful run may be, in minutes. 30 = six missed # cycles, and sits above STENION_ALERT_THRESHOLD (~20 min) so the webhook fires # before the monitor goes red. A malformed value falls back to the default. #STENION_HEALTH_STALE_MINUTES=30 # Multiples of the above, with nothing succeeding anywhere, before `down` rather # than `degraded`. 2 = 60 min. A broad RPC outage hits every adapter at once while # our own infrastructure is fine, so blaming the cron takes the longer window. #STENION_HEALTH_DOWN_MULTIPLIER=2 # --- Post-deploy smoke check (scripts/smoke-protocol-404.mjs) --- # # NOT read from this file — the script reads process.env directly. Export them, or # just pass them as arguments, which take precedence either way: # pnpm smoke:protocol-404 https://stenion.vercel.app [some-unknown-id] # Listed here only so every variable the repo understands is in one place. # Asserts /api/v1/protocol/:id 404s for an unknown id. Manual, against a DEPLOYED # URL — `next dev` answers 200 there, so it is not meaningful locally. # Base URL to check. No default; without it (and no argument) the script exits 2. #STENION_SMOKE_BASE_URL=https://stenion.vercel.app # The id to request. Must NOT exist — a real id makes the check fail, correctly. #STENION_SMOKE_PROTOCOL_ID=stenion-smoke-missing-protocol