LLM Service
A separate, standalone process (
services/llm/server.ts) that keeps model API keys off the main app entirely. It powers two things: turning a URL into an agent-callable MCP server (POST /scan, used by /tools/generate) and Dream Cycle's memory extraction (POST /dream/extract, called automatically bystart_dream— you never call this route yourself). Most people reading this page will never need to run their own copy of it.
Live endpoint (agents: start here)
HyperMove's own deployment of this service is already running, reachable over HTTPS (Caddy-fronted, Let's Encrypt cert) at:
https://hypermove.duckdns.org/llm
No setup, no key, no signup — every example below works against this address right now. Confirm it's up before anything else:
curl https://hypermove.duckdns.org/llm/health
# { "ok": true, "provider": "bedrock", "commit": "f1202e41...", "deployed_at": "2026-07-27T09:02:00Z" }commit/deployed_at (added 2026-07-27) tell you which build is actually running:
- It's a build-time constant injected by
scripts/deploy-vps.sh, not a runtimegitlookup. - Check this before filing "is X actually fixed yet" — compare
commitagainst the commit SHA you expect to be live rather than trusting a maintainer's unverifiable "should be deployed now."
The service itself still listens on plain HTTP internally (localhost:3001 on the
host):
- Caddy terminates TLS and reverse-proxies
/llm/*→:3001(stripping the/llmprefix). - Every route documented below is reachable as
https://hypermove.duckdns.org/llm/<route>. - The bare
http://<ip>:3001address is no longer the documented public entry point — prefer the HTTPS domain above for anything crossing the public internet.
If you're building an agent that uses Dream Cycle (submit_episode_log /
start_dream / query_dream over MCP), you don't need this address at
all — start_dream already calls this service internally, server-side. This
address only matters if you're calling /scan directly, or self-hosting your
own copy — see Dream Cycle if that's not you.
Do I need this page?
Almost certainly not as a Dream Cycle or MCP Gateway user — extraction is already hosted and configured for you (see Dream Cycle). This page is for:
- Agents/integrators calling
POST /scandirectly to turn a URL into an MCP server (the endpoint above is all you need). - Operators deploying HyperMove's own infrastructure who need to configure or redeploy this service.
- Self-hosters who want to point Dream Cycle's extraction stage at their own model, provider, or key instead of HyperMove's default.
- Anyone curious what's actually running behind
/tools/generateand Dream Cycle'sstart_dream.
What it is
A plain Node HTTP server (no framework), run with tsx, deployed as its own
process independent of the main Next.js app. One config surface
(LLM_PROVIDER + a matching API key) serves every route below — there's no
per-route credential duplication.
cd services/llm
npm install
PORT=3001 BEDROCK_API_KEY=... npx tsx server.ts
# or: npm start (reads services/llm/.env.local automatically)Configuration — one provider, one key
LLM_PROVIDER=bedrock # bedrock (default) | anthropic | openai
# bedrock — uses the Converse API (uniform request/response shape across
# every Bedrock model, so swapping BEDROCK_MODEL never needs a code change):
BEDROCK_API_KEY=...
BEDROCK_REGION=us-east-1 # optional, this is the default
BEDROCK_MODEL=deepseek.v3.2 # optional, this is the default — the
# cheapest DeepSeek variant Bedrock hosts
# ($0.62/$1.85 per 1M input/output tokens,
# vs DeepSeek-R1's $1.35/$5.40)
# anthropic:
ANTHROPIC_API_KEY=...
ANTHROPIC_MODEL=claude-sonnet-4-20250514 # optional
# openai:
OPENAI_API_KEY=...
OPENAI_MODEL=gpt-4o # optional
# Optional — quota/payment tracking (Supabase Postgres). Omitted → every
# quota check no-ops permissively (unlimited, dev-friendly default).
DATABASE_URL=postgres://...Switching provider — or switching model within Bedrock — is one env var; every route
(/scan, /dream/extract) picks it up automatically, no code change.
Dream Cycle users never need any of the above. This key lives entirely on this
service; the 5 Dream Cycle MCP tools (submit_episode_log, start_dream,
get_dream_config, query_dream, get_dream_stats) never accept or require an API
key parameter — see Dream Cycle.
Routes
| Route | Purpose |
|---|---|
GET /health | { ok: true, provider: "bedrock" } — confirms the process is up and which provider it's configured for. |
POST /scan | Crawl a URL, ask the model to synthesize an MCP manifest (tools + schemas) from the page, return it plus a ready-to-paste mcpConfig. Powers /tools/generate. |
POST /dream/extract | Extract {rules, preferences, error_patterns, facts} from a short episode-cluster summary. Called automatically by Dream Cycle's start_dream — see Dream Cycle. Not meant to be called directly by end users. |
GET /quota?wallet=0x… | Read a wallet's free-scan quota + tier. |
POST /quota/consume | Decrement one free scan for a wallet (402 when exhausted). |
POST /upgrade | Verify an on-chain payment (native BTC or ERC-20) and upgrade a wallet to the pro tier for 30 days. |
POST /register | Persist a generated MCP manifest so it's servable later at /<wallet>/<slug>. |
GET / POST /<wallet>/<slug> | Serve a previously registered MCP manifest (GET) or handle JSON-RPC calls against its tools (POST) — the hosted-MCP surface /scan + /register produce. |
1 — Turn a URL into an MCP server
curl -s -X POST https://hypermove.duckdns.org/llm/scan \
-H 'content-type: application/json' \
-d '{"url":"https://example.com","wallet":"0xYourWalletAddress"}'Returns:
{
"manifest": { "name": "example-com-mcp", "tools": [ /* … */ ] },
"mcpConfig": { "mcpServers": { "example-com-mcp": { "url": "https://your-deploy/0x…/example-com-mcp", "transport": "http" } } },
"crawlData": { "url": "...", "title": "...", "toolCount": 2 }
}Paste mcpConfig straight into any MCP client's config file — the generated
server is already registered (via /register, called internally) and
servable at that URL.
2 — Dream Cycle extraction (you don't call this directly)
curl -s -X POST https://hypermove.duckdns.org/llm/dream/extract \
-H 'content-type: application/json' \
-d '{"summary":"agent failed: gripper timeout during pick_and_place, retried without cooldown","max_output_tokens":150}'Returns:
{ "rules": ["Add cooldown period after gripper timeout before retrying pick_and_place"],
"preferences": [],
"error_patterns": ["Gripper times out during pick_and_place when retried immediately without cooldown"],
"facts": [] }This exists so src/lib/mcp/dream/extract.ts has a real model to call —
start_dream invokes it once per cluster automatically.
On any error or malformed model output it degrades to
{rules:[],preferences:[],error_patterns:[],facts:[]} rather than crashing the
pipeline, so a Dream Cycle run always reaches a terminal status even if this service
is briefly unreachable.
Point Dream Cycle at your own instance (advanced, optional)
By default, the main app's DREAM_EXTRACT_URL points at HyperMove's own deployed
instance — nothing to configure:
- It falls back to
LLM_SERVICE_URL, thenhttp://localhost:3001/dream/extract. - The connection is over the internal loopback (same host, server-to-server — no public network hop, so plain HTTP is correct there).
If you're self-hosting and want a different model or provider for extraction specifically:
# In the main app's env (not this service's):
DREAM_EXTRACT_URL=https://your-llm-service.example.com/dream/extractYour instance just needs to implement the same two-field-in, four-array-out contract shown in step 2 above.
Deploy
This service is deployed independently of the main app (its own process,
its own restart lifecycle) — typically via pm2:
cd services/llm
npm install
pm2 start "npx tsx server.ts" --name llm-service --cwd $(pwd)
pm2 saveHealth-check after any redeploy:
curl http://localhost:3001/health
# { "ok": true, "provider": "bedrock" }Front it with a reverse proxy for public HTTPS access (HyperMove's own deployment uses Caddy — see the live endpoint at the top of this page):
hypermove.duckdns.org {
handle_path /llm/* {
reverse_proxy localhost:3001
}
reverse_proxy localhost:3003 # the main Next.js app
}
caddy reload --config <path> --adapter caddyfile applies changes with zero
downtime (no restart needed) once Caddy is already running against that file.
A routing gotcha if you add a new route here
server.ts uses a hand-rolled router (plain if chains on req.method + url, no
framework) with one greedy catch-all near the bottom: any exactly-two-segment path
(/<wallet>/<slug>) is claimed by the hosted-MCP handler before anything defined after
it in the file gets a chance to run.
If you add a new route whose path also happens to have exactly two segments (like
/dream/extract did):
- Place it above that catch-all in the file — same as
/quota/consumeand/dream/extractare today. - Otherwise it will silently return
204for every request instead of running your handler.
Was this helpful?