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 by start_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 runtime git lookup.
  • Check this before filing "is X actually fixed yet" — compare commit against 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 /llm prefix).
  • Every route documented below is reachable as https://hypermove.duckdns.org/llm/<route>.
  • The bare http://<ip>:3001 address 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 /scan directly 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/generate and Dream Cycle's start_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

RoutePurpose
GET /health{ ok: true, provider: "bedrock" } — confirms the process is up and which provider it's configured for.
POST /scanCrawl 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/extractExtract {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/consumeDecrement one free scan for a wallet (402 when exhausted).
POST /upgradeVerify an on-chain payment (native BTC or ERC-20) and upgrade a wallet to the pro tier for 30 days.
POST /registerPersist 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, then http://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/extract

Your 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 save

Health-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):

  1. Place it above that catch-all in the file — same as /quota/consume and /dream/extract are today.
  2. Otherwise it will silently return 204 for every request instead of running your handler.

Was this helpful?