Free Beta
Home / Docs / API Reference

API Reference

On-device control API · Android (MetricsService) and iPhone / iPad (control sidecar) · local HTTP, bearer token, off by default

Overview

Both TokForge apps embed a small HTTP control server on the device. It lets an AI agent, a developer, or a home-automation tool drive the app headlessly: discover hardware, load models, run inference and benchmarks, manage conversations, characters, memory, image generation, voice, and more, all over local HTTP.

The server is off by default on both platforms, requires a bearer token, and is meant for your own trusted network. It is not a cloud API and there is nothing to sign up for. Never expose the port to the public internet.

What you can do over the control plane:

  • Read device hardware, thermal, and inference state
  • List, download, load, and unload models
  • Run chat, roleplay, and group-chat turns, and stream tokens
  • Generate images and manage image models, LoRAs, and reference images
  • Manage characters, personas, lorebooks, memory, and documents (RAG)
  • Run agents and tool loops, and opt-in web search
  • Run benchmarks and auto-tune, and drive the live UI for testing

Platforms: Android & iOS

Each platform exposes its own control plane with the same concepts and mostly parallel routes. Pick a platform below and the reference switches to match.

AreaAndroidiPhone & iPad
Control planeMetricsService: NanoHTTPD, default port 8088Control sidecar: HTTP/1.1, default port 8765
EnableSettings → Advanced mode → Developer tools → Metrics ServerSettings → enable the control server (advanced), opt-in
Default bindRelease: 127.0.0.1; opt in to 0.0.0.0 for LANLoopback 127.0.0.1; dev-only LAN opt-in
AuthBearer tokenBearer token
LLM enginesllama.cpp (GGUF), MNN (CPU / OpenCL), remote APIllama.cpp + Metal, MNN, MLX, CoreML (lab), remote endpoint
Image generationStable Diffusion (CPU / OpenCL), optional Hexagon NPUApple CoreML on the Neural Engine, plus stable-diffusion.cpp on 12 GB
Voice / visionOffline TTS / voice cloning, Whisper STT; MNN visionApple TTS/STT plus offline voices; Apple Vision, MNN vision (gated)
Both control planes are off by default and require a token. They are for local automation, observability, and end-to-end driving on your own network. The route counts are large and evolve build to build: this page documents the major families with real examples rather than every route.

Enable the server

The MetricsService is disabled by default. To turn it on:

  1. Open TokForge → Settings and switch on Advanced mode (toggle at the top of Settings).
  2. Open the Developer tools section, then find Metrics Server and toggle it on.
  3. Choose a Bind Host:
    • Localhost only (127.0.0.1): reachable only from the device itself or through ADB port forwarding. Best for development.
    • All interfaces (0.0.0.0): reachable from any device on your LAN, for example http://192.168.1.50:8088/health.
  4. Set the Port (default 8088) and set or regenerate the Metrics Auth Token.

Release builds honour your Bind Host choice. A normal Play Store build binds 0.0.0.0 and is reachable over the LAN when you select All interfaces; you do not need a debug build or ADB. The only build-type difference is the first-run default and whether auth can be skipped on loopback:

BuildFirst-run bindLocalhost (127.0.0.1)All interfaces (0.0.0.0)
Debug0.0.0.0Auth bypassedAuth required
Release127.0.0.1Auth requiredAuth required
Security note. With All interfaces, anything on your LAN that can reach the phone can reach the API. Keep auth on, use a strong token, and never forward the port to the internet.

Authentication

Nearly every route needs Authorization: Bearer <token>. The token lives in Settings → Developer tools → Metrics Server (the Metrics Auth Token field); tap Regenerate to rotate it, or rotate over the API:

curl -X POST -H "Authorization: Bearer CURRENT_TOKEN" \
     http://localhost:8088/control/rotate-auth-token
# -> {"status":"ok","auth_token":"NEW_TOKEN","token_rotated":true}

A missing or invalid token returns 401:

{"error": "Unauthorized: provide Bearer token in Authorization header"}

Unauthenticated endpoints

Exactly three routes need no token, plus the loopback-gated bootstrap. Everything else, including /state/hardware, requires a bearer token.

EndpointDescription
GET /healthServer status, loaded models, uptime
GET /versionApp version, build type, device
GET /metricsLoaded-model state
GET/POST /auth/bootstrapLoopback only. Returns the current token. On release builds it also requires a shell-proven headless session; the simple path is to copy the token from Settings.

Connecting

Over the LAN (All interfaces). Find the phone's IP in Settings → Wi-Fi (tap the network) and use it as the base URL. /health needs no token; everything else does:

BASE="http://192.168.1.50:8088"
AUTH="Authorization: Bearer YOUR_TOKEN"

curl -s "$BASE/health" | jq
curl -s -H "$AUTH" "$BASE/state" | jq

Over ADB (Localhost, or when the router blocks device-to-device traffic). Forward the port over a cable:

adb forward tcp:8088 tcp:8088
curl -s http://localhost:8088/health | jq

# Multiple devices: use a unique local port each
adb -s DEVICE2_SERIAL forward tcp:8089 tcp:8088
If /health times out over Wi-Fi even though both devices are on the same network, the router is probably isolating clients (AP / client isolation). Use ADB forwarding instead.

Health & state

MethodRoutePurpose
GET/healthBackends loaded, uptime, request counters No auth
GET/versionApp / build / device No auth
GET/metricsLoaded-model flags No auth
GET/stateFull snapshot (settings, memory, battery, thermal)
GET/state/inferenceLoaded models, backend availability, generation status
GET/state/hardwareSoC, cores, GPU, RAM, recommended config Auth
GET/state/settingsUser settings and endpoints
GET/storageModel directory and disk usage
GET/performanceLive memory / thermal / last-generation stats
GET/control/generation-statusReal in-flight inference lock state
curl -s http://localhost:8088/health | jq
curl -s -H "$AUTH" http://localhost:8088/state/hardware | jq '.soc_model, .total_ram_mb'

Models & downloads

Long-running calls (load, download, scan) return an operation_id in their 2xx body. Poll GET /control/operation/{id} for the real lifecycle (queued → running → completed | failed, with download percentage).

MethodRoutePurpose
GET/modelsInstalled models with metadata
GET/models/{id}One model by database id
POST/control/load-modelLoad a model (returns operation_id; 409 insufficient_ram unless forced)
POST/control/load-model-pathLoad any on-disk model by absolute path
POST/control/unload-allUnload everything
POST/control/switch-backendSwitch MNN / GGUF / remote (409 mid-generation)
POST/control/download-modelDownload by URL (returns operation_id; re-issuing resumes)
POST/control/cancel-downloadCancel; keeps .part files for resume
POST/models/search/queryHugging Face search (then /models/search/download-file)
POST/control/install-modelInstall a local tar / tar.gz MNN bundle
POST/control/scan-modelsRegister newly added model files
POST/control/delete-modelDelete a model
# Load model 1, then poll the operation to confirm it finished
OP=$(curl -s -H "$AUTH" -H 'Content-Type: application/json' \
       -X POST "$BASE/control/load-model" -d '{"model_id":1}' | jq -r '.operation_id')
while :; do
  S=$(curl -s -H "$AUTH" "$BASE/control/operation/$OP" | jq -r '.status')
  [ "$S" = completed ] && break
  [ "$S" = failed ] && { echo "load failed"; break; }
  sleep 1
done

Chat & conversations

Two ways to run a turn. The direct synchronous path is POST /conversations/{id}/send or the unified POST /api/chat. The UI-automation path (/control/chat/type then /control/chat/send) drives the real visible chat and returns immediately; poll /control/generation-status and the messages route for progress.

MethodRoutePurpose
POST/control/create-conversationCreate (requires character_id; navigate:true to open it)
POST/conversations/{id}/sendDirect synchronous send (text or message); group-aware
POST/api/chatUnified end-to-end chat (accepts image_path / image_base64 for vision)
POST/control/chat/typeType a draft into the live composer
POST/control/chat/sendQueue the send the UI uses; returns immediately
POST/control/chat/regenerateAdd another candidate reply
POST/control/chat/select-responsePick a response variant (variant_index or direction)
POST/control/stop-generationStop; ?await=true blocks until idle
GET/conversations/{id}/messagesMessages with response variants
GET/conversationsList conversations
DELETE/conversations/{id}Delete a conversation
CID=$(curl -s -H "$AUTH" -H 'Content-Type: application/json' \
  -d '{"character_id":1,"navigate":true}' \
  "$BASE/control/create-conversation" | jq -r '.conversation_id')

curl -s -H "$AUTH" -H 'Content-Type: application/json' \
  -d "{\"conversation_id\":$CID,\"text\":\"Reply with exactly: PONG\"}" \
  "$BASE/conversations/$CID/send" | jq '.finish_reason, .content'

Every terminal reply from /conversations/{id}/send and /api/chat carries done, finish_reason (stop / length / cancelled / error / timeout), tokens_emitted, and elapsed_ms. A clean answer is done == true and finish_reason in {stop, length}. A timeout means the HTTP wall fired but generation continues on the device (re-read the conversation).

Group chat

On a group conversation, the same send routes run the group turn policy (speaker selection, per-member persona and memory). Pin a speaker with member_id or character_id in the body.

MethodRoutePurpose
POST/control/conversations/{id}/groupPromote to group, set roster and activation strategy
GET/control/conversations/{id}/membersList members
POST/control/conversations/{id}/membersAdd / update a member (turn_order, is_muted, talkativeness, voice_ref); seat conflict returns 409 seat_occupied
DELETE/control/conversations/{id}/members/{character_id}Remove a member (the primary cannot be removed)

Memory & RAG

