Give your website or app an AI agent backend in one command

octomind server turns an agent into a WebSocket service: one session per user, real tools, and a cost frame closing every turn. Your backend keeps the login and talks to it server-to-server.

curl -fsSL https://octomind.run/install.sh | bash
OCTOMIND_SERVER_TOKEN="$(cat .token)" octomind server developer:general --port 0 --allow-origin http://localhost:3000
🚀 WebSocket server started on ws://127.0.0.1:62580
Connections must authenticate (OCTOMIND_SERVER_TOKEN is set)
Allowed browser origins: http://localhost:3000
Press Ctrl+C to stop the server
octomind 0.58.8 · captured

How it works

  1. Run the AI agent backend with a token

    Start the server on loopback with a random shared secret. Every handshake must present it as a Bearer header, so only your backend can connect. Pick the agent with a tag; each session can override it.

    export OCTOMIND_SERVER_TOKEN="$(openssl rand -hex 16)"
    octomind server developer:general --port 8080
  2. Connect from your backend, one session per user

    Your server opens the socket with the token, binds the user to their own session_id and working directory, sends the prompt, and closes when the cost frame echoes its request_id. This is the 21-line Node client the proof below ran.

    // Runs on YOUR backend, after your own auth has decided who the user is.
    import WebSocket from 'ws';
    
    const [session_id, prompt] = process.argv.slice(2); // session_id = one per user, your mapping
    const ws = new WebSocket(process.env.OCTOMIND_URL, {
    	headers: { Authorization: `Bearer ${process.env.OCTOMIND_SERVER_TOKEN}` }
    });
    const send = (frame) => ws.send(JSON.stringify(frame));
    
    ws.on('open', () =>
    	send({ type: 'session', session_id, cwd: process.env.SITE_DIR, request_id: 'bind-1' })
    );
    ws.on('message', (raw) => {
    	const frame = JSON.parse(raw);
    	console.log(JSON.stringify(frame));
    	if (frame.type === 'status' && frame.session_id === session_id && !frame.data) {
    		send({ type: 'message', session_id, content: prompt, request_id: 'ask-1' });
    	}
    	if (frame.type === 'cost' && frame.request_id === 'ask-1') ws.close();
    	if (frame.type === 'error') ws.close();
    });
  3. Check that nobody else gets in

    A handshake without the token gets 401, an unlisted browser origin gets 403, and only the Bearer header upgrades to a socket. These are the real answers from the server started above.

    H='-H Connection:Upgrade -H Upgrade:websocket -H Sec-WebSocket-Version:13 -H Sec-WebSocket-Key:dGhlIHNhbXBsZSBub25jZQ=='
    echo "== no token"; curl -s -i $H http://127.0.0.1:62580/ | grep -E "^HTTP|^Missing|^Origin"
    echo "== unlisted origin"; curl -s -i $H -H "Origin: https://evil.example" http://127.0.0.1:62580/ | grep -E "^HTTP|^Missing|^Origin"
    echo "== allowlisted origin, no token"; curl -s -i $H -H "Origin: http://localhost:3000" http://127.0.0.1:62580/ | grep -E "^HTTP|^Missing|^Origin"
    echo "== token"; curl -s -i $H -H "Authorization: Bearer $(cat .token)" http://127.0.0.1:62580/ | grep -E "^HTTP"

What you get

  • Sessions that outlive the connection

    Send a session_id and the server resumes that session from memory or disk, or creates it under that name. The session stays after the socket closes, so a user who reloads the page picks up the same conversation from a new connection.

    src/websocket/server.rs:1043
  • Per-session working directory and role

    Each bind can carry an absolute cwd and an agent tag. AGENTS.md, .agents/ guardrails, skills and tools, and the MCP project scope all follow that directory, so one server can host a different project per customer.

    src/websocket/protocol.rs:44
  • request_id correlation on every turn

    Any client frame can carry a request_id of up to 256 bytes. The server echoes it in the ack and on the cost frame that closes that turn, so your backend matches answers to requests without counting frames.

    src/websocket/server.rs:1835
  • Cost split between the main model and the helpers

    The cost frame carries running totals for tokens, cache reads and writes, reasoning and USD. Supervisor and compression calls are split out as aux_input_tokens, aux_output_tokens and aux_cost, so you can bill a user for exactly what their session spent.

    src/websocket/protocol.rs:449
  • Cancel from any connection

    A cancel frame stops the session’s running turn wherever it runs, even from a different socket. The turn ends with its usual cost frame marked cancelled. A client that disconnects mid-turn cancels its turn too.

    src/websocket/server.rs:592

One real turn, frame by frame

The Node client from step 2, run against a server started with the hero command, with a fresh session and a small site directory as cwd. The agent listed the directory with a real tool call, answered in one sentence as its AGENTS.md asked, and the cost frame closed request ask-1: $0.0137 for the session, of which $0.0015 was supervisor work. The handshake checks ran against the hero server on port 62580.

OCTOMIND_URL=ws://127.0.0.1:62777 OCTOMIND_SERVER_TOKEN="$(< .token)" SITE_DIR=/private/tmp/claude-usecases/agent-backend/work/site node agent2.mjs "usecase-demo-8ceed2" "Which files are in this site directory? Check, then answer."
{"type":"status","message":"Connected to Octomind WebSocket server (role: developer:general)"}
{"type":"ack","request_id":"bind-1","message_type":"session","session_id":"usecase-demo-8ceed2","status":"received","capabilities":["message_attachments_v1","session_control_v1","run_events_v1"]}
{"type":"status","message":"Session created: usecase-demo-8ceed2","session_id":"usecase-demo-8ceed2"}
{"type":"ack","request_id":"ask-1","message_type":"message","session_id":"usecase-demo-8ceed2","status":"received"}
…
{"type":"tool_use","tool":"view","tool_id":"call_-7144401294843704817","server":"octofs","params":{"path":"."},"session_id":"usecase-demo-8ceed2"}
{"type":"tool_result","tool":"view","tool_id":"call_-7144401294843704817","server":"octofs","content":"AGENTS.md\t3L\t~32t","success":true,"session_id":"usecase-demo-8ceed2"}
…
{"type":"assistant","content":"The site directory contains a single file: `AGENTS.md`.","session_id":"usecase-demo-8ceed2"}
{"type":"cost","session_tokens":37850,"session_cost":0.013651174000000002,"input_tokens":1128,"output_tokens":50,"cache_read_tokens":36480,"cache_write_tokens":0,"reasoning_tokens":192,"aux_input_tokens":12833,"aux_output_tokens":914,"aux_cost":0.0015223740000000004,"supervisor_accounting":{
…
},"session_id":"usecase-demo-8ceed2","request_id":"ask-1"}
octomind 0.58.8 · captured
H='-H Connection:Upgrade -H Upgrade:websocket -H Sec-WebSocket-Version:13 -H Sec-WebSocket-Key:dGhlIHNhbXBsZSBub25jZQ==' echo "== no token"; curl -s -i $H http://127.0.0.1:62580/ | grep -E "^HTTP|^Missing|^Origin" echo "== unlisted origin"; curl -s -i $H -H "Origin: https://evil.example" http://127.0.0.1:62580/ | grep -E "^HTTP|^Missing|^Origin" echo "== allowlisted origin, no token"; curl -s -i $H -H "Origin: http://localhost:3000" http://127.0.0.1:62580/ | grep -E "^HTTP|^Missing|^Origin" echo "== token"; curl -s -i $H -H "Authorization: Bearer $(cat .token)" http://127.0.0.1:62580/ | grep -E "^HTTP"
== no token
HTTP/1.1 401 Unauthorized
Missing or invalid token. Send `Authorization: Bearer <OCTOMIND_SERVER_TOKEN>`.
== unlisted origin
HTTP/1.1 403 Forbidden
Origin not allowed. Start the server with --allow-origin <ORIGIN> to permit it.
== allowlisted origin, no token
HTTP/1.1 401 Unauthorized
Missing or invalid token. Send `Authorization: Bearer <OCTOMIND_SERVER_TOKEN>`.
== token
HTTP/1.1 101 Switching Protocols
octomind 0.58.8 · captured

What it doesn't do

  • No TLS on the socket

    The server speaks plain ws:// and binds 127.0.0.1 by default. Terminate TLS at a reverse proxy such as nginx or Caddy, and keep the octomind port off the public internet. Docs →

  • Browsers cannot connect with the token

    The token is an Authorization: Bearer header, and the browser WebSocket API cannot set headers. Put your own backend in the middle: the browser talks to your app, and your server talks to octomind. Docs →

  • Replies arrive whole, not token by token

    The assistant frame carries the full reply once the model finishes. Show progress from the thinking, tool_use and tool_result frames that stream in while the turn runs.

  • No users, per-session authorization or connection cap

    session_id is a name, not a secret: any connection with the token can resume or cancel any session, and the server accepts unlimited connections. Your backend decides which user maps to which session and how many sockets it opens. For hard tenant separation, run one process per tenant with its own OCTOMIND_DATA_DIR. Docs →

  • One turn at a time per session

    A second message to a busy session gets an error saying it is busy processing another request. Queue messages per session in your backend, or wait for the cost frame before sending the next one.

Questions

What is an AI agent backend?

It is the server process that runs the agent loop, its tools and its sessions, while your app only sends messages and renders replies. With octomind, that process is octomind server, and your app talks to it over a WebSocket.

Can my frontend connect to octomind server directly?

Not with a token, because browsers cannot send the Authorization header the handshake requires. Connect from your backend, which already knows who the user is, and relay frames to the browser over your own channel.

Does the WebSocket stream tokens as the model writes?

No. Each assistant reply arrives as one frame. Thinking, tool_use and tool_result frames arrive live during the turn, so you can show what the agent is doing while it works.

How do I serve many users from one agent server?

Give each user their own session_id, and optionally their own cwd and role, from your backend. All sessions in one process share its config and credentials, so run one process per tenant when they must not share.

How do I know what each request cost?

Every turn ends with a cost frame that echoes your request_id and carries running session totals in tokens and USD. Supervisor and compression spend is reported separately in the aux_* fields.

How do I host it in production?

Run it on a private port with OCTOMIND_SERVER_TOKEN set, put a TLS-terminating reverse proxy in front if anything outside the host connects, and keep user authentication in your own backend.

Start with one command

Apache-2.0 and any model. Install it, then run step one above.

curl -fsSL https://octomind.run/install.sh | bash
WebSocket server: protocol and frames →