TokForge TokForge
Guide 9: Advanced Development

Building an AI Agent With TokForge's 270+ API Endpoints

Learn to orchestrate TokForge from Python. Build agents that manage models, run conversations, track usage, and automate AI workflows on your phone.

Run this on your own phone

TokForge is free. Android is live on Google Play; iPhone and iPad are in public beta on TestFlight. No account, and it works with the network off once a model is downloaded.

Get it on Google Play iPhone beta on TestFlight Discord community

Reading on a computer? Check which models fit your phone, or see real measured speeds on the phone-speed leaderboard.

What You'll Build

By the end of this guide, you'll have a working Python AI agent that:

Essentially, you'll treat TokForge on your phone as a remote AI backend, controlled entirely from your laptop via Python.

Prerequisites

Enable the API in Settings → Connections → Settings Mode: Advanced → Developer tools (Advanced) → Developer API server (Advanced).

The TokForge API Architecture

TokForge exposes its on-device control API as 270+ endpoints on Android. They fall into a handful of areas; here is a sample from each:

Area Purpose Key Endpoints
Health Service status and app version /health, /version
Model catalogue List downloaded models and browse the curated catalogue /models/downloaded, /models/recommended
Models Load, unload, query models /models/{id}/load, /models/{id}, /control/unload-all
Generation Check or stop a reply in progress /control/generation-status, /control/stop-generation
Conversation Multi-turn dialogue management /conversations/new, /conversations/{id}/send
Memory Character memory & facts /memory/facts, /memory/search
Character Character cards /characters, /characters/import
Benchmark Run speed tests and read saved results /benchmark/run, /benchmark/results
Download Manage model downloads /models/recommended/{id}/download, /control/download-status, /control/cancel-download
Diagnostic Logs, debugging, status /debug/logcat, /debug/errors
Settings App configuration /settings, /settings/{key}

Authentication: Every endpoint except a few status probes (such as /health and /version) requires the access token from the app, sent as a Bearer token in the Authorization header:

Authorization: Bearer YOUR_API_TOKEN

Request/Response Format: Requests use GET, POST or DELETE, and payloads are JSON. Chat replies also carry telemetry such as token count, engine and timing fields.

Step 1: Build Your API Client Class

Create a reusable TokForgeClient class to abstract HTTP calls. This makes your agent code clean and maintainable.

import requests
import json
from typing import Optional, Dict, Any

class TokForgeClient:
    """HTTP client for TokForge API with automatic Bearer token handling."""

    def __init__(self, host: str, port: int = 8088, token: str = ""):
        """
        Args:
            host: IP or hostname of phone (e.g., "192.168.1.100")
            port: TokForge API port (default 8088)
            token: Access token from the app's developer API settings
        """
        self.base_url = f"http://{host}:{port}"
        self.token = token
        self.headers = {"Content-Type": "application/json"}
        if token:
            self.headers["Authorization"] = f"Bearer {token}"

    def _request(self, method: str, path: str, data: Optional[Dict] = None) -> Any:
        """Make HTTP request with error handling."""
        url = f"{self.base_url}{path}"
        try:
            if method == "GET":
                resp = requests.get(url, headers=self.headers, timeout=30)
            elif method == "POST":
                resp = requests.post(url, headers=self.headers, json=data or {}, timeout=30)
            else:
                raise ValueError(f"Unsupported method: {method}")

            resp.raise_for_status()
            return resp.json()
        except requests.exceptions.RequestException as e:
            print(f"API Error [{method} {path}]: {e}")
            return {"error": str(e)}

    def get(self, path: str) -> Any:
        """GET request."""
        return self._request("GET", path)

    def post(self, path: str, data: Optional[Dict] = None) -> Any:
        """POST request."""
        return self._request("POST", path, data)

    # High-level convenience methods

    def health(self) -> Dict:
        """Check service health."""
        return self.get("/health")

    def models_downloaded(self) -> Any:
        """List all downloaded models (a JSON array)."""
        return self.get("/models/downloaded")

    def models_recommended(self) -> Any:
        """List catalogue models this phone can run (a JSON array)."""
        return self.get("/models/recommended")

    def load_model(self, model_id: int) -> Dict:
        """Load a downloaded model by its numeric id."""
        return self.post(f"/models/{model_id}/load", {})

    def unload_all(self) -> Dict:
        """Unload all loaded models."""
        return self.post("/control/unload-all", {})

    def characters(self) -> Any:
        """Get all character cards (a JSON array)."""
        return self.get("/characters")

    def new_conversation(self, character_id: int) -> Dict:
        """Start a new conversation with a character."""
        return self.post("/conversations/new", {"character_id": character_id})

    def send_message(self, conv_id: int, message: str) -> Dict:
        """Send a message in a conversation. Returns response with telemetry."""
        return self.post(f"/conversations/{conv_id}/send", {"message": message})

    def memory_facts(self, character_id: int) -> Any:
        """Get accumulated memory facts for a character (a JSON array)."""
        return self.get(f"/memory/facts?character_id={character_id}")

    def generation_status(self) -> Dict:
        """Check if generation is in progress."""
        return self.get("/control/generation-status")

    def settings(self) -> Dict:
        """Get app settings."""
        return self.get("/settings")

    def benchmark_results(self) -> Any:
        """Get saved benchmark results."""
        return self.get("/benchmark/results")

