Repository navigation
Expand file tree
/
Copy path.env.example
More file actions
601 lines (562 loc) · 33.3 KB
/
Copy path.env.example
File metadata and controls
601 lines (562 loc) · 33.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
# --- General ---
# Environment: development, test, production
NODE_ENV=development
# Structured log level: trace, debug, info, warn, error, fatal, or silent (default: info)
# Logs are newline-delimited JSON when NODE_ENV=production, human-readable otherwise.
LOG_LEVEL=info
# --- Server ---
# Port to listen on (default: 3002)
PORT=3002
# Host to bind to (default: 0.0.0.0)
HOST=0.0.0.0
# --- Database ---
# PostgreSQL connection string with credentials
DATABASE_URL=postgresql://lens:lens@localhost:5432/lens
# Direct unpooled PostgreSQL connection string (used by Prisma migrations and CLI to bypass connection poolers)
DIRECT_DATABASE_URL=postgresql://lens:lens@localhost:5432/lens
# Maximum connections per process pool (default: 10).
# Capacity planning arithmetic: total DB connections = (instances * DB_POOL_SIZE) + admin headroom.
# Ensure this product does not exceed Postgres max_connections (typically 100).
DB_POOL_SIZE=10
# Maximum time in milliseconds to wait to acquire a connection from the pool (default: 5000 ms)
DB_POOL_TIMEOUT_MS=5000
# Retention window in days for partitioned price_points table (default: 30)
PRICE_POINTS_RETENTION_DAYS=30
# Explicit opt-in flag to drop expired partitions older than PRICE_POINTS_RETENTION_DAYS (default: false, DESTRUCTIVE).
# Keep disabled unless aggregations/rollups have safely processed raw partition data (#231).
PRICE_POINTS_DROP_EXPIRED=false
# Number of future daily partitions to pre-create ahead of need (default: 7)
PRICE_POINTS_PRECREATE_DAYS=7
# --- Cache & Performance ---
# Redis connection URL for caching and queues
REDIS_URL=redis://localhost:6379
# How long to cache price responses in seconds (default: 10)
PRICE_CACHE_TTL=10
# How long to cache GET /snapshot responses per network in seconds (default: 15)
SNAPSHOT_CACHE_TTL=15
# How long an API-key lookup is cached in-process, in seconds (default: 30).
# Worst-case window for a key revoked on another instance to stop working here.
API_KEY_CACHE_TTL=30
# Default timeout for outbound HTTP requests in milliseconds (default: 10000)
HTTP_TIMEOUT_MS=10000
# --- Outbound request policy (SSRF) ---
# Comma-separated hostnames the app may reach when a caller supplies the target
# URL — e.g. the Bazaar MCP server fetching a node's /discovery endpoint. Leave
# empty to allow any *public* host. Private, loopback, link-local and cloud
# metadata addresses are refused regardless of this list.
# Example: OUTBOUND_ALLOWED_HOSTS=bazaar.example.com,lens.internal.io
OUTBOUND_ALLOWED_HOSTS=
# --- MCP tool result bounds ---
# Tool results are written straight into an agent's context window, so they are
# size-bounded: an unbounded result is a self-inflicted denial of service whose
# failure is misread as a bad model. Truncated results say so in the payload.
# Maximum size, in bytes, of one MCP tool result (default: 16384; 1024..1048576)
MCP_MAX_RESULT_BYTES=16384
# Maximum bytes read from a single upstream response body before the stream is
# cancelled, so an oversized body is never buffered in full
# (default: 262144; 4096..8388608)
MCP_MAX_UPSTREAM_BYTES=262144
# Page size requested of a Bazaar node when the caller gives no limit, and the
# most a caller may ask for (defaults: 50 and 200, matching /discovery)
MCP_DEFAULT_PAGE_LIMIT=50
MCP_MAX_PAGE_LIMIT=200
# --- Active Network ---
# Which network the node operates on: "testnet" or "mainnet" (default: testnet)
# This drives all config.horizon / config.rpc / config.soroswap shortcut paths.
STELLAR_NETWORK=testnet
# Comma-separated list of networks the ingesters run for, e.g. "testnet,mainnet".
# Each venue ingester (SDEX, AMM, Soroswap, Snapshot, Aquarius, DeFindex) is started once
# Each venue ingester (SDEX, AMM, Soroswap, Snapshot, Aquarius, Phoenix) is started once
# per network listed here, each instance independently fault-isolated.
# When unset (or nothing valid remains after parsing) it falls back to just
# STELLAR_NETWORK, so a single-network node starts one instance of each ingester.
ENABLED_NETWORKS=testnet
# ─────────────────────────────────────────────────────────────────────────────
# Per-network Stellar configuration
#
# For each field below, Lens resolves the value in this priority order:
# 1. Paired var — e.g. HORIZON_URL_TESTNET / HORIZON_URL_MAINNET
# 2. Generic var — e.g. HORIZON_URL (back-compat; applied to testnet only)
# 3. Built-in default for that network
#
# Only the vars for your active network need to be set; the other network's
# values are only used when STELLAR_NETWORK switches to it.
# ─────────────────────────────────────────────────────────────────────────────
# --- Stellar Network — Testnet ---
HORIZON_URL_TESTNET=https://horizon-testnet.stellar.org
RPC_URL_TESTNET=https://soroban-testnet.stellar.org
NETWORK_PASSPHRASE_TESTNET=Test SDF Network ; September 2015
# Soroswap factory contract (56-char C... id) — from https://github.com/soroswap/core (public/testnet.contracts.json).
# A malformed id disables Soroswap on this network and logs a warning naming this variable.
SOROSWAP_FACTORY_ADDRESS_TESTNET=CDP3HMUH6SMS3S7NPGNDJLULCOXXEPSHY4JKUKMBNQMATHDHWXRRJTBY
# Soroswap has a testnet deployment; leave enabled. Set to "false" to disable.
SOROSWAP_ENABLED_TESTNET=true
# Aquarius has no public testnet deployment today — disabled by default.
AQUARIUS_ENABLED_TESTNET=false
# DeFindex vault factory contract (56-char C... id) — from
# https://github.com/defindex-io/stellar-contracts (public/testnet.contracts.json).
# Testnet redeploys often, so refresh this when the published id changes.
# A malformed id disables DeFindex on this network and logs a warning naming this variable.
DEFINDEX_FACTORY_ADDRESS_TESTNET=CDSCWE4GLNBYYTES2OCYDFQA2LLY4RBIAX6ZI32VSUXD7GO6HRPO4A32
DEFINDEX_ENABLED_TESTNET=true
# Phoenix (Soroban AMM) has no published testnet deployment, so there is no
# built-in factory id here: leave empty and the venue stays disabled on testnet.
# Point it at a factory you deployed (Phoenix's `contracts/factory`) to enable it.
PHOENIX_FACTORY_ADDRESS_TESTNET=
PHOENIX_ENABLED_TESTNET=true
# Reflector oracle contract (56-char C... id) — see https://reflector.network. Empty = oracle disabled.
# A malformed id disables the oracle on this network and logs a warning naming this variable.
REFLECTOR_CONTRACT_ID_TESTNET=
# Blend lending pools (comma-separated 56-char C... ids) on testnet.
BLEND_POOLS_TESTNET=CCEBVDYM32YNYCVNRXQKDFFPISJJCV557CDZEIRBEE4NCV4KHPQ44HGF
# Blend backstop contract on testnet (56-char C... id).
BLEND_BACKSTOP_TESTNET=CBDVWXT433PRVTUNM56C3JREF3HIZHRBA64NB2C3B2UNCKIS65ZYCLZA
# Blend lending rate ingestion enabled on testnet (default: true).
BLEND_ENABLED_TESTNET=true
# SEP-40 publisher (#222): the oracle contract Lens pushes ITS aggregate into,
# so Soroban contracts can read a Lens price. Deploy it from oracle/contract/
# and paste the id `soroban contract deploy` prints — never a placeholder.
SEP40_CONTRACT_ID_TESTNET=
# Secret key of the account the contract lists as publisher. It pays the write
# fee, so it must be funded on testnet. Never commit a real key; docs/
# sep40-oracle-publisher.md covers funding, separation from the admin key and
# rotation. Publication stays off while this is unset.
SEP40_PUBLISHER_SECRET_KEY_TESTNET=
# Off by default: publishing spends fees and writes to a chain, so it is
# opt-in per network. Setting this to true without the two values above logs a
# warning and leaves publication off rather than half-working.
SEP40_ENABLED_TESTNET=false
# Comma-separated pairs to watch on testnet.
# Format: "CODE:ISSUER/CODE:ISSUER". Use "native" for XLM.
WATCHED_PAIRS_TESTNET=XLM:native/USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5
# Multi-hop routing configuration for testnet
ROUTING_MAX_HOPS_TESTNET=3
ROUTING_INTERMEDIATES_TESTNET=XLM:native,USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5
# --- Stellar Network — Mainnet ---
HORIZON_URL_MAINNET=https://horizon.stellar.org
# Mainnet has no free Soroban RPC — set an external provider endpoint here.
# Placeholder only; never commit a real provider URL or API key.
RPC_URL_MAINNET=https://your-provider.example.com/soroban-rpc
NETWORK_PASSPHRASE_MAINNET=Public Global Stellar Network ; September 2015
# Mainnet Soroswap factory contract (56-char C... id) — see https://github.com/soroswap/core
SOROSWAP_FACTORY_ADDRESS_MAINNET=CA4HEQTL2WPEUYKYKCDOHCDNIV4QHNJ7EL4J4NQ6VADP7SYHVRYZ7AW2
SOROSWAP_ENABLED_MAINNET=true
AQUARIUS_ENABLED_MAINNET=true
# DeFindex vault factory contract on mainnet (56-char C... id) — from
# https://github.com/defindex-io/stellar-contracts (public/mainnet.contracts.json).
# A malformed id disables DeFindex on this network and logs a warning naming this variable.
DEFINDEX_FACTORY_ADDRESS_MAINNET=CDKFHFJIET3A73A2YN4KV7NSV32S6YGQMUFH3DNJXLBWL4SKEGVRNFKI
DEFINDEX_ENABLED_MAINNET=true
# Phoenix AMM (Soroban) factory contract (56-char C... id) — taken verbatim from
# Phoenix's own repository: scripts/extend-queries.sh and scripts/deploy_pool.sh
# both set FACTORY to this id alongside mainnet.sorobanrpc.com and the public
# passphrase (github.com/Phoenix-Protocol-Group/phoenix-contracts). Pools are
# enumerated from it with `query_pools`, never from a list kept in this repo.
# A malformed id disables Phoenix on this network and logs a warning naming this
# variable; an empty id disables it silently.
PHOENIX_FACTORY_ADDRESS_MAINNET=CB4SVAWJA6TSRNOJZ7W2AWFW46D5VR4ZMFZKDIKXEINZCZEGZCJZCKMI
PHOENIX_ENABLED_MAINNET=true
# Reflector oracle contract on mainnet (56-char C... id) — copy it from https://reflector.network.
# There is no built-in default: leave empty to keep the oracle disabled.
REFLECTOR_CONTRACT_ID_MAINNET=
# Blend lending pools (comma-separated 56-char C... ids) on mainnet.
BLEND_POOLS_MAINNET=CAJJZSGMMM3PD7N33TAPHGBUGTB43OC73HVIK2L2G6BNGGGYOSSYBXBD,CCCCIQSDILITHMM7PBSLVDT5MISSY7R26MNZXCX4H7J5JQ5FPIYOGYFS,CDVQVKOY2YSXS2IC7KN6MNASSHPAO7UN2UR2ON4OI2SKMFJNVAMDX6DP,CBP7NO6F7FRDHSOFQBT2L2UWYIZ2PU76JKVRYAQTG3KZSQLYAOKIF2WB
# Blend backstop contract on mainnet (56-char C... id).
BLEND_BACKSTOP_MAINNET=CAQQR5SWBXKIGZKPBZDH3KM5GQ5GUTPKB7JAFCINLZBC5WXPJKRG3IM7
# Blend lending rate ingestion enabled on mainnet (default: true).
BLEND_ENABLED_MAINNET=true
# SEP-40 publisher on mainnet. Same three variables as testnet above; mainnet is
# additionally gated on a funded publisher account and a real RPC provider
# (RPC_URL_MAINNET), so read docs/sep40-oracle-publisher.md before enabling it.
SEP40_CONTRACT_ID_MAINNET=
SEP40_PUBLISHER_SECRET_KEY_MAINNET=
SEP40_ENABLED_MAINNET=false
# Comma-separated pairs to watch on mainnet.
WATCHED_PAIRS_MAINNET=XLM:native/USDC:GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN
# Multi-hop routing configuration for mainnet
ROUTING_MAX_HOPS_MAINNET=3
ROUTING_INTERMEDIATES_MAINNET=XLM:native,USDC:GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN
# --- Venue endpoint overrides (optional, shared across networks unless a
# paired _TESTNET / _MAINNET variant is set) ---
# Soroswap token-list JSON URL.
SOROSWAP_TOKEN_LIST_URL=https://raw.githubusercontent.com/soroswap/token-list/main/tokenList.json
# Aquarius AMM pools API base URL.
AQUARIUS_API_URL=https://amm.aquarius.network/api/v1/pools/
# How often to poll DeFindex vault share prices in milliseconds (default: 60000 = 60s)
DEFINDEX_POLL_INTERVAL_MS=60000
# --- Back-compat single-network vars (testnet) ---
# These are still respected when the paired _TESTNET vars above are unset.
# New deployments should prefer the paired vars above.
HORIZON_URL=https://horizon-testnet.stellar.org
RPC_URL=https://soroban-testnet.stellar.org
NETWORK_PASSPHRASE=Test SDF Network ; September 2015
SOROSWAP_FACTORY_ADDRESS=CDP3HMUH6SMS3S7NPGNDJLULCOXXEPSHY4JKUKMBNQMATHDHWXRRJTBY
# DeFindex vault factory contract ID on testnet (falls back from paired DEFINDEX_FACTORY_ADDRESS_TESTNET)
DEFINDEX_FACTORY_ADDRESS=CDSCWE4GLNBYYTES2OCYDFQA2LLY4RBIAX6ZI32VSUXD7GO6HRPO4A32
# Phoenix factory id for testnet only (applied when
# PHOENIX_FACTORY_ADDRESS_TESTNET is unset). Never used on mainnet, so a
# single-network testnet value cannot override mainnet's built-in factory.
PHOENIX_FACTORY_ADDRESS=
WATCHED_PAIRS=XLM:native/USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5
# Routing configuration fallbacks
ROUTING_MAX_HOPS=3
ROUTING_INTERMEDIATES=XLM:native,USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5
# Reflector oracle contract ID on testnet (falls back from paired REFLECTOR_CONTRACT_ID_TESTNET)
REFLECTOR_CONTRACT_ID=
# Blend lending pools on testnet (falls back from paired BLEND_POOLS_TESTNET)
BLEND_POOLS=CCEBVDYM32YNYCVNRXQKDFFPISJJCV557CDZEIRBEE4NCV4KHPQ44HGF
# Blend backstop contract on testnet (falls back from paired BLEND_BACKSTOP_TESTNET)
BLEND_BACKSTOP=CBDVWXT433PRVTUNM56C3JREF3HIZHRBA64NB2C3B2UNCKIS65ZYCLZA
# Blend lending rate ingestion enabled (falls back from paired BLEND_ENABLED_TESTNET)
BLEND_ENABLED=true
# SEP-40 publisher fallbacks on testnet (fall back from the paired _TESTNET vars above)
SEP40_CONTRACT_ID=
SEP40_PUBLISHER_SECRET_KEY=
# --- SEP-40 Oracle Publisher (issue #222) ---
# WHEN Lens writes its aggregate to the contract; the contract/keys per network
# are configured above. The feed is deviation-triggered with a heartbeat, not a
# write per tick — writing every sample is what makes an on-chain oracle
# unaffordable, and these are the knobs that decide the difference.
# Publish once the aggregate has moved at least this many percent away from the
# value already on chain (default 0.5).
SEP40_DEVIATION_PCT=0.5
# …and publish at least once per this many milliseconds even if it has not moved
# (default 900000 = 15 min), so a quiet market does not leave consumers reading
# a timestamp that only gets older.
SEP40_MAX_INTERVAL_MS=900000
# Hard floor between two writes for the same asset, whatever triggered them
# (default 60000). This is the rate limit: it caps the transaction count in a
# fast market at one write per asset per interval, not one per sample.
SEP40_MIN_WRITE_INTERVAL_MS=60000
# Never publish an aggregate whose newest contributing observation is older than
# this many seconds (default 120). Stricter than the API's own 300 s stale flag
# on purpose: an HTTP price can be "as good as we have it", a price a contract
# settles against cannot. See docs/sep40-oracle-publisher.md.
SEP40_MAX_AGE_SECONDS=120
# How often the publisher evaluates the triggers above (default 30000). Shorter
# means a deviation is caught sooner; it does NOT mean more writes, because
# SEP40_MIN_WRITE_INTERVAL_MS bounds those.
SEP40_POLL_INTERVAL_MS=30000
# --- Indexer Configuration ---
# Frequency of polling for new trades/snapshots in milliseconds (default: 5000)
POLL_INTERVAL_MS=5000
# Number of SDEX trades to fetch per page (default: 200)
SDEX_PAGE_SIZE=200
# Number of AMM trades to fetch per page (default: 200)
AMM_PAGE_SIZE=200
# How often to poll Soroswap pool reserves in milliseconds (default: 60000 = 60s)
SOROSWAP_POLL_INTERVAL_MS=60000
# How often to poll Blend pool reserves in milliseconds (default: 60000 = 60s)
BLEND_POLL_INTERVAL_MS=60000
# Per-ingester cycle deadline in milliseconds (default: 60000 = 60s). A poll
# cycle that overruns it is aborted and counted in the
# ingester_abandoned_cycles_total metric instead of stalling the ingester loop.
# Set 0 to disable the deadline. Precedence, most specific first:
# INGEST_CYCLE_DEADLINE_MS_<VENUE>_<NETWORK> e.g. INGEST_CYCLE_DEADLINE_MS_SOROSWAP_MAINNET
# INGEST_CYCLE_DEADLINE_MS_<VENUE> venues: SDEX, AMM, Soroswap, Aquarius, Snapshot
# INGEST_CYCLE_DEADLINE_MS_<NETWORK>, then INGEST_CYCLE_DEADLINE_MS
INGEST_CYCLE_DEADLINE_MS=60000
INGEST_CYCLE_DEADLINE_MS_TESTNET=60000
INGEST_CYCLE_DEADLINE_MS_MAINNET=60000
# How often to poll Phoenix pool reserves in milliseconds (default: 60000 = 60s)
PHOENIX_POLL_INTERVAL_MS=60000
# How long a Phoenix pool enumeration is reused before the factory is asked
# again, in milliseconds (default: 600000 = 10 min). Discovery reads every pool
# once, so this is what keeps it off the critical path of every price poll. A
# failed refresh keeps the previous list rather than extending this timer.
PHOENIX_POOL_CACHE_TTL_MS=600000
# --- WebSocket backpressure (/ws) ---
# See docs/websocket-backpressure.md for the full policy.
# Max price updates per second per client (default: 100). 0 disables the window.
WS_COALESCE_INTERVAL_MS=100
# Bytes a client socket may have queued before updates are coalesced to the
# newest one instead of sent (default: 262144 = 256 KiB). Clamped to half of
# WS_MAX_BUFFERED_BYTES if it is set higher.
WS_SLOW_CLIENT_BUFFER_BYTES=262144
# Bytes a client socket may have queued before it is disconnected with close
# code 1013 (default: 1048576 = 1 MiB).
WS_MAX_BUFFERED_BYTES=1048576
# How long a client may stay congested before it is disconnected anyway
# (default: 30000). Coalescing bounds the memory a slow client costs; this
# bounds the time. 0 keeps congested clients indefinitely.
WS_MAX_CONGESTED_MS=30000
# How often the aggregate ws_buffered_bytes gauge is refreshed in milliseconds
# (default: 5000).
WS_BUFFER_SWEEP_INTERVAL_MS=5000
# Interval in milliseconds between WebSocket ping frames (default: 30000).
WS_PING_INTERVAL_MS=30000
# Number of consecutive missed pings before a client is terminated (default: 2).
WS_MAX_MISSED_PINGS=2
# --- Auth & Security ---
# Secret key required for admin endpoints (e.g., adding/removing pairs)
ADMIN_API_KEY=super-secret-admin-key
# Shared secret guarding the API-key admin endpoints (POST/DELETE /admin/keys,
# GET /admin/usage) and the `npm run key:issue` CLI. Send as X-Admin-Token header
# or Authorization: Bearer <token>. When unset, empty, or a placeholder, the admin
# surface is disabled at startup and admin routes return 503 admin_not_configured.
ADMIN_TOKEN=super-secret-admin-token
# Set to "false" to disable API-key auth entirely (e.g. local dev). Any other
# value (or unset) leaves auth enabled, so every non-public route needs a key.
REQUIRE_API_KEY=true
# Minimum interval, in seconds, between database writes stamping an API key's
# `lastUsedAt`. Authentication is otherwise a read; without a throttle a busy
# key turns every authenticated request into a write. 0 disables the throttle
# and writes on every request (default: 60).
KEY_LAST_USED_THROTTLE_SECONDS=60
# Default overlap duration in hours for key rotation via POST /admin/keys/:id/rotate (default: 24)
# During this window, both old and replacement keys function simultaneously to prevent service disruption.
KEY_ROTATION_OVERLAP_HOURS=24
# Fallback rate limit (requests per minute per IP) for unauthenticated or public requests (default: 100)
RATE_LIMIT_IP_MAX=100
# Requests this single instance will accept per minute in total, across every
# caller, before shedding with 503 (default: 1000). Must be a positive integer;
# an invalid value is a startup error rather than "no ceiling". This is a
# per-process self-defence limit, not a quota — with several instances the fleet
# ceiling is this times the instance count, which is the intended behaviour.
# Expensive route templates are shed at 80% of it, and /status, /live and /ready
# are exempt.
GLOBAL_RATE_LIMIT_MAX=1000
# --- Health probes ---
# GET /live is liveness (no dependency checks) and GET /ready is readiness
# (boot complete + Postgres reachable/migrated + Redis). Both are public and
# exempt from rate limiting and from http_requests_total, because a platform
# polls them on a fixed interval.
#
# Whether Redis takes part in readiness (default: true). Set to "false" for a
# deployment that deliberately runs Redis-less: startup already treats an
# unavailable Redis as non-fatal and serves prices uncached from Postgres, so
# failing readiness there would pull a healthy instance out of rotation.
READINESS_REDIS_CHECK=true
# Milliseconds a single dependency probe may take before /ready calls it down
# (default: 2000). Kept well under the platform's probe interval — a probe
# slower than its own polling period reports stale answers.
READINESS_TIMEOUT_MS=2000
# How long /ready reuses one answer before re-probing (default: 2000). Bounds
# how many dependency round trips a fleet-wide poll interval can produce, and
# how many outstanding probes a slow database can accumulate.
READINESS_CACHE_MS=2000
# Maximum number of GraphQL field selections from an operation root to a leaf
# (default: 3). Must be a positive integer. Pure __schema/__type introspection
# is exempt so GraphiQL can load; mixed operations still limit application fields.
GRAPHQL_MAX_QUERY_DEPTH=3
# Comma-separated list of trusted web origins permitted to call credentialed routes (/usage/me).
# Empty/unset refuses cross-origin requests on credentialed routes.
CORS_ALLOWED_ORIGINS=https://app.lens.stellar.org,http://localhost:3000
# Comma-separated list of web origins permitted to call operator /admin/* routes.
# Default empty: closed to cross-origin browsers.
CORS_ADMIN_ALLOWED_ORIGINS=
# Comma-separated list of web origins explicitly blocked from calling public routes (/price, /health, /openapi.json).
# Empty/unset means all public cross-origin requests are permitted (* without credentials).
CORS_PUBLIC_BLOCKED_ORIGINS=
# --- x402 Payment Gate ---
# Stellar public key where API payments should be sent.
# If unset, x402 gating is disabled.
# Optionally set ORACLE_PAYMENT_ADDRESS_TESTNET / ORACLE_PAYMENT_ADDRESS_MAINNET
# to use a different payout address per network (falls back to the shared
# ORACLE_PAYMENT_ADDRESS above for whichever one is unset).
ORACLE_PAYMENT_ADDRESS=GD...
# Optional network-specific Stellar payout address for testnet payments (overrides ORACLE_PAYMENT_ADDRESS on testnet)
ORACLE_PAYMENT_ADDRESS_TESTNET=
# Optional network-specific Stellar payout address for mainnet payments (overrides ORACLE_PAYMENT_ADDRESS on mainnet)
ORACLE_PAYMENT_ADDRESS_MAINNET=
# URL of the x402 facilitator (default: https://x402.org/facilitator)
X402_FACILITATOR_URL=https://x402.org/facilitator
# --- Bazaar catalog integrity ---
# Catalog listings arrive from clients in the payment payload, so writes are
# rate limited per payTo. A rejected listing never fails the payment.
BAZAAR_CATALOG_WRITES_PER_MIN=10
BAZAAR_CATALOG_WRITES_PER_DAY=200
# --- Oracle Relay Example ---
# Lens API base URL consumed by the relay example.
ORACLE_RELAY_API_URL=http://localhost:3002
# Soroban RPC endpoint for transaction submission.
ORACLE_RELAY_RPC_URL=https://soroban-testnet.stellar.org
# Stellar network passphrase for transaction signing.
ORACLE_RELAY_NETWORK_PASSPHRASE=Test SDF Network ; September 2015
# Soroban contract ID for the relay example.
ORACLE_RELAY_CONTRACT_ID=CC...
# Secret key for the relay's source account.
ORACLE_RELAY_SOURCE_SECRET=S...
# Asset pair pushed to the oracle contract.
ORACLE_RELAY_ASSET_A=XLM
ORACLE_RELAY_ASSET_B=USDC
# How often the relay polls Lens and updates the contract.
ORACLE_RELAY_INTERVAL_MS=60000
# --- Price Alert Bot Example ---
# Lens WebSocket endpoint the alert bot connects to.
ALERT_BOT_WS_URL=ws://localhost:3002/ws
# Pair to watch and the threshold to alert on.
ALERT_BOT_ASSET_A=XLM
ALERT_BOT_ASSET_B=USDC
ALERT_BOT_THRESHOLD=0.15
# Alert direction: "above" or "below".
ALERT_BOT_DIRECTION=above
# Optional base64 X-PAYMENT header for x402-gated streams.
ALERT_BOT_PAYMENT=
# Optional HTTPS URL to forward alerts to, plus its HMAC secret.
ALERT_BOT_NOTIFY_URL=
ALERT_BOT_NOTIFY_SECRET=alert-bot
# --- x402 Facilitator (POST /settle) ---
# The Stellar secret key the facilitator signs settlement transactions with,
# per network. This is a fee and sequence-number key only: the payer authorises
# inside the payload as a signed Soroban auth entry, so no payer key is ever
# held by this process. Leave unset to run without settlement — /settle then
# fails explicitly rather than half-working, and /supported and the Bazaar
# still serve.
FACILITATOR_SECRET_KEY_TESTNET=
FACILITATOR_SECRET_KEY_MAINNET=
# Unsuffixed form, applied to testnet only.
FACILITATOR_SECRET_KEY=
# Maximum fee in stroops the facilitator will pay (default 50000).
FACILITATOR_FEE_STROOPS_TESTNET=50000
FACILITATOR_FEE_STROOPS_MAINNET=50000
# Per-settlement spend ceiling (#147). Settlements exceeding this native amount are rejected.
FACILITATOR_SPEND_CEILING=
# Comma-separated allowlist of permitted caller API key IDs (#147).
FACILITATOR_CALLER_ALLOWLIST=
# --- Price Plausibility Sanity Bounds ---
# Lower and upper bounds for sanity-checking derived venue prices.
# Derived spot prices outside this range are rejected with a log warning and metric increment.
PRICE_MIN_PLAUSIBLE=0.0000000001
PRICE_MAX_PLAUSIBLE=10000000000
# --- MCP server (npm run mcp:server) ---
# The agent's own x402 payer keys, used to answer a 402 challenge when paid_call
# fetches a gated resource. These spend money, so keep them out of any other
# process and fund only what you are willing to lose.
MCP_AGENT_SECRET_KEY_TESTNET=
# WARNING: MCP_AGENT_SECRET_KEY_MAINNET signs transactions on the public network
# and can spend real funds. Keep this out of any other process and fund only
# what you are willing to lose.
MCP_AGENT_SECRET_KEY_MAINNET=
# Explicit opt-in required to enable mainnet payment signing (even if MCP_AGENT_SECRET_KEY_MAINNET is set):
MCP_ENABLE_MAINNET=false
# Alias for MCP_ENABLE_MAINNET:
MCP_ALLOW_MAINNET=false
# Transport to serve: "stdio" (default) or "http". stdio speaks over the process's
# stdin/stdout and only works for a client that can launch this process on the
# same machine; "http" serves Streamable HTTP so remote/hosted clients can connect.
MCP_TRANSPORT=stdio
# Required when MCP_TRANSPORT=http, and refused without it. Every request must
# carry `Authorization: Bearer <token>`. Generate a strong random value
# (e.g. `openssl rand -hex 32`); it authorises paid_call spending, so treat it
# as a credential, and never put it in a URL.
MCP_HTTP_AUTH_TOKEN=
# Bind address for the HTTP transport (default: 127.0.0.1). Reachability beyond
# loopback is only safe behind a TLS-terminating reverse proxy.
MCP_HTTP_HOST=127.0.0.1
# Port for the HTTP transport (default: 3003; the REST API listens on PORT=3002).
MCP_HTTP_PORT=3003
# Path the HTTP transport answers MCP POSTs on (default: /mcp).
MCP_HTTP_PATH=/mcp
# Ceiling per paid_call, enforced by the x402 fetch wrapper.
MCP_MAX_PAYMENT_PRICE=$1.00
# Comma-separated origins (or URL prefixes) paid_call may pay for, e.g.
# "https://api.lens.example,localhost:3000". An entry with no scheme matches
# both http and https; an entry with a path only allows that path and what sits
# under it. UNSET IS EMPTY, AND AN EMPTY LIST PAYS NOBODY: every paid_call is
# refused with ERR_HOST_NOT_ALLOWED before a request is sent. This is a
# prompt-injection guard — text an agent reads must not choose where money goes.
MCP_PAID_CALL_ALLOWED_HOSTS=
# Path to the OpenAPI document served by the MCP `lens://openapi.json` resource.
# Leave unset to serve the openapi.json at the package root; set it only when the
# document lives elsewhere, e.g. mounted separately in a container.
MCP_OPENAPI_PATH=
# ─── Snapshot retention ────────────────────────────────────────────────────
# Rows removed by one DELETE during a retention run. The job drains in batches
# of this size, skipping locked rows and yielding between them, so a first run
# against a table that has never been pruned cannot hold a long lock or blow
# the WAL. Default 1000.
SNAPSHOT_PRUNE_BATCH_SIZE=1000
# ─── Venue Dispersion Reconciliation ──────────────────────────────────────────
# How often periodic cross-venue reconciliation runs in milliseconds (default: 60000 = 1 minute)
RECONCILIATION_INTERVAL_MS=60000
# Maximum age of quotes in seconds to be considered fresh (default: 300 = 5 minutes)
RECONCILIATION_FRESHNESS_SECONDS=300
# Default cross-venue dispersion threshold percentage to trigger alerts (default: 2.0 = 2%)
RECONCILIATION_THRESHOLD_PCT=2.0
# Minimum cooldown period in seconds between repeated alerts for the same pair (default: 300 = 5 minutes)
RECONCILIATION_ALERT_COOLDOWN_SECONDS=300
# Optional comma-separated per-pair threshold overrides in percentage, e.g. "XLM/USDC:1.5,EURC/USDC:3.0"
RECONCILIATION_PAIR_THRESHOLDS=
# Rows removed by one DELETE during a lending rate retention run (default 1000).
RATE_PRUNE_BATCH_SIZE=1000
# --- MCP agent spending limits ---
# Most a single payment may cost. Enforced as the x402 client's per-payment cap
# (spendControls.maxAmountPerPayment), so a call priced higher is refused before
# any money moves (default: $1.00).
MCP_MAX_PAYMENT_PRICE=$1.00
# Cumulative ceiling on what paid calls may cost in one UTC day. The per-call cap
# above only bounds one mistake; without this, an agent in a retry loop can pay
# that amount over and over. Once a further call's worst case no longer fits the
# remaining budget, paid_call is refused with ERR_BUDGET_EXCEEDED without
# attempting payment. Read live with the payment_budget tool (default: $10.00).
MCP_DAILY_SPEND_CEILING=$10.00
# Where the day's running total is kept, so a server restart does not refill the
# budget: the total is per UTC day and read back from this file at startup, and
# the window still resets at 00:00 UTC. If the file is missing it starts at zero;
# if it is unreadable or unwritable the budget is treated as spent and paid calls
# are refused until it is fixed (fail closed). Keep it off ephemeral filesystems
# (default: ~/.lens-mcp/spend-state.json).
MCP_SPEND_STATE_FILE=
# Where the payment audit trail is appended, one JSON record per paid call:
# timestamp, full URL, host, method, price, asset, network, transaction hash and
# the outcome. Never the secret key and never the signed payment envelope. A call
# that failed is recorded too, since a payment can settle on a call that errored.
# If this file cannot be created or written, paid calls are refused: a tool that
# moves money does not get to spend without a record (fail closed). Keep it off
# ephemeral filesystems (default: ~/.lens-mcp/payments.jsonl).
MCP_PAYMENT_LEDGER_FILE=
# Comma-separated hosts allowed to become the `host` label of the spend metrics.
# Prometheus series are permanent, so labelling by whatever URL an agent asks for
# lets one caller mint unbounded time series; hosts outside this list are reported
# as `unknown`, and an empty value reports every host as `unknown`. The audit file
# above always keeps the real host regardless (e.g. bazaar.example,price.feed).
MCP_METRIC_HOSTS=
# Comma-separated network ids allowed to become the `network` label of the spend
# metrics. Same bounded-series rule as above. The value here is the default — the
# networks this agent signs for — spelled out, because the .env.example drift
# guard reads a commented entry as undocumented. Setting it to an empty value
# instead reports every network as `unknown`.
MCP_METRIC_NETWORKS=stellar:testnet,stellar:pubnet
# With MCP_TRANSPORT=http the registry above is scrapeable from the MCP server
# itself: GET /metrics on MCP_HTTP_PORT, requiring the same MCP_HTTP_AUTH_TOKEN
# bearer token. stdio starts no listener, so there the running total is what the
# payment_budget tool reports and what the audit file holds.
# Where `npm run validate:live-spec` points when checking the published
# OpenAPI document against a running service. LIVE_API_URL wins if both are
# set; falling back to LENS_URL keeps one variable for anyone who already has
# it configured. Unset means localhost:3002, which is the API's default port,
# so a local run needs neither.
LIVE_API_URL=
LENS_URL=
# Base URL for Coinbase Exchange's public market-data API, used by the CEX
# adapter to read reference prices. Only worth setting to point at a sandbox or
# a recording proxy when testing; the adapter sends no credentials, so this is
# not a place to route through anything untrusted. Unset means
# https://api.exchange.coinbase.com.
COINBASE_EXCHANGE_BASE_URL=
# ─── Price points retention ────────────────────────────────────────────────
# Retention window in days for raw price_points rows before they are pruned.
# Raw points older than this window are rolled up into 1-hour price_aggregates
# before deletion, preserving full OHLCV, VWAP, venue volume breakdown, and
# trade counts while bounding database storage on shared tiers.
# Default: 30 days (defensible because downstream volume queries inspect up to
# 30-day windows, e.g. /volumes/:asset?window=30d, and it aligns with snapshot
# retention while preventing unbounded table growth).
PRICE_POINTS_RETENTION_DAYS=30
# Maximum rows removed by one database statement during a retention run.
# The job deletes in bounded batches using FOR UPDATE SKIP LOCKED and pauses
# between batches so it never holds long table locks.
# Default: 1000.
PRICE_POINTS_PRUNE_BATCH_SIZE=1000
# --- Subscription fan-out benchmark (scripts/benchmarkSubscriptions.ts) ---
# Only read by the benchmark script, not by the server. A label stamped into
# the script's JSON result so separate runs can be told apart.
BENCH_MODE=unlabelled
# Subscribers per (pair, network), and events published per (pair, network).
# The default shape — 20 subscribers x 10 pairs x 2 networks = 400
# subscriptions — fills every subscription cleanly and is a representative
# fan-out; the script refuses to start beyond roughly 500 concurrent sockets
# because the sandbox fd cap would garble the numbers.
BENCH_SUBSCRIBERS_PER_PAIR=20
BENCH_EVENTS_PER_PAIR=200