MethodRoutePurpose
GET / POST/memory/factsList / create facts (?await_embedding=true blocks until indexed)
POST/memory/facts/{id}/pinPin a fact
GET/memory/edgesKnowledge-graph edges
POST/memory/documents/ingestIngest a document (text or URL) for RAG
GET/memory/documentsList ingested documents
POST/rag/searchRetrieve chunks for a query
GET/memory/searchSearch across facts and documents
GET/memory/statsCounts and storage usage
POST/control/reflectTrigger background memory extraction
curl -s -H "$AUTH" -H 'Content-Type: application/json' \
  -X POST "$BASE/rag/search" -d '{"query":"what does the contract say about refunds?"}' | jq

Image generation

Text-to-image is a guarded chat feature. For a release-faithful result, drive it through the chat path (create conversation, select an image model with /control/chat/image-models/select, then type and send). The direct generate routes below are the headless equivalent.

MethodRoutePurpose
GET/control/imagegen/statusSelector gates, download progress, route labels
POST/control/imagegen/select-modelChoose an image model
POST/control/imagegen/generateRender (507 image_generation_low_memory if the memory gate refuses)
POST/control/imagegen/batchBatch render (202 with batch_id)
GET / POST / DELETE/control/imagegen/referenceReference-image identity (img2img)
GET / POST / DELETE/control/imagegen/loraManage LoRAs
GET/conversations/{id}/messages/{message_id}/imageExport a generated image (PNG)

Voice & TTS

MethodRoutePurpose
GET/control/voice/listAvailable voices
POST/control/voice/selectSelect a voice
POST/control/tts/speakSpeak text aloud on the device
GET/control/tts/statusTTS status
POST/control/tts/stopStop playback
POST/control/install-tts-packInstall an offline TTS voice pack

POST /control/voice/clone and DELETE /control/voice/{id} are debug-build only.

Agents

Agents run a tool loop over whatever local backend is loaded (MNN included). They only refuse when nothing is loaded.

MethodRoutePurpose
GET/control/agent/listList agents (alias /control/agents)
POST/control/agent/runRun an agent
GET/control/agent/statusRun status and results
POST/control/agent/cancelCancel a run
POST/control/agent/save / deleteSave or delete (built-ins are fork-on-edit editable)

Benchmark & ForgeLab

MethodRoutePurpose
POST/benchmark/runRun one benchmark (returns operation_id)
GET/benchmark/results / matrixQuery results, or a cross-device matrix
POST/benchmark/auto-matrixSweep model / backend combinations
GET/benchmark/exportExport results (JSON / CSV)
POST/control/auto-tuneSweep configs, recommend optimal settings (returns operation_id)
GET/control/autoforge/plan / candidatesAutoForge route plan and candidates
GET / POST/forge/profiles, /forge/shareForge profiles and leaderboard share
curl -s -H "$AUTH" -H 'Content-Type: application/json' \
  -X POST "$BASE/benchmark/run" \
  -d '{"prompt":"Benchmark prompt","num_tokens":100}' | jq '.operation_id'

Settings & backup

MethodRoutePurpose
GET/settings / /settings/{key}Read settings (unknown key: 404)
POST/settings/{key}Set one value (invalid value: 422 invalid_settings_value)
POST/control/set-settingsBulk settings update
POST/control/set-inference-configSampler / context / KV / attention config
GET/debug/exportExport full app state as a ZIP (models, conversations, settings, logs)

Debug-only surfaces

Some routes exist only on debug builds and are stripped from the release APK (they answer 404 on release):

  • GET/POST /debug/ui/*: the UI-drive harness (tree / tap / text / scroll / await) for driving Compose screens over HTTP.
  • POST /debug/crash-test, POST /control/import-model-path, POST /control/image-models/import, POST /control/voice/clone, POST /control/tq3_trace.

A few observability routes are release-available (for example GET /debug/log-tail, GET /debug/tombstones, GET /debug/generation-trace, GET /debug/inference-readiness), still behind the bearer token.

Errors & guards

The server returns honest status codes rather than a hollow 200:

CodeMeaning
400Malformed input
401Missing or bad bearer token
404Unknown id or route
405Path exists under another verb (an Allow header is set)
409Busy or wrong state (for example generation_in_flight; pass ?force=true where allowed)
413payload_too_large: the request body or attachment exceeds the cap
422Invalid settings value or a bodied DELETE on a query-only route
503A benchmark harness holds the server-side bench lock; the body names the holder
507image_generation_low_memory: the image memory gate refused
500Uncaught error (detail redacted when bound to 0.0.0.0)

Mutation guards: switch-backend, clear-generated-caches, and reset-state(scope=all) answer 409 while a generation is in flight. To serialize cleanly, stop first with POST /control/stop-generation?await=true.

Full catalog

This page documents the major families with runnable examples. The live server is always the source of truth: a release build exposes roughly 280 routes across about two dozen handler groups, and the exact set for your build is what the app registers. Start from GET /health and GET /state/inference, then explore the families above. Route names and paths here are used exactly as the app implements them.