Step 2: Build Your Agent Loop

Now create the core agent logic that orchestrates conversations and handles telemetry.

class TokForgeAgent:
    """AI Agent that orchestrates TokForge for multi-turn dialogue."""

    def __init__(self, client: TokForgeClient, model_id: int, character_id: int):
        self.client = client
        self.model_id = model_id
        self.character_id = character_id
        self.conversation_id = None
        self.turn_count = 0
        self.total_tokens = 0

    def setup(self) -> bool:
        """Initialize agent: check health, load model, start conversation."""
        # 1. Check health
        health = self.client.health()
        if "error" in health:
            print(f"❌ Health check failed: {health['error']}")
            return False
        print(f"✓ Service is healthy")

        # 2. Load model
        print(f"Loading model {self.model_id}...")
        load_result = self.client.load_model(self.model_id)
        if "error" in load_result:
            print(f"❌ Failed to load model: {load_result['error']}")
            return False
        print(f"✓ Model loaded")

        # 3. Start conversation
        print(f"Starting conversation with character {self.character_id}...")
        conv = self.client.new_conversation(self.character_id)
        if "error" in conv or "conversation_id" not in conv:
            print(f"❌ Failed to create conversation: {conv}")
            return False
        self.conversation_id = conv["conversation_id"]
        print(f"✓ Conversation started: {self.conversation_id}")

        return True

    def send_prompt(self, prompt: str) -> Optional[str]:
        """Send a prompt and return the response."""
        if not self.conversation_id:
            print("❌ No active conversation")
            return None

        self.turn_count += 1
        print(f"\n[Turn {self.turn_count}] You: {prompt}")

        response_data = self.client.send_message(self.conversation_id, prompt)

        if "error" in response_data:
            print(f"❌ Error: {response_data['error']}")
            return None

        # Extract response and telemetry (top-level fields)
        response_text = response_data.get("response", "")
        token_count = response_data.get("token_count", 0)
        backend = response_data.get("backend", "?")

        print(f"Agent: {response_text}")
        print(f"  {token_count} tokens on {backend}")

        self.total_tokens += token_count

        return response_text

    def get_memory(self) -> list:
        """Retrieve accumulated memory facts."""
        facts = self.client.memory_facts(self.character_id)
        return facts if isinstance(facts, list) else []

    def print_stats(self):
        """Print conversation statistics."""
        print(f"\n" + "="*50)
        print(f"Conversation Summary")
        print(f"="*50)
        print(f"Turns: {self.turn_count}")
        print(f"Total tokens: {self.total_tokens}")
        memory = self.get_memory()
        print(f"Memory facts: {len(memory)}")
        print("="*50)

Step 3: Advanced Patterns

Multi-Character Orchestration

Switch between characters mid-session or run parallel conversations:

class MultiCharacterAgent:
    """Manage multiple character conversations simultaneously."""

    def __init__(self, client: TokForgeClient, model_id: int):
        self.client = client
        self.model_id = model_id
        self.conversations = {}  # {character_id: conv_id}

    def start_with(self, character_id: int) -> int:
        """Start conversation with a character."""
        if character_id in self.conversations:
            return self.conversations[character_id]

        conv = self.client.new_conversation(character_id)
        conv_id = conv.get("conversation_id")
        if conv_id:
            self.conversations[character_id] = conv_id
        return conv_id

    def send_to(self, character_id: int, message: str) -> Dict:
        """Send message to specific character."""
        conv_id = self.conversations.get(character_id)
        if not conv_id:
            conv_id = self.start_with(character_id)
        return self.client.send_message(conv_id, message)

Usage Monitoring Dashboard

Track metrics over time to optimize agent behavior. To compare speeds between phones, use the leaderboard.

import time

class PerformanceMonitor:
    """Track per-turn usage from send responses."""

    def __init__(self):
        self.metrics = []

    def record(self, response: Dict):
        """Record one reply from /conversations/{id}/send."""
        self.metrics.append({
            "timestamp": time.time(),
            "tokens": response.get("token_count", 0),
            "elapsed_ms": response.get("elapsed_ms", 0),
            "backend": response.get("backend")
        })

    def report(self):
        """Print usage summary."""
        if not self.metrics:
            print("No metrics recorded")
            return

        tokens = [m["tokens"] for m in self.metrics]
        times = [m["elapsed_ms"] for m in self.metrics]
        engines = sorted({str(m["backend"]) for m in self.metrics})

        print(f"\nUsage Report ({len(self.metrics)} replies)")
        print(f"  Avg tokens per reply: {sum(tokens)/len(tokens):.1f}")
        print(f"  Avg reply time: {sum(times)/len(times)/1000:.1f} s")
        print(f"  Engines used: {', '.join(engines)}")

Auto Model Switching

Switch models based on task complexity:

