-
-
Notifications
You must be signed in to change notification settings - Fork 26
Expand file tree
/
Copy path.env.example
More file actions
1555 lines (1416 loc) · 88.2 KB
/
Copy path.env.example
File metadata and controls
1555 lines (1416 loc) · 88.2 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
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# ────────────────────────────────────────────────────────────────────────────
# three.ws — backend environment
# Copy to .env.local for local dev. In production, set these in Vercel.
# ────────────────────────────────────────────────────────────────────────────
# Public site origin (no trailing slash)
PUBLIC_APP_ORIGIN=https://three.ws
# Postgres (Neon serverless driver — HTTPS connection string)
DATABASE_URL=postgres://user:pass@ep-xxx.neon.tech/neondb?sslmode=require
# S3-compatible storage (AWS S3, Cloudflare R2, Backblaze B2, etc.)
S3_ENDPOINT=
S3_ACCESS_KEY_ID=
S3_SECRET_ACCESS_KEY=
S3_BUCKET=3d-agent-avatars
# Public CDN base URL for the bucket (custom domain or provider default)
S3_PUBLIC_DOMAIN=https://pub-<hash>.r2.dev
# Public asset URLs are published as https://three.ws/cdn/<r2-key> (api/cdn-object.js),
# not as the bucket origin above: that origin is rate-limited and CORS-restricted.
# Upstash Redis — REQUIRED in production for x402 payment idempotency (USE-15).
# Without these, every /api/x402/* endpoint exits with code 1 on cold-start (500).
# Create a database at https://console.upstash.com → Redis, then copy the REST URL
# and REST token. If you linked via the Vercel KV integration the vars are named
# three_KV_REST_API_URL / three_KV_REST_API_TOKEN — re-add them under these names.
UPSTASH_REDIS_REST_URL=
UPSTASH_REDIS_REST_TOKEN=
# Optional dedicated CACHE store (recommended at scale). The vars above back the
# fail-closed rate limiter; pointing the best-effort caches (galaxy feed, pulse,
# agent lists, …) at a SECOND Upstash store stops large cache writes from contending
# with — and burning the command quota of — the limiter. That contention is what
# causes the redis SET timeouts on /api/galaxy/flows. Co-locate this store with your
# prod Vercel region for low write latency. Unset = caches share the store above (no
# behavior change). Vercel-KV second store prefixed `cache`: cache_KV_REST_API_*.
UPSTASH_CACHE_REST_URL=
UPSTASH_CACHE_REST_TOKEN=
# Optional cache tuning (api/_lib/cache.js). CMD timeout: per-command deadline in ms
# before falling back to memory (default 3000, clamped 500–30000) — raise it when the
# cache store is not co-located with the function region. Max value bytes: values whose
# serialized JSON exceeds this stay memory-only instead of timing out over REST and
# flapping the write-suppression gate (default 262144 = 256KB, clamped 16KB–8MB).
CACHE_REDIS_CMD_TIMEOUT_MS=
CACHE_REDIS_MAX_VALUE_BYTES=
# Optional: Error monitoring
SENTRY_DSN=
# Optional: Payment/business metrics → Axiom (success rate, p95 latency, per network).
# Unset = no-op (see api/_lib/axiom.js). Create an API token + dataset at axiom.co,
# then set both. AXIOM_URL overrides the host (e.g. https://api.eu.axiom.co for EU).
AXIOM_TOKEN=
AXIOM_DATASET=
# JWT signing — rotate by appending to key set, never remove the old one mid-rotation
# Generate with: openssl rand -base64 64
JWT_SECRET=
# Active key id (for future rotation)
JWT_KID=k1
# Dedicated secret for encrypting custodial agent wallet private keys at rest
# (AES-256-GCM via HKDF, with a random per-record salt). Keep this DISTINCT from
# JWT_SECRET so wallet confidentiality never depends on the session secret and
# rotating one doesn't affect the other. If unset, the app falls back to
# JWT_SECRET (with a warning). Generate with: openssl rand -base64 48
WALLET_ENCRYPTION_KEY=
# Password hashing cost (bcryptjs rounds)
PASSWORD_ROUNDS=11
# /api/chat picks a provider in this order (first key found wins, unless the
# client explicitly sets `provider` in the request body):
# 1. ANTHROPIC_API_KEY → Claude Sonnet 4.6 (best tool calling)
# 2. OPENROUTER_API_KEY → free-tier models (Llama 3.3 70B, GPT-OSS, Hermes 3)
# 3. GROQ_API_KEY → Llama 3.3 70B on Groq (sub-second latency)
# 4. OPENAI_API_KEY → gpt-4o-mini
# Anthropic — get a key at https://console.anthropic.com
ANTHROPIC_API_KEY=
# Optional overrides for chat model + output cap.
CHAT_MODEL=claude-sonnet-4-6
CHAT_MAX_TOKENS=1024
# Admin key for three.ws chat brand config. Set in Vercel, enter in chat settings to unlock branding panel.
# Generate with: openssl rand -base64 32
CHAT_ADMIN_KEY=
# OpenRouter — powers free-tier models in /api/chat AND the built-in proxy at
# /api/chat/proxy. Free-tier (`:free` suffix) models include Llama 3.3 70B,
# GPT-OSS 120B, Hermes 3 405B. Sign up free at https://openrouter.ai
OPENROUTER_API_KEY=
# Groq — fastest hosted Llama (sub-second). Get a key at https://console.groq.com
GROQ_API_KEY=
# NVIDIA NIM — free, OpenAI-compatible inference for 100+ hosted models
# (Nemotron, DeepSeek, Kimi, MiniMax, Llama 4). One key unlocks all of them at
# https://integrate.api.nvidia.com/v1. Powers the NVIDIA providers in
# /api/brain/chat and a free fallback tier in the embed proxy (/api/llm/anthropic).
# Get a free key (no credit card) at https://build.nvidia.com → any model → "Get API Key".
# NOTE: this is the hosted-API var. NGC_API_KEY is only for self-hosted NIM Docker
# containers (nvcr.io) — not used here. Same secret value, different name/path.
NVIDIA_API_KEY=
# OpenAI — used for gpt-4o-mini fallback. Get a key at https://platform.openai.com
OPENAI_API_KEY=
# xAI Grok: paid OpenAI-compatible inference (grok-4.5 flagship, grok-4.3
# long-context, grok-4.1-fast budget). Selectable in /api/chat, /api/brain/chat,
# the embed proxy, and a paid rung in the shared LLM chain. Users can also bring
# their own key (Settings > AI Provider Keys). XAI_API_KEY works as an alias.
# Get a key at https://console.x.ai
GROK_API_KEY=
# Alibaba Cloud DashScope (international) — direct Qwen access for the brain
# demo (/api/brain/chat). When unset, Qwen requests fall back to OpenRouter.
# Get a key at https://dashscope-intl.console.aliyun.com
DASHSCOPE_API_KEY=
# ModelScope — Alibaba's model hub. Optional; enables Qwen3-Coder-480B and
# other ModelScope-hosted models via their OpenAI-compatible inference API.
# Get a token at https://modelscope.cn
MODELSCOPE_API_KEY=
# IBM watsonx.ai — adds IBM Granite as a selectable brain in /api/chat (the 3D
# avatar) and /api/brain/chat (the model playground). The key is exchanged for
# a short-lived IAM bearer token and every call is scoped to your project, so
# both WATSONX_API_KEY and WATSONX_PROJECT_ID (or WATSONX_SPACE_ID) are required
# for Granite to appear. Create an API key at https://cloud.ibm.com/iam/apikeys
# and find the project id under watsonx.ai → project → Manage → General.
WATSONX_API_KEY=
WATSONX_PROJECT_ID=
# WATSONX_SPACE_ID= # alternative to WATSONX_PROJECT_ID
# WATSONX_URL=https://us-south.ml.cloud.ibm.com # regional host (eu-de, jp-tok, …)
# WATSONX_MODEL_ID=ibm/granite-3-8b-instruct # default Granite chat model
# WATSONX_API_VERSION=2024-05-31
# IBM watsonx Orchestrate — adds a "watsonx Orchestrate" brain to /api/chat so a
# three.ws 3D avatar becomes the embodied front-end of an enterprise Orchestrate
# agent. The URL is the agent's OpenAI-compatible chat-completions endpoint (the
# Orchestrate instance "Test URL"; "/chat/completions" is appended if omitted).
# The key is the bearer token from the agent's Agent Connect config. Both are
# required for the provider to appear.
WATSONX_ORCHESTRATE_URL=
WATSONX_ORCHESTRATE_API_KEY=
# WATSONX_ORCHESTRATE_AGENT=orchestrate-agent # agent id sent as the model field
# Privy wallet auth (client-side — get app ID from https://dashboard.privy.io)
VITE_PRIVY_APP_ID=
# Privy app ID (server-side — for verifying identity tokens in /api/auth/privy/verify)
PRIVY_APP_ID=
# Privy app secret — from https://dashboard.privy.io → Settings → API keys.
# Used to fetch linked wallets from the Privy user API after login.
# Optional: when unset, wallet sync is skipped (login still works).
PRIVY_APP_SECRET=
# Avaturn (photo-to-avatar pipeline). API key lives server-side; used by
# /api/onboarding/avaturn-session to exchange 3 selfies for a session URL.
# Sign up at https://avaturn.me/developer.
AVATURN_API_KEY=
# Optional override for self-hosted / staging Avaturn deployments.
AVATURN_API_URL=https://api.avaturn.me
# Avaturn default editor — opened by the "Use default avatar" card at /create.
# No session API / no photos required. Point at the hosted editor URL provided
# by Avaturn; the developer ID (if needed) is appended as a query param.
VITE_AVATURN_EDITOR_URL=https://editor.avaturn.me/
VITE_AVATURN_DEVELOPER_ID=
# Avatar regeneration provider (optional, auto-detected when omitted).
# Resolution order when AVATAR_REGEN_PROVIDER is unset:
# 1. REPLICATE_API_TOKEN present → "replicate" (paid, fast, reliable)
# 2. GCP_RECONSTRUCTION_URL set → "gcp" (self-hosted, custom GPU)
# 3. HF_TOKEN present → "huggingface" (free HF Spaces queue)
# 4. otherwise → "none" (501 returned)
# Set this explicitly to force a provider (or "stub" for queue-only testing,
# or "none" to disable selfie reconstruct entirely).
AVATAR_REGEN_PROVIDER=
# Replicate (when AVATAR_REGEN_PROVIDER=replicate). Get an API token at
# https://replicate.com/account/api-tokens. Each mode maps to a model version
# hash from replicate.com — pick a model that takes a GLB URL (or text prompt
# for restyle, or image URL(s) for reconstruct) and returns a GLB asset.
#
# Recommended commercial-OK starting points (2026-05):
# reconstruct → tencent/hunyuan-3d-3.1 (image-to-textured-GLB, ~30-60s, best quality/price)
# OR camenduru/tripo-sr (cheaper fallback, ~$0.0023/run, no PBR textures)
# restyle → tencent/hunyuan-3d-3.1 (same engine, text or image conditioning)
# remesh → mesh-cleanup pipelines on replicate
# retex → image-conditioned texturing models on replicate
# rerig → VAST-AI-Research/UniRig (MIT, SIGGRAPH 2025 SOTA — deploy via cog from HF
# weights until on Replicate, or proxy a dedicated GPU)
#
# Copy the version hash from each model's "Run with API" tab on replicate.com.
REPLICATE_API_TOKEN=
REPLICATE_RECONSTRUCT_MODEL=
REPLICATE_RESTYLE_MODEL=
REPLICATE_REMESH_MODEL=
REPLICATE_RETEX_MODEL=
REPLICATE_RERIG_MODEL=
# /forge text→3D — reference-image model for the flux pass that precedes TRELLIS
# reconstruction. Optional: defaults to black-forest-labs/flux-schnell (Apache-2.0,
# fast). Override with any owner/name[:version] or version hash on Replicate.
REPLICATE_TXT2IMG_MODEL=
# /forge persists every generation (durable GLB + reference image + the human
# keep/discard verdict — the text→3D data flywheel) when BOTH a database
# (DATABASE_URL) and object storage (S3_* above) are configured. With either
# missing, /forge still generates models; it just doesn't retain them or show
# the "Your creations" gallery. No extra config needed beyond those two.
# Hugging Face provider (when AVATAR_REGEN_PROVIDER=huggingface, or auto-
# detected when only HF_TOKEN is set). Read-only token is sufficient since
# the default target Space is public. Get one at huggingface.co/settings/tokens.
#
# Hits the Gradio /call/<api_name> queue endpoint of a public image-to-3D
# Space. Default targets tencent/Hunyuan3D-2's generation_all API, which
# accepts our 3 selfie photos as mv_image_{front,left,right} and returns
# a textured GLB. Override the target by setting the two env vars below.
#
# Reliability note: HF Spaces share free GPU pools (ZeroGPU). Queue waits
# and per-Space quotas can cause 30s-2min latency or outright rejection
# during peak hours. For production traffic, prefer REPLICATE_API_TOKEN.
HF_TOKEN=
HF_RECONSTRUCT_SPACE=tencent/Hunyuan3D-2
HF_RECONSTRUCT_API_NAME=generation_all
# ── Persona Hub (optional) ────────────────────────────────────────────────────
# When set, /api/auth/persona/issue signs tokens with ES256 (asymmetric) so
# tenants can verify offline via the JWK Set at /.well-known/jwks.json. When
# unset, persona tokens are signed with HS256 (shared JWT_SECRET) and tenants
# must hit /api/auth/persona/verify to validate.
#
# Generate a fresh keypair with:
# node scripts/generate-persona-key.mjs
# then paste the output here / into Vercel env.
PERSONA_JWKS_KID=
PERSONA_JWKS_PRIVATE_KEY_PEM=
# Optional — auto-derived from the private key if omitted:
PERSONA_JWKS_PUBLIC_KEY_PEM=
# ── ERC-7710 Delegation Relayer (POST /api/permissions/redeem) ───────────────
# Enable the server-side relayer endpoint (default: false).
PERMISSIONS_RELAYER_ENABLED=false
# Hex private key of the relayer EOA that pays gas for redeemDelegations.
# This is NOT the user's key — only the agent's server-side relayer.
# Generate: node -e "const {Wallet} = require('ethers'); const w = Wallet.createRandom(); console.log(w.privateKey, w.address)"
# NEVER commit a real value. Rotate via Vercel env vars.
AGENT_RELAYER_KEY=0x...
# EIP-55 checksummed address derived from AGENT_RELAYER_KEY — fund with testnet ETH.
AGENT_RELAYER_ADDRESS=0x...
# Per-chain RPC URLs for DelegationManager calls.
# Pattern: RPC_URL_<CHAINID>. Override the default public RPCs with Alchemy/Infura for production.
RPC_URL_1=https://cloudflare-eth.com # Ethereum mainnet (use Alchemy/Infura in prod)
RPC_URL_8453=https://mainnet.base.org # Base mainnet (use Alchemy/Infura in prod)
RPC_URL_84532=https://sepolia.base.org # Base Sepolia (primary dev chain)
RPC_URL_11155111=https://rpc.sepolia.org # Ethereum Sepolia
RPC_URL_421614=https://sepolia-rollup.arbitrum.io/rpc # Arbitrum Sepolia (use Alchemy/Infura in prod)
RPC_URL_11155420=https://sepolia.optimism.io # Optimism Sepolia (use Alchemy/Infura in prod)
# Optional PAID metered reserve per chain (e.g. a Quicknode credit-funded
# endpoint). Pattern: RPC_URL_<CHAINID>_LAST_RESORT. Tried AFTER every free
# public fallback so its monthly quota is only spent when the free chain fails.
# RPC_URL_8453_LAST_RESORT=
# Per-chain DelegationManager contract address overrides (optional).
# Defaults are from MetaMask Delegation Toolkit v1. Override if you deploy a custom instance.
# DELEGATION_MANAGER_ADDRESS_84532=0x...
# DELEGATION_MANAGER_ADDRESS_11155111=0x...
# ── IPFS pinning — used by POST /api/pinning/pin before on-chain registration.
# Pinata (preferred): get JWT from https://app.pinata.cloud/keys
PINATA_JWT=
# Web3.Storage (fallback): get token from https://web3.storage/tokens
# (legacy v1 API — upload endpoint: https://api.web3.storage/upload)
WEB3_STORAGE_TOKEN=
# ── KOL wallet P&L analytics (api/kol/[action].js wallets handler) ─────────
# Birdeye API key for Solana wallet portfolio + P&L data.
# Get one at https://birdeye.so — free tier allows ~100 req/min.
BIRDEYE_API_KEY=
# ── Pump.fun live feed integration (api/_lib/pumpfun-mcp.js) ──────────────
# Upstream pumpfun-claims-bot MCP server URL. When set, /api/agents/pumpfun
# and /api/agents/pumpfun-feed are enabled, the cron at /api/cron/pumpfun-signals
# starts crawling, and the pumpfun-watch agent skills become functional.
# Run `npx pumpfun-claims-bot` (or deploy via Railway) to host the upstream MCP.
PUMPFUN_BOT_URL=
# Optional bearer token if the upstream MCP requires auth.
PUMPFUN_BOT_TOKEN=
# ── aixbt intelligence bridge (api/_lib/aixbt.js, api/aixbt/*) ───────────────
# Connects three.ws agents to aixbt's live narrative + momentum intelligence.
# When set, /api/aixbt/{intel,projects,grounding,chat} go live, the aixbt agent
# skills (aixbt-intel, aixbt-scan) work, and the paid MCP tools (aixbt_intel,
# aixbt_projects) return real data. Without it those endpoints return a designed
# "not configured" 503 — never fake data.
# Get a key with a full aixbt.tech subscription or by paying for a time-boxed
# x402 key pass: POST https://api.aixbt.tech/x402/v2/api-keys/{1d|1w|4w} (USDC on Base).
AIXBT_API_KEY=
# Optional override for the aixbt REST base (defaults to the v2 API).
AIXBT_API_BASE=https://api.aixbt.tech/v2
# ── ERC-8004 agent registry crawler (api/cron/erc8004-crawl) ─────────────────
# Blocks to scan on first run (no cursor). Default 2000 keeps cron under 300s.
# For a one-time backfill, set to 50000 and trigger the endpoint manually —
# do NOT leave this value set for normal cron operation.
ERC8004_CRAWL_LOOKBACK=
# ── Rider VR gate — token-gated access via $THREE payments ───────────────────
# Solana wallet that receives 8,000 $THREE to unlock the VR experience.
RIDER_VAULT_ADDRESS=wwwDoZ4C3qcqeBWCugzAGAikPyEPSfmRJ3Lyi9dWE3T
# Shared secret sent by Helius in the 'authorization' header on webhook pushes.
# Generate: openssl rand -base64 32
RIDER_HELIUS_WEBHOOK_SECRET=
# ── Helius — NFT portfolio, enhanced transactions, wallet monitoring ──────────
# Used by: /api/agents/nfts (nft-portfolio + wallet-activity skills),
# /api/nft/resolve, /api/wallet/balances, /api/tx/explain,
# and pump.fun wallet webhooks.
# Get a free key at https://dev.helius.xyz (free tier: 100k req/mo DAS + RPC).
HELIUS_API_KEY=
# Shared secret echoed by Helius in the 'authorization' header on webhook pushes.
# Generate: openssl rand -base64 32
HELIUS_WEBHOOK_AUTH=
# ── Solana RPC overrides (used by Solana attestation crawler + pump skills) ──
# Defaults to public mainnet/devnet — set Helius/Triton/QuickNode for prod.
SOLANA_RPC_URL=
SOLANA_RPC_URL_DEVNET=
# ── Solana Developer Platform (SDP) — custodial wallets, SPL issuance, ───────
# payments, and compliance screening via the Solana Foundation's enterprise API
# (https://platform.solana.com). Used by the same-origin proxy at /api/sdp/* and
# the helpers in api/_lib/sdp.js.
# When set, /api/sdp/v1/{wallets,issuance,payments,compliance,...} go live under
# our origin (the key stays server-side, never reaches the browser). Without it,
# authenticated routes return a designed "not configured" 503 — never fake data;
# the unauthenticated /api/sdp/health probe still works.
# Get a project-scoped key from https://platform.solana.com (sandbox keys hit
# the same wire surface — swap the key to go to production).
SDP_API_KEY=
# Optional override for the SDP API host. Defaults to the production API; point
# at the sandbox or a self-hosted Worker deployment if needed.
SDP_API_BASE=https://api.solana.com
# ── x402 micropayments (POST /api/mcp) ─────────────────────────────────────
# Per-network payTo wallets that receive USDC for paid MCP calls.
X402_PAY_TO_SOLANA=wwwwwDxFWRn7grgr3Esrsg5C6NvDoDHSA4gaCffccrU
X402_PAY_TO_BASE=0x4022de2d36c334e73c7a108805cea11c0564f402
# ── BNB Chain gasless sends (MegaFuel) ─────────────────────────────────────
# Optional NodeReal MegaFuel API key. NOT used by api/_lib/bnb/megafuel.js at
# send time — pm_isSponsorable / eth_sendRawTransaction run unauthenticated
# against the public bsc-megafuel(-testnet).nodereal.io endpoints; sponsorship
# is resolved server-side purely by matching the tx's `from` address against a
# pre-provisioned sponsor policy. This key is only needed for the separate
# ONE-TIME policy-management step (pm_createPolicy/pm_updatePolicy against
# open-platform-ap.nodereal.io/{key}/megafuel(-testnet), not yet implemented
# here) that whitelists a sender/contract. Until a policy exists for your
# sender, every send transparently self-pays — the feature works end-to-end
# without this key. Provision at https://dashboard.nodereal.io (MegaFuel
# product). Never commit a real key.
NODEREAL_MEGAFUEL_KEY=
# Funded BSC testnet (chainId 97) deployer/sender key for live gasless + contract
# proofs. Generate a throwaway: node -e "console.log(require('viem/accounts').generatePrivateKey())"
# then fund the address from https://www.bnbchain.org/en/testnet-faucet. Never commit a real key.
BNB_TESTNET_DEPLOYER_KEY=
# Deployed WorldMoves.sol (contracts/src/WorldMoves.sol) contract address per
# network, read by api/_lib/bnb/world-moves.js's worldMovesAddress(). BSC
# mainnet is unused this campaign; BSC testnet deploy is currently blocked on
# BNB_TESTNET_DEPLOYER_KEY above (see contracts/DEPLOYMENTS.md's WorldMoves
# section) — set this the moment `forge script script/DeployWorldMoves.s.sol
# --broadcast` produces a real public-testnet address. Until set, any call
# that needs the address (buildMoveTx/sendMove without an explicit
# opts.address) throws a clear "not deployed yet" error rather than guessing.
WORLD_MOVES_ADDRESS_MAINNET=
WORLD_MOVES_ADDRESS_TESTNET=
# Greenfield vault platform account (prompt 09/11 — api/_lib/bnb/greenfield-write.js,
# api/bnb/vault-upload.js). Creates buckets/objects on Greenfield on behalf of
# sellers; same secp256k1 curve as a BSC EOA, so it falls back to
# BNB_TESTNET_DEPLOYER_KEY above when unset — one funded throwaway key can drive
# both legs. Generate + fund the same way as BNB_TESTNET_DEPLOYER_KEY. Never
# commit a real key.
GREENFIELD_VAULT_OPERATOR_KEY=
# Greenfield bucket that hosts every vault object (default matches
# specs/vault-manifest.md's example). Must be a globally unique, DNS-safe
# Greenfield bucket name (lowercase letters/digits/hyphens, 3-63 chars).
GREENFIELD_VAULT_BUCKET_TESTNET=three-ws-vault-testnet
GREENFIELD_VAULT_BUCKET_MAINNET=three-ws-vault-mainnet
# Deployed GreenfieldVault.sol (contracts/src/GreenfieldVault.sol) address per
# network, read raw by api/bnb/vault-upload.js's vaultContractAddress(). Deploy
# is blocked on BNB_TESTNET_DEPLOYER_KEY above (see contracts/DEPLOYMENTS.md's
# GreenfieldVault section) — until set, vault-upload records the manifest spec's
# own placeholder address and returns contractDeployed:false, never a fabricated
# real-looking address.
GREENFIELD_VAULT_ADDRESS_MAINNET=
GREENFIELD_VAULT_ADDRESS_TESTNET=
# Dedicated EIP-712 signing key for signed offers and payment receipts (USE-17).
# This is a dedicated signing key, NOT a wallet that holds funds. Generate with:
# node -e "console.log(require('viem/accounts').generatePrivateKey())"
# Never reuse an X402_PAY_TO_* address here — the issuer module rejects it on boot.
# Rotate by changing the value and bumping `#key-1` -> `#key-2` in code if needed.
# When unset, signed offers/receipts are silently disabled (rollback without redeploy).
X402_RECEIPT_SIGNING_KEY=0xREPLACE_WITH_DEDICATED_SIGNING_KEY_HEX_64_CHARS
# Prepaid credits (POST /api/credits/deposit): the Solana wallet users send SOL or
# $THREE to when topping up their balance. Defaults to X402_PAY_TO_SOLANA, then the
# $THREE treasury, if unset — set it to route deposits to a dedicated wallet.
CREDITS_DEPOSIT_WALLET_SOLANA=
# USDC asset addresses (mainnet).
X402_ASSET_MINT_SOLANA=EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v
X402_ASSET_ADDRESS_BASE=0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913
# Price in base units (USDC = 6 decimals; 1000 = 0.001 USDC).
X402_MAX_AMOUNT_REQUIRED=1000
# USE-15 — TTL (seconds) for the payment-identifier idempotency cache.
# Same paymentId within the window returns the cached response without
# re-charging. Per-route override via paidEndpoint({ paymentIdentifier:
# { ttlSeconds } }). 3600s = "long TTL" per x402 docs.
X402_IDEMPOTENCY_TTL_SECONDS=3600
# Per-network facilitators. PayAI supports both Solana and Base mainnet;
# x402.org's reference facilitator only supports base-sepolia, so do not point
# X402_FACILITATOR_URL_BASE at it for production.
#
# SOLANA ROUTING (the real defaults — see api/_lib/x402/ring-config.js):
# 1. If X402_FACILITATOR_URL_SOLANA is set explicitly, it ALWAYS wins.
# 2. Else, if X402_SELF_FACILITATOR_ENABLED=true, Solana settlement DEFAULTS to
# this deploy's own self-hosted facilitator ($APP_ORIGIN/api/x402-facilitator).
# 3. Else it falls back to the external PayAI facilitator below.
# So the self-hosted facilitator is NOT "always-on": it is the default ONLY when
# X402_SELF_FACILITATOR_ENABLED=true AND no explicit URL overrides it. Leave the
# line below commented for stock (external) routing; uncomment + point it at your
# self facilitator to force in-house settlement regardless of the flag.
# X402_FACILITATOR_URL_SOLANA=https://three.ws/api/x402-facilitator
X402_FACILITATOR_URL_BASE=https://facilitator.payai.network
# Advertise the Base (eip155:8453) payment rail WITHOUT CDP credentials. OFF by
# default: setting X402_FACILITATOR_URL_BASE alone is NOT treated as proof the
# facilitator settles — a stale/decommissioned host answers /verify with 404 and
# every Base-preferring buyer would pay, fail verification, and get a 502. Set to
# true ONLY after confirming your non-CDP Base facilitator actually verifies+settles.
# With CDP creds set (CDP_API_KEY_ID + CDP_API_KEY_SECRET), Base routes to Coinbase
# and this flag is unnecessary.
X402_ADVERTISE_BASE=false
# Optional bearer tokens for self-hosted/private facilitators.
X402_FACILITATOR_TOKEN_SOLANA=
X402_FACILITATOR_TOKEN_BASE=
# Solana fee payer advertised to clients (must match facilitator's /supported).
X402_FEE_PAYER_SOLANA=2wKupLR9q6wXYppw8Gr2NvWxKBUqm4PPJKkQfoxHDBg4
# ── Closed-loop x402 ring (self-hosted facilitator) ────────────────────────
# The self-cycled agent economy: platform-controlled wallets pay the platform's
# OWN x402 endpoints in real USDC, settled by the platform's OWN facilitator, so
# no third party ever touches a ring settlement. All OFF by default — the
# facilitator answers 503 and nothing settles until you turn it on. Full guide:
# docs/x402-ring-economy.md. Generate the wallets with:
# node scripts/x402-ring-setup.mjs
#
# Turn the facilitator ON. BOTH of these are required for in-house settlement:
# - X402_SELF_FACILITATOR_ENABLED=true (else /api/x402-facilitator → 503)
# - X402_FACILITATOR_URL_SOLANA pointed at your self facilitator, OR left unset
# so the flag's default (this deploy's /api/x402-facilitator) applies.
# Setting the flag WITHOUT correcting an external X402_FACILITATOR_URL_SOLANA is
# the mis-envelope /api/x402-ring and /api/x402-status report as a config_warning.
X402_SELF_FACILITATOR_ENABLED=false
# Extra treasury/payTo addresses the self facilitator will settle to, beyond
# X402_PAY_TO_SOLANA (comma-separated). Only platform-controlled wallets belong
# here — this allowlist is what keeps every settled dollar inside the platform.
X402_SELF_FACILITATOR_PAYTO_ALLOWLIST=
# Sponsor (fee-payer) secret the facilitator co-signs with in SPONSOR mode. Its
# pubkey MUST equal X402_FEE_PAYER_SOLANA. Not needed in self-pay mode (below).
X402_FEE_PAYER_SECRET_BASE58=
# Sponsor SOL floor (lamports). Below this the facilitator refuses to settle,
# pausing the loop before it can drain your SOL. Default 0.02 SOL.
X402_SPONSOR_SOL_FLOOR_LAMPORTS=20000000
# Ring payer USDC float floor (atomic, 6dp). Below this the wallet-balance monitor
# raises an ops alert to top up the payer's recirculating USDC. Default $5.
X402_RING_PAYER_USDC_FLOOR_ATOMIC=5000000
# Self-pay mode (RECOMMENDED, lowest fee): the payer pays its own 1-signature fee
# (5000 lamports) and no sponsor key is needed. Off = sponsor mode (2 signatures,
# ~2× the base fee). When off, /api/x402-ring flags ring_self_pay_off.
X402_RING_SELF_PAY=true
# Ring payer wallet secret — the wallet that pays the ring (USDC float that
# recirculates). Fund with USDC + a little SOL for its own fees in self-pay mode.
X402_SEED_SOLANA_SECRET_BASE58=
# Treasury secret — used by the rebalancer to sweep treasury→payer so the float
# never drains one-way. Pubkey is X402_PAY_TO_SOLANA. Missing → warn.
X402_TREASURY_SECRET_BASE58=
# Per-call ring settlement size (USDC atomics, 6dp). Bigger = fewer txs = less
# SOL burned for the same gross volume. $1.00 default; raise to $10–$100 for
# near-zero SOL. MUST be ≤ X402_VOLUME_PER_RUN_CAP_ATOMIC (5-min volume loop) AND
# ≤ X402_RING_TICK_CAP_ATOMIC (per-minute ring tick) or ring-settle is skipped
# every tick (flagged ring_price_exceeds_run_cap; payX402 also logs it loudly).
# The two cap defaults below are set to fit this price out of the box.
X402_PRICE_RING_SETTLE=1000000
# Per-tick spend ceiling for the 5-min VOLUME LOOP (USDC atomics). One tick can't
# exceed this on top of the daily cap. Default $1.10 — RAISED from the old $0.05
# so it accommodates the $1.00 ring-settle it rotates (at $0.05 ring-settle was
# silently skipped every cycle). Keep it ≥ X402_PRICE_RING_SETTLE.
X402_VOLUME_PER_RUN_CAP_ATOMIC=1100000
# Daily spend cap for the autonomous loop (USDC atomics) — your daily volume
# target. Leave unset for the loop's built-in default. SEPARATE from the ring
# tick's own daily cap (X402_RING_DAILY_CAP_ATOMIC below) — the two never mix.
X402_AUTONOMOUS_DAILY_CAP_ATOMIC=
# ── Per-minute ring tick (api/cron/x402-ring-tick.js) ──────────────────────────
# A dedicated * * * * * cron that produces steady, capped, fee-minimal paid
# traffic: cheap tips/services every minute plus a periodic ring-settle carrier.
# ON by default once the ring envelope above is valid (validateRingConfig clean);
# X402_RING_TICK_ENABLED=false or the global X402_AUTONOMOUS_ENABLED=false pause it.
X402_RING_TICK_ENABLED=true
# Paid calls attempted per minute (cheap tips dominate the count). Default 3.
X402_RING_TICK_CALLS=3
# Fire one ring-settle every Nth tick (the fee-optimal large-volume carrier;
# 0 disables it). Default 5 → a settle roughly every 5 minutes.
X402_RING_SETTLE_EVERY_N_TICKS=5
# Per-tick spend ceiling (USDC atomics). MUST fit a settle tick: ring-settle
# price + the cheap co-riders. Default $1.10 (fits $1.00 ring-settle + 2 tips).
X402_RING_TICK_CAP_ATOMIC=1100000
# Ring tick's OWN daily ceiling (USDC atomics), summed from x402_autonomous_log
# rows tagged pipeline='ring-tick'. Does NOT consume the autonomous loop's cap.
# Default $50. At 3 calls/min the tick reaches this bound mid-day then no-ops
# cleanly until UTC midnight — raise it for full-day coverage.
X402_RING_DAILY_CAP_ATOMIC=50000000
# Minimum treasury→payer sweep the rebalancer bothers with (USDC atomics).
# Default $0.10. Lower it so the (120s) rebalancer tops the payer up sooner as
# the per-minute tick cycles the float in small increments.
X402_RING_MIN_SWEEP_ATOMIC=100000
# Revenue share of every treasury sweep routed to the economy master wallet, in
# basis points (default 2000 = 20%; 0 disables the leg). This is the ONE inflow
# that keeps the funding root's USDC stocked so economy-fuel can convert it to
# SOL and treasury-topup can hold every engine (circulation treasury, ring
# sponsor) above its floor. Ledgered as kind='revshare' in x402_ring_ledger and
# verified per-leg by the ring reconciler.
X402_RING_MASTER_REVSHARE_BPS=2000
# Hard per-payment network-fee ceiling (lamports). payX402 refuses to broadcast
# a ring payment whose fee configuration could exceed it. Default 10000 —
# double the 1-signature self-pay floor, so both modes fit with headroom.
X402_RING_MAX_FEE_PER_TX_LAMPORTS=10000
# Daily SOL fee budget for the whole ring (lamports). The nightly fee audit
# pages ops when the day's real, chain-read burn crosses it. Default 0.05 SOL.
X402_RING_DAILY_FEE_BUDGET_LAMPORTS=50000000
# Only OUR endpoints get paid — disables the external x402 section of the loop.
X402_EXTERNAL_ENABLED=false
# No charity split leaves the ring (basis points; 0 = nothing leaves).
X402_CHARITY_AUDIT_BPS=0
# ERC-8021 Schema 2 builder-code app identifier (USE-20). When set, every 402
# challenge advertises the `builder-code` extension, every paid request must
# echo it (anti-tamper), and settlement-tx calldata is suffixed with a CBOR
# map for on-chain attribution. Pattern: ^[a-z0-9_]{1,32}$. Leave unset to
# disable. Coinbase builder rewards / x402scan analytics use this to credit
# the app that exposed the paid endpoint.
X402_BUILDER_CODE_APP=three_d_agent
# Wallet builder code our own buyer client attributes itself with when
# making outbound paid calls (USE-20). Distinct from X402_BUILDER_CODE_APP:
# this populates `w` on PaymentPayload.extensions["builder-code"] so the
# settlement CBOR records both who exposed the endpoint AND who paid for it.
X402_BUILDER_CODE_WALLET=3d_agent
# Spending caps for buyer clients (USE-22). All values are micro-USD
# (6-decimal atomics of USDC); leave any line blank to skip that cap.
# Example: 1_000_000 = $1.00; 100_000 = $0.10; 1_000 = $0.001.
# Per-call ceiling enforced before signing; hourly/daily are sliding
# windows tracked in Redis (server) or localStorage (browser).
X402_MAX_PER_CALL_ATOMIC=1000000
X402_MAX_PER_HOUR_ATOMIC=10000000
X402_MAX_PER_DAY_ATOMIC=100000000
# ── x402 access-control bypass (USE-23) ────────────────────────────────────
# Shared secret for internal service-to-service calls. Any Vercel function
# (cron job, webhook handler, the agent runtime) that hits one of our own
# paid /api/x402/* endpoints sends `X-API-Key: $INTERNAL_API_KEY` to skip
# the 402 challenge — we don't pay ourselves to call our own infra.
# Generate: openssl rand -base64 32
# Compared with constant-time equality in api/_lib/x402/access-control.js.
# Leave unset to disable internal bypass (subscription keys and OAuth bypass
# still work). Partner / subscription keys are issued at
# POST /api/x402/admin/subscriptions and stored hashed in x402_subscriptions.
INTERNAL_API_KEY=
# ── play / holder gate HMAC secret (REQUIRED IN PRODUCTION) ────────────────
# The single HMAC key that mints and verifies every short-lived entry credential:
# /play sign-in nonces + passes (api/_lib/play-pass.js), holder-world passes
# (api/_lib/holder-pass.js), guest/presence tokens, $THREE quote signing, clash,
# and world-service auth. The standalone Colyseus server (multiplayer/) verifies
# the same tokens, so it MUST be set to the IDENTICAL value there.
# Fail-closed by design: when unset with NODE_ENV=production, these modules refuse
# to sign/verify with the public dev secret and throw — surfacing as a 500 on
# GET /api/play/nonce (the first endpoint /play hits) and on the holder gate.
# Leave unset only in local dev / CI (an insecure dev secret is used, with a warning).
# Generate: openssl rand -base64 48
HOLDER_PASS_SECRET=
# ── multiplayer ↔ API shared secret (presence + world persistence) ─────────
# One HMAC secret shared between this API and the standalone Colyseus server
# (multiplayer/). The API mints presence tickets the realm rooms verify, and the
# server signs world-persistence writes (api/world/[action]) the API verifies.
# Falls back to HOLDER_PASS_SECRET so a single secret can configure every gate.
# Generate: openssl rand -base64 32
MULTIPLAYER_SHARED_SECRET=
# Base URL the multiplayer server uses to reach the persistence API for durable
# per-world saves. Defaults to https://three.ws; set to http://localhost:3000 in
# local dev so room builds persist against your dev API. (multiplayer-side env.)
WORLD_API_BASE=
# ── x402 Offer & Receipt extension (USE-17) ────────────────────────────────
# Signs every 402 offer and every settled-payment receipt so buyers get
# cryptographic proof of the interaction. Verifiers resolve the signer key via
# /.well-known/did.json (rewritten to /api/x402/did). Buyers can re-query their
# own receipts later via /api/x402/my-receipts.
#
# Key separation is CRITICAL — this MUST be a dedicated EOA/JWK, never one of
# the X402_PAY_TO_* wallets. Receipts are server claims about transactions;
# co-mingling with the funds-receiving wallet would imply the payment cosigner
# endorses every receipt ever issued. The issuer module fails loudly on boot
# if the derived address collides with any X402_PAY_TO_* key.
#
# EIP-712 mode (default): set OFFER_RECEIPT_SIGNING_PRIVATE_KEY to a fresh
# 0x-hex EVM private key. Generate with:
# node -e "console.log(require('viem/accounts').generatePrivateKey())"
# JWS mode (did:web): set OFFER_RECEIPT_FORMAT=jws + OFFER_RECEIPT_JWK to a
# JSON-encoded JWK private key. Vercel has no KMS; for production-grade key
# custody, swap the issuer for a Google KMS / AWS KMS / HashiCorp Vault signer.
# When all signing keys are unset, the extension is silently disabled and the
# 402/200 wire format stays valid (extension is purely additive).
OFFER_RECEIPT_SIGNING_PRIVATE_KEY=
OFFER_RECEIPT_FORMAT=eip712
OFFER_RECEIPT_JWK=
OFFER_RECEIPT_JWS_ALG=EdDSA
# Bare hostname used to build did:web identifiers (no scheme, no path).
# Defaults to the host parsed from PUBLIC_APP_ORIGIN.
SERVER_DOMAIN=
# Coinbase Developer Platform (CDP) facilitator. REQUIRED for agentic.market /
# CDP Bazaar listing — only endpoints whose first verify+settle is processed
# by CDP get cataloged. PayAI is a fine fallback for non-Bazaar use.
# When both keys are set, Base mainnet payments route to CDP automatically.
# Sign up at https://cdp.coinbase.com → Project → API keys → Generate.
X402_CDP_FACILITATOR_URL=https://api.cdp.coinbase.com/platform/v2/x402/facilitator
CDP_API_KEY_ID=
# Auto-detected. Either:
# * Newer Ed25519: base64-encoded 32-byte seed or 64-byte (seed||pub).
# * Legacy ECDSA P-256: PEM-encoded EC private key (paste the full
# "-----BEGIN EC PRIVATE KEY----- ... -----END EC PRIVATE KEY-----"
# value in Vercel; newlines are preserved).
CDP_API_KEY_SECRET=
# zauthx402 — telemetry + monitoring for x402 endpoints. Optional; when unset
# the SDK is not invoked. Get a key at https://zauth.inc/provider-hub.
ZAUTH_API_KEY=
# Set to "1" to enable verbose [zauthSDK:*] logs in Vercel.
ZAUTH_DEBUG=
# Set to "1" to include request/response bodies in zauth telemetry. OFF by
# default — these are payment + MCP endpoints, so bodies (payment payloads,
# tool args) are withheld from the third-party backend. Endpoint health is
# reported regardless. Enable only for short-lived debugging.
ZAUTH_INCLUDE_BODIES=
# Auto-refund hot wallets. When at least one is set, the SDK automatically
# refunds callers who pay and receive a 5xx or empty response. Use DEDICATED
# keypairs funded only with the refund float — never the main treasury keys.
#
# EVM (Base) refund wallet — hex private key (0x…). Generate:
# node -e "console.log(require('viem/accounts').generatePrivateKey())"
ZAUTH_REFUND_PRIVATE_KEY=
# Solana refund wallet — base58 secret key. Generate:
# node -e "const {Keypair}=require('@solana/web3.js');console.log(require('bs58').default.encode(Keypair.generate().secretKey))"
ZAUTH_SOLANA_PRIVATE_KEY=
# Per-refund ceiling in USD. Defaults to 0.10 (above our highest tool price $0.05).
ZAUTH_REFUND_MAX_USD=
# Daily / monthly spend caps across all auto-refunds. Defaults: $25 / $250.
ZAUTH_REFUND_DAILY_CAP_USD=
ZAUTH_REFUND_MONTHLY_CAP_USD=
# ── Resend (transactional email) ─────────────────────────────────────────────
# Powers: account welcome, email verification codes, password reset, subscription
# confirmations, dunning, and newsletter contact capture (/api/newsletter-subscribe).
# Sign up at https://resend.com → API Keys → Create.
RESEND_API_KEY=
# Sender identity. Domain MUST be verified in Resend (SPF + DKIM DNS records)
# before sends will succeed. Use Resend's onboarding@resend.dev while verifying.
EMAIL_FROM=three.ws <notifications@three.ws>
# Reply-To for transactional mail (defaults to support@three.ws so replies reach a human).
EMAIL_REPLY_TO=support@three.ws
# Audience UUID for /api/newsletter-subscribe. Without it, the endpoint silently
# no-ops. Create in Resend → Audiences, then paste the UUID here.
RESEND_AUDIENCE_ID=
# ── GitHub OAuth (social memory seeding) ─────────────────────────────────────
# Create an OAuth App at https://github.com/settings/developers.
# Callback URL: https://your-domain.com/api/auth/github/callback
GITHUB_OAUTH_CLIENT_ID=
GITHUB_OAUTH_CLIENT_SECRET=
# ── X (Twitter) OAuth 2.0 PKCE (social memory seeding) ───────────────────────
# Create an app at https://developer.twitter.com with Read + OAuth 2.0 enabled.
# Callback URL: https://your-domain.com/api/auth/x/callback
X_OAUTH_CLIENT_ID=
X_OAUTH_CLIENT_SECRET=
# ── Enterprise SAML 2.0 SSO (three.ws is the Service Provider) ───────────────
# Lets platform users sign in through your IdP (IBM Cloud App ID, Okta, Azure
# AD, Google Workspace, …). When configured, an "SSO" button appears on /login.
#
# Setup:
# 1. Register three.ws as a SAML app in your IdP using our SP details:
# Entity ID / Audience : https://your-domain.com/api/auth/saml/metadata
# ACS (Reply) URL : https://your-domain.com/api/auth/saml/acs
# Or just hand the IdP our metadata document (it has everything):
# https://your-domain.com/api/auth/saml/metadata
# 2. Point us at the IdP — EITHER paste its metadata URL (simplest)…
SAML_IDP_METADATA_URL=
# …OR set the fields explicitly (these win if both are present):
SAML_IDP_SSO_URL=
# IdP signing certificate — PEM block or bare base64. Literal "\n" escapes are
# fine (they're restored to newlines). Required for verifying assertions.
SAML_IDP_CERT=
# Optional: pins the expected assertion Issuer and enables IdP-initiated logout.
SAML_IDP_ENTITY_ID=
SAML_IDP_SLO_URL=
#
# IBM Cloud note: the IAM "Identity providers" screen makes IBM Cloud the SP and
# expects YOUR IdP — it is not itself a SAML IdP for third-party apps. To use IBM
# as the IdP here, create the SAML app in IBM Cloud App ID (or IBM Security
# Verify) and use ITS metadata URL / SSO URL / signing cert above.
#
# SP entity ID override (defaults to the metadata URL above). Set only if your
# IdP requires a specific Audience value.
SAML_SP_ENTITY_ID=
# Optional SP keypair (PEM) — when set, our AuthnRequests are signed and
# encrypted assertions can be decrypted. Most IdPs don't require this.
SAML_SP_PRIVATE_KEY=
SAML_SP_CERT=
# Security posture. The assertion must be signed by default; the response
# envelope signature is opt-in (many IdPs only sign the assertion).
SAML_WANT_ASSERTIONS_SIGNED=true
SAML_WANT_RESPONSE_SIGNED=false
SAML_SIGNATURE_ALGORITHM=sha256
# NameID format to request: "unspecified" (default — let the IdP decide) or a
# full URN, e.g. urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress.
SAML_IDENTIFIER_FORMAT=unspecified
# Accepted clock skew (ms) between us and the IdP. Default 5s.
SAML_CLOCK_SKEW_MS=5000
# SP-initiated only by default (replay-resistant). Set true to also accept
# unsolicited IdP-initiated responses (no InResponseTo).
SAML_ALLOW_IDP_INITIATED=false
# Label on the /login SSO button.
SAML_BUTTON_LABEL=Single sign-on (SSO)
# ── News-feed syndication (admin "Publish" → Dev.to / Medium) ────────────────
# Used by /api/admin/news (the local-only CMS) to mirror newly-saved posts
# to external publishers. When unset, syndication for that target is silently
# skipped — the admin UI surfaces "skipped: <KEY> not set".
# See docs/syndication.md for setup details.
# Dev.to: generate at https://dev.to/settings/extensions ("Generate API Key")
DEV_TO_API_KEY=
# Medium: generate an integration token at https://medium.com/me/settings/security
# (Medium's API is deprecated for new accounts as of 2024 but still works for
# accounts that had API access enabled.) MEDIUM_AUTHOR_ID is optional —
# the syndicator auto-discovers it via /v1/me and caches in-memory.
MEDIUM_INTEGRATION_TOKEN=
MEDIUM_AUTHOR_ID=
# ── x402 MCP bridge (USE-11) ────────────────────────────────────────────────
# The mcp-bridge/ workspace is a Claude Desktop / Cursor MCP server that
# auto-pays for x402-paid HTTP endpoints. It registers a single
# `call_paid_endpoint` fallback tool plus one tool per resource discovered
# from the Coinbase x402 Bazaar. Channel state (batch-settlement vouchers)
# persists in ~/.x402-mcp-bridge/channels/client/ so Claude Desktop restarts
# don't re-deposit on the next call.
#
# Buyer keys — one of EVM or SVM is required for the bridge to start.
# These keys SIGN PAYMENTS. Never share them, never commit them, and never
# point them at a wallet you don't control.
MCP_BRIDGE_EVM_PRIVATE_KEY=
MCP_BRIDGE_SVM_PRIVATE_KEY=
# Per-call spending cap in atomic units of the accept's asset. Default 100000
# = $0.10 USDC (6 decimals). Payments above this are aborted with a clear
# error before any signature is created.
MCP_BRIDGE_MAX_PRICE_PER_CALL_ATOMIC=100000
# Max number of bazaar tools to register at startup. Set 0 to disable
# bazaar-driven dynamic tools (the fallback call_paid_endpoint still works).
MCP_BRIDGE_DISCOVER_LIMIT=20
# Override the bazaar discovery endpoint (default = CDP mainnet bazaar).
MCP_BRIDGE_BAZAAR_URL=https://api.cdp.coinbase.com/platform/v2/x402/discovery/resources
# Batch-settlement tuning. The bridge pre-funds N request-amounts on the first
# call so subsequent voucher-only calls don't pay gas. MAX_DEPOSIT_ATOMIC is a
# hard ceiling on any single deposit (default $5).
MCP_BRIDGE_BATCH_DEPOSIT_MULTIPLIER=5
MCP_BRIDGE_MAX_DEPOSIT_ATOMIC=5000000
# Override where batch-settlement channel state is persisted. Defaults to
# ~/.x402-mcp-bridge/channels. Restart-safe.
X402_MCP_BRIDGE_CHANNELS_DIR=
# ── USE-10: @three-ws/mcp-server (paid MCP tools for Claude Desktop / Cursor) ─
# Bearer token for the hosted three.ws MCP endpoint (.mcp.json "3d-agent" remote).
# Get one from your three.ws dashboard. Never commit the literal value.
THREE_WS_MCP_TOKEN=
# Wallets that RECEIVE USDC payments when an MCP client pays for a tool call.
# Default to the existing X402_PAY_TO_* wallets when unset.
MCP_EVM_PAYMENT_ADDRESS=
# If unset, the stdio @three-ws/mcp-server still boots and routes paid-call USDC to the
# platform X402_PAY_TO_SOLANA payout; set a 32-44 char base58 wallet to collect it yourself.
MCP_SVM_PAYMENT_ADDRESS=
# Connector-review entitlement for the stdio @three-ws/mcp-server. Set MCP_REVIEW_SECRET on the
# server and hand a reviewer the SAME value as MCP_REVIEW_MODE in their client env; paid tools
# then run for real with NO charge. Off (no bypass) unless MCP_REVIEW_SECRET is set.
MCP_REVIEW_SECRET=
MCP_REVIEW_MODE=
# Max forge_free generations to prefer a durable result when the free NVIDIA lane degrades (1-4).
FORGE_FREE_ATTEMPTS=2
# Override the previewUrl returned by get_pose_seed.
MCP_POSE_PREVIEW_BASE=https://three.ws/pose
# Per-chain RPC override for agent_reputation reads. Example:
# MCP_AGENT_REP_RPC_8453=https://mainnet.base.org
# MCP_AGENT_REP_RPC_42161=https://arb1.arbitrum.io/rpc
# Block window scanned for recent ReputationSubmitted/Staked events (default
# 200000). Lower for high-RPS chains to keep RPC calls under provider limits.
MCP_AGENT_REP_LOG_WINDOW=200000
# ── @three-ws/omniology-mcp (Omniology contest MCP server) ────────────────────
# Stateless MCP server in packages/omniology-mcp: read tools wrap Omniology's
# PUBLIC contest feed; submit_entry settles USDC on Solana (x402) then forwards
# an authenticated POST to Omniology. Needs NO Redis/QStash — it proxies an
# external API. The stdio entry point fails fast at startup if the base URL is
# unset (config.js assertBaseUrl), so this is REQUIRED to run the server.
#
# Base URL of the Omniology contest API (serves /v1/contests/live and
# /v1/contests/{id}/entries). No default — point it at the live deployment.
OMNIOLOGY_BASE_URL=
# Optional bearer token sent to Omniology ONLY on the authenticated forward of a
# paid submit_entry. Never exposed to the MCP client. Leave unset for read-only.
OMNIOLOGY_API_KEY=
# Per-request timeout (ms) for calls to Omniology. Default 20000.
OMNIOLOGY_TIMEOUT_MS=20000
# Price charged for submit_entry (USDC), parsed as a "$X.XX" string. Default $0.05.
OMNIOLOGY_SUBMIT_PRICE_USD=$0.05
# x402 facilitator the submit_entry payment settles through. Both default to
# the payai network facilitator when unset; the token is only needed for
# facilitators that require auth.
X402_FACILITATOR_URL=https://facilitator.payai.network
X402_FACILITATOR_TOKEN=
# ── SNS subdomain minting on threews.sol ─────────────────────────────────────
# Base58-encoded 64-byte ed25519 secret for the wallet that owns the platform
# parent .sol domain (default `threews.sol`). The /api/sns-subdomain and
# /api/threews/subdomain endpoints use this key to sign createSubdomain +
# URL-record + transferSubdomain in a single atomic transaction, then hand the
# subdomain to the user. Leave unset to disable subdomain minting (both
# endpoints return 503 instead of attempting the on-chain write).
THREEWS_SOL_PARENT_SECRET_BASE58=
# Override the parent domain if the platform acquires a different root.
# Default: threews.sol.
THREEWS_SOL_PARENT_DOMAIN=threews.sol
# Origin used when writing the URL record on a freshly-minted subdomain.
# Brave's SNS resolver follows this URL when a user types `<label>.threews.sol`.
# Default: https://three.ws.
STOREFRONT_ORIGIN=https://three.ws
# ── Session + CSRF (server) ──────────────────────────────────────────────────
# Salt used to derive purchase-receipt keys and other internal HMACs. Falls
# back to the literal string "dev" if unset — that fallback is FINE in dev,
# CATASTROPHIC in prod (every install would share the same receipt namespace).
# Generate: openssl rand -base64 64
SESSION_SECRET=
# Escape hatch for CSRF enforcement on mutating endpoints. Set to "1" to
# disable for local automation / test harnesses. Leave UNSET in production —
# bearer-token requests are already exempt because the token itself proves
# intent (cookies aren't auto-attached on bearer requests).
CSRF_DISABLED=
# Generic bearer secret used by webhook handlers that don't have a provider-
# specific signing scheme (currently /api/webhooks/solana-pay). Senders must
# include `Authorization: Bearer $WEBHOOK_SECRET`. When unset, those webhooks
# reject every request.
# Generate: openssl rand -base64 32
WEBHOOK_SECRET=
# Legacy alias for PUBLIC_APP_ORIGIN used by api/_lib/email.js and a few
# auth callbacks. Always set BOTH to the same value during transition; the
# alias will be removed in a future cleanup pass.
APP_ORIGIN=https://three.ws
# ── Cron protection ──────────────────────────────────────────────────────────
# Shared secret expected as `Authorization: Bearer $CRON_SECRET` on calls to
# /api/cron/*. Vercel cron requests bypass this check via the `x-vercel-cron`
# header, so the secret only matters for manual / external invocation. Without
# it, only the Vercel platform can fire cron jobs.
# Generate: openssl rand -base64 32
CRON_SECRET=
# ── Pump.fun trade ingestion ─────────────────────────────────────────────────
# Trades are ingested from the chain itself: api/_lib/pump-onchain-trades.js
# holds one logsSubscribe on the pump program over the Solana RPC failover
# chain (free keyless lanes first; keyed/paid lanes as reserve) and decodes
# TradeEvents locally. This is the authoritative source and needs no key.
#
# Optional: pin the firehose to specific RPC endpoints (comma-separated
# http(s) URLs, tried first) instead of the derived free-first ordering.
PUMP_ONCHAIN_WS_URLS=
#
# Optional: PumpPortal per-mint trade stream as an augmenting second source.
# PumpPortal gates subscribeTokenTrade behind an API key whose linked wallet
# holds at least 0.02 SOL; without one the refusal is logged (throttled) and
# the on-chain firehose carries alone. subscribeNewToken (coin creations) is
# free and used regardless.
PUMPPORTAL_API_KEY=
# ── Database retention (/api/cron/db-retention) ──────────────────────────────
# Keeps the Neon branch under its project-size cap. The pump.fun intel firehose
# (pump_coin_intel + its mint-keyed satellites) ingests tens of thousands of rows
# a day; the retention cron prunes anything older than the window below and
# self-tunes under storage pressure. Docs: docs/ops/db-retention.md
#
# Normal retention window for the coin-intel firehose, in days. Clamped [2,365].
# Default 14 matches the smart-money engine's judge horizon. A 512 MB Neon free
# tier cannot physically hold 14 days of the firehose — the pressure valve below
# will hold a shorter effective window until the branch is upgraded. Raise this
# after moving to a larger Neon plan.
PUMP_INTEL_RETENTION_DAYS=14
# Hard floor the pressure valve tightens to when the branch is over the high-water
# mark. Clamped [1, PUMP_INTEL_RETENTION_DAYS]. Coins are judged within a day or
# two of launch, so 3 keeps everything load-bearing while shedding the oldest data.
PUMP_INTEL_MIN_RETENTION_DAYS=3
# Storage high-water mark in MB. At/above this the retention cron switches to the
# min window so the hard cap (512 MB on the free tier) is never actually reached.
# Clamped [128, 100000]. Lower it for more headroom; raise it after a plan upgrade.
DB_RETENTION_HIGH_WATER_MB=470
# ── Economy consolidation sweep (api/_lib/economy-sweepback.js) ───────────────
# The treasury-sweepback cron returns engine-signer surplus (SOL above each
# signer's operating float, plus stray token balances) to the economy master.
# Destination is locked in code to ECONOMY_MASTER_ADDRESS. Optional dust floor:
# a SOL sweep smaller than this is skipped so fees never exceed the return.
ECONOMY_SWEEPBACK_MIN_SOL=
# ── Per-signer floor overrides (api/_lib/solana-signers.js) ───────────────────
# Override any registry signer's SOL floor (and topup refill target) without a
# code deploy: SIGNER_MIN_SOL_<NAME> / SIGNER_REFILL_TO_SOL_<NAME>, signer name
# upper-cased with dashes as underscores. Applied at module load, so topup
# targets, balance alerts, and the floors dashboard all agree. Example: park the
# autonomous launcher's 1 SOL floor while launches are paused so its deficit
# stops outranking active engines in the deficit-sorted topup queue.
# SIGNER_MIN_SOL_COIN_LAUNCHER_MASTER=0.05
# ── Agent activity engine (api/_lib/circulation.js, /api/cron/pulse-tick) ─────
# Operates a pool of platform-owned agents that transact with one another on-chain
# (tips, payments, trades, launches, on-chain registration) so the live money feed
# reflects real wallet activity. Fully inert unless ENABLED is truthy AND a funded
# treasury secret is set. All amounts are small and topped up just-in-time.
CIRCULATION_ENABLED=
# 64-byte Solana secret key (base58, base64, or JSON array) for the funded treasury
# wallet the engine tops agent wallets up from.
CIRCULATION_TREASURY_SECRET=
# 'mainnet' (default) or 'devnet'.
CIRCULATION_NETWORK=
# Target number of operated agents (default 14) and light actions per tick (default 2).
CIRCULATION_POOL_TARGET=
CIRCULATION_ACTIONS_PER_TICK=
# Adaptive rate governor. The tick reads the treasury once and funds only as many
# SOL-spending actions as it can afford, filling the rest of the budget with
# actions that cost nothing on-chain (trials, reviews, listings). Without this the
# engine planned a fixed rate, drained the treasury, and then produced nothing but
# "treasury balance too low" skips until a human refilled it.
# SOL held back so the treasury can always pay its own transfer fees (default 0.005).
CIRCULATION_TREASURY_RESERVE_SOL=
# Estimated just-in-time top-up one paid action draws; sets the throttle curve
# (default 0.012, the observed per-action funding need).
CIRCULATION_EST_ACTION_SOL=
# Optional: enables on-chain (ERC-8004) agent registration. EVM private key (0x…)
# of a gas-funded treasury wallet, plus the chain id (default 8453 / Base). Without
# this, the deploy action is skipped.
CIRCULATION_EVM_TREASURY_SECRET=
CIRCULATION_EVM_CHAIN_ID=
# ── IRL proof-of-presence (epic IRL-Hardening H3) ────────────────────────────
# HMAC key that signs the short-lived proof-of-presence tokens minted by
# POST /api/irl/fix-token and verified on the GET /api/irl/pins nearby read
# (api/_lib/irl-presence.js). A token binds a nearby read to the coarse (~150 m)
# cell the caller actually had a GPS fix in, so a viewer can't browse pins at a
# location they aren't standing near.
#
# Enforcement is gated on this secret being set to a STRONG value (>= 16 chars):
# - SET in production: the nearby read REQUIRES a valid `x-irl-fix` token and
# fails CLOSED (401/403) when it's missing, expired, forged, or out of area.
# - UNSET (dev/preview): proof-of-presence is BYPASSED so local/sandbox testing
# isn't gated — the read works exactly as before. The active mode is logged
# once at cold start by both fix-token.js and pins.js.
# Production MUST set this. Generate: openssl rand -base64 32
IRL_FIX_SECRET=
# ── Upstash QStash (async work queue) ────────────────────────────────────────
# Powers async knowledge ingest at /api/widgets/:id/knowledge-process. Without
# QSTASH_TOKEN the publisher disables itself silently and large PDFs/URLs fall
# back to inline processing (slower, risk of timeout on Vercel's 60s limit).