class AdaptiveAgent:
    """Switch models based on task requirements."""

    def __init__(self, client: TokForgeClient, fast_model: int, powerful_model: int):
        self.client = client
        self.fast = fast_model
        self.powerful = powerful_model
        self.current = None

    def choose_model(self, prompt: str) -> int:
        """Select model based on prompt length/complexity."""
        # Simple heuristic: complex prompts → powerful model
        if len(prompt) > 200 or any(x in prompt.lower() for x in ["analyze", "complex", "detailed"]):
            return self.powerful
        return self.fast

    def send(self, prompt: str) -> Dict:
        """Send with automatic model selection."""
        model = self.choose_model(prompt)
        if model != self.current:
            print(f"Switching to {model}")
            self.client.load_model(model)
            self.current = model
        # ... send prompt ...

Step 4: Full Working Example

Put it all together in a complete, runnable script:

#!/usr/bin/env python3
"""
Complete example: AI Agent controlling TokForge on your phone.
Run: python agent.py --host 192.168.1.100 --token YOUR_TOKEN --model 3 --character 2
"""

import argparse
import sys

def main():
    parser = argparse.ArgumentParser(description="TokForge AI Agent")
    parser.add_argument("--host", required=True, help="Phone IP (e.g., 192.168.1.100)")
    parser.add_argument("--port", type=int, default=8088, help="API port")
    parser.add_argument("--token", required=True, help="Access token from the app")
    parser.add_argument("--model", type=int, required=True, help="Model id from /models/downloaded")
    parser.add_argument("--character", type=int, required=True, help="Character id from /characters")

    args = parser.parse_args()

    # Initialize client and agent
    client = TokForgeClient(args.host, args.port, args.token)
    agent = TokForgeAgent(client, args.model, args.character)

    # Setup
    if not agent.setup():
        print("Setup failed")
        sys.exit(1)

    # Interactive loop
    monitor = PerformanceMonitor()

    print("\nAgent ready. Type 'quit' to exit, 'stats' for summary.\n")

    while True:
        try:
            user_input = input("You: ").strip()

            if user_input.lower() == "quit":
                break
            elif user_input.lower() == "stats":
                agent.print_stats()
                monitor.report()
                continue
            elif not user_input:
                continue

            # Send prompt and collect telemetry
            response = client.send_message(agent.conversation_id, user_input)
            if "error" not in response:
                print(f"Agent: {response.get('response', '')}")
                monitor.record(response)

        except KeyboardInterrupt:
            print("\nExiting...")
            break
        except Exception as e:
            print(f"Error: {e}")

    # Final summary
    agent.print_stats()
    monitor.report()

if __name__ == "__main__":
    main()

Example API Responses

Real field names, shortened. ... marks fields left out, and <number> stands in for values that depend on your phone.

Health Endpoint

GET /health

Check service status. Works without a token. For the app version, call GET /version.

{
  "status": "ok",
  "device": { "model": "Pixel 9", ... },
  "mnn_loaded": true,
  "gguf_loaded": false,
  "any_model_loaded": true,
  "control_plane": { "uptime_ms": <number>, "request_count": <number>, ... },
  ...
}

Models Downloaded

GET /models/downloaded

List all downloaded models with metadata, as a JSON array.

[
  {
    "id": 3,
    "name": "Qwen3.5 2B",
    "size_bytes": <number>,
    "model_type": "mnn",
    "is_default": true,
    "can_load_as_chat": true,
    ...
  },
  {
    "id": 7,
    "name": "Llama 3.1 8B",
    "size_bytes": <number>,
    "model_type": "mnn",
    "is_default": false,
    "can_load_as_chat": true,
    ...
  }
]

Send Message (Conversation)

POST /conversations/{id}/send

Send a message and get the reply with telemetry. The call returns when the reply is finished.

Request:
{
  "message": "What is the capital of France?"
}

Response:
{
  "status": "ok",
  "conversation_id": 12,
  "user_message_id": 341,
  "assistant_message_id": 342,
  "response": "The capital of France is Paris. It's located in the north-central part of the country and is known for its iconic landmarks like the Eiffel Tower and Notre-Dame Cathedral.",
  "token_count": <number>,
  "elapsed_ms": <number>,
  "backend": "mnn",
  "finish_reason": "stop",
  ...
}

Memory Facts

GET /memory/facts?character_id={id}

Retrieve accumulated memory for a character, most important first, as a JSON array.

[
  {
    "id": 17,
    "character_id": 2,
    "content": "User is interested in AI and machine learning",
    "importance": <number>,
    "is_pinned": false,
    "created_at": <number>,
    ...
  },
  {
    "id": 18,
    "character_id": 2,
    "content": "User prefers detailed technical explanations",
    "importance": <number>,
    "is_pinned": false,
    "created_at": <number>,
    ...
  }
]

Deployment & Best Practices

Next Steps

You now have the foundation for building sophisticated AI agents with TokForge. Here are natural extensions:

Pro Tip: Start with a simple single-conversation agent (like the Step 4 example), test it end-to-end, then layer in multi-character support, monitoring, and adaptive model selection. Don't be afraid to experiment!