← Back to Blog
Daemon Mode and REST API
14 min read daemon rest-api devops

Running OpenACP as a Daemon with REST API: The DevOps Guide

For personal use, running OpenACP in the foreground is fine -- start it when you need it, stop it when you are done. But for teams, servers, and automated workflows, you need OpenACP running as an always-on background service with programmatic access. This guide covers daemon mode, the REST API, auto-start configuration, plugin management, structured logging, and real-world automation patterns.

Daemon Mode: Always-On Agent Access

Daemon mode runs OpenACP as a background process that persists across terminal sessions and system restarts. Instead of keeping a terminal window open, the daemon runs silently, listening for messages from your configured platforms and managing agent sessions automatically.

Starting the Daemon

# Start OpenACP as a daemon
openacp --daemon start

# Output:
# OpenACP daemon started (PID: 42381)
# REST API listening on http://127.0.0.1:21420
# Telegram adapter connected
# Discord adapter connected
# Ready to receive messages

When started in daemon mode, OpenACP forks into the background and writes its PID to ~/.openacp/daemon.pid. Logs are written to ~/.openacp/logs/ instead of stdout.

Managing the Daemon

# Check if the daemon is running
openacp --daemon status

# Output:
# OpenACP daemon: running (PID: 42381)
# Uptime: 3 hours, 14 minutes
# Active sessions: 2
# Total sessions today: 7
# Memory usage: 184 MB
# REST API: http://127.0.0.1:21420

# Stop the daemon gracefully
openacp --daemon stop

# Output:
# Stopping OpenACP daemon (PID: 42381)...
# Terminating 2 active sessions...
# Daemon stopped.

# View daemon logs
openacp --daemon logs

# Follow logs in real-time (like tail -f)
openacp --daemon logs --follow

Daemon Lifecycle

The daemon follows a well-defined lifecycle:

  1. Start: Process forks, writes PID file, initializes adapters, starts REST API server
  2. Ready: All adapters connected, accepting messages and API requests
  3. Running: Managing sessions, handling messages, serving file viewer and API
  4. Stopping: Receives SIGTERM, gracefully terminates active sessions (30-second grace period)
  5. Stopped: All sessions terminated, PID file removed, process exits

If the daemon crashes unexpectedly, the PID file remains. The next openacp --daemon start detects the stale PID file, verifies the process is no longer running, cleans up, and starts a fresh instance.

Auto-Start on Boot

For server deployments, you want OpenACP to start automatically when the machine boots. Here are configurations for common init systems:

systemd (Linux)

# /etc/systemd/system/openacp.service
[Unit]
Description=OpenACP - AI Agent Bridge
After=network-online.target
Wants=network-online.target

[Service]
Type=forking
User=openacp
Group=openacp
WorkingDirectory=/home/openacp
ExecStart=/usr/local/bin/openacp --daemon start
ExecStop=/usr/local/bin/openacp --daemon stop
PIDFile=/home/openacp/.openacp/daemon.pid
Restart=on-failure
RestartSec=10
StandardOutput=journal
StandardError=journal

# Security hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=read-only
ReadWritePaths=/home/openacp/.openacp /home/openacp/projects

[Install]
WantedBy=multi-user.target
# Enable and start the service
sudo systemctl daemon-reload
sudo systemctl enable openacp
sudo systemctl start openacp

# Check status
sudo systemctl status openacp

# View logs via journald
sudo journalctl -u openacp -f

launchd (macOS)

<!-- ~/Library/LaunchAgents/org.openacp.daemon.plist -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key>
  <string>org.openacp.daemon</string>
  <key>ProgramArguments</key>
  <array>
    <string>/usr/local/bin/openacp</string>
    <string>--daemon</string>
    <string>start</string>
  </array>
  <key>RunAtLoad</key>
  <true/>
  <key>KeepAlive</key>
  <true/>
  <key>StandardOutPath</key>
  <string>/tmp/openacp.stdout.log</string>
  <key>StandardErrorPath</key>
  <string>/tmp/openacp.stderr.log</string>
</dict>
</plist>
# Load and start
launchctl load ~/Library/LaunchAgents/org.openacp.daemon.plist

# Unload
launchctl unload ~/Library/LaunchAgents/org.openacp.daemon.plist

Docker

# Dockerfile
FROM node:22-slim

RUN npm install -g @openacp/cli

# Create non-root user
RUN useradd -m -s /bin/bash openacp
USER openacp
WORKDIR /home/openacp

# Copy configuration
COPY --chown=openacp:openacp config.json .openacp/config.json

EXPOSE 21420

CMD ["openacp"]
# docker-compose.yml
version: '3.8'
services:
  openacp:
    build: .
    restart: unless-stopped
    ports:
      - "127.0.0.1:21420:21420"
    volumes:
      - ./config.json:/home/openacp/.openacp/config.json:ro
      - ./projects:/home/openacp/projects
    environment:
      - OPENACP_TELEGRAM_TOKEN=${TELEGRAM_TOKEN}
      - OPENACP_DISCORD_TOKEN=${DISCORD_TOKEN}
      - NODE_ENV=production

REST API Overview

When running (in either foreground or daemon mode), OpenACP exposes a REST API on port 21420. This API provides programmatic access to session management, message sending, status monitoring, and administrative operations.

The port number 21420 is the default and can be configured:

{
  "api": {
    "port": 21420,
    "host": "127.0.0.1",
    "auth": {
      "type": "bearer",
      "token": "your-secret-api-token"
    }
  }
}

Authentication

The API uses Bearer token authentication. Every request must include an Authorization header:

curl -H "Authorization: Bearer your-secret-api-token" \
  http://127.0.0.1:21420/api/health

If no auth token is configured, the API is accessible without authentication -- but only on localhost. This is acceptable for personal use but should always be secured for team deployments.

API Endpoints Reference

Here is the complete API reference for all available endpoints:

POST /api/sessions/new

Create a new agent session programmatically.

curl -X POST http://127.0.0.1:21420/api/sessions/new \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "agent": "claude",
    "cwd": "/home/user/projects/my-app",
    "userId": "api-user",
    "platform": "api"
  }'

# Response:
{
  "sessionId": "sess_abc123",
  "agent": "claude",
  "status": "active",
  "createdAt": "2026-03-26T10:30:00Z"
}

POST /api/sessions/:id/send

Send a message to an active session.

curl -X POST http://127.0.0.1:21420/api/sessions/sess_abc123/send \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "message": "Add error handling to the login endpoint"
  }'

# Response:
{
  "messageId": "msg_def456",
  "status": "sent"
}

POST /api/sessions/:id/cancel

Cancel the current operation in a session.

curl -X POST http://127.0.0.1:21420/api/sessions/sess_abc123/cancel \
  -H "Authorization: Bearer YOUR_TOKEN"

# Response:
{
  "status": "cancelled"
}

GET /api/sessions/:id/status

Get the current status of a session.

curl http://127.0.0.1:21420/api/sessions/sess_abc123/status \
  -H "Authorization: Bearer YOUR_TOKEN"

# Response:
{
  "sessionId": "sess_abc123",
  "status": "active",
  "agent": "claude",
  "platform": "api",
  "createdAt": "2026-03-26T10:30:00Z",
  "lastActivity": "2026-03-26T10:35:22Z",
  "messageCount": 5,
  "usage": {
    "inputTokens": 12450,
    "outputTokens": 3210,
    "estimatedCost": 0.42
  }
}

GET /api/topics

List all active topics (Telegram forum topics, Discord threads, Slack channels).

curl http://127.0.0.1:21420/api/topics \
  -H "Authorization: Bearer YOUR_TOKEN"

# Response:
{
  "topics": [
    {
      "id": "topic_123",
      "platform": "telegram",
      "name": "Feature: Auth Module",
      "sessionId": "sess_abc123",
      "messageCount": 12,
      "lastActivity": "2026-03-26T10:35:22Z"
    }
  ]
}

POST /api/cleanup

Clean up expired sessions and temporary resources.

curl -X POST http://127.0.0.1:21420/api/cleanup \
  -H "Authorization: Bearer YOUR_TOKEN"

# Response:
{
  "cleanedSessions": 3,
  "freedMemoryMB": 450,
  "removedTempFiles": 12
}

GET /api/health

Health check endpoint for monitoring and load balancers.

curl http://127.0.0.1:21420/api/health \
  -H "Authorization: Bearer YOUR_TOKEN"

# Response:
{
  "status": "healthy",
  "version": "1.4.2",
  "uptime": 11640,
  "activeSessions": 2,
  "adapters": {
    "telegram": "connected",
    "discord": "connected",
    "slack": "disconnected"
  },
  "memory": {
    "heapUsedMB": 142,
    "heapTotalMB": 256,
    "rssMB": 310
  }
}

SSE Streaming: Real-Time Event Feed

The REST API includes a Server-Sent Events (SSE) endpoint for real-time streaming of session events. This is useful for building custom dashboards, monitoring tools, or alternative frontends.

# Connect to the SSE stream
curl -N http://127.0.0.1:21420/api/sessions/sess_abc123/stream \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Accept: text/event-stream"

# Events stream in real-time:
data: {"type":"text","content":"I'll add error handling to the login endpoint.","timestamp":"2026-03-26T10:35:22Z"}

data: {"type":"tool_use","tool":"bash","args":{"command":"cat src/routes/login.ts"},"timestamp":"2026-03-26T10:35:23Z"}

data: {"type":"permission_request","tool":"file_write","path":"src/routes/login.ts","timestamp":"2026-03-26T10:35:25Z"}

data: {"type":"text","content":"I've added try-catch blocks around the authentication logic...","timestamp":"2026-03-26T10:35:30Z"}

data: {"type":"done","timestamp":"2026-03-26T10:35:31Z"}

SSE Event Types

Event Type Description Fields
text Agent text response (streamed) content, timestamp
tool_use Agent using a tool tool, args, timestamp
tool_result Tool execution result tool, result, timestamp
permission_request Permission gate triggered tool, details, timeout
permission_response User approved/denied tool, decision, timestamp
error Error occurred message, code, timestamp
done Agent finished responding timestamp, usage

Building a Custom Dashboard with SSE

// Browser-based dashboard using EventSource
const eventSource = new EventSource(
  'http://127.0.0.1:21420/api/sessions/sess_abc123/stream',
  { headers: { 'Authorization': 'Bearer YOUR_TOKEN' } }
);

eventSource.addEventListener('text', (event) => {
  const data = JSON.parse(event.data);
  appendToChat(data.content);
});

eventSource.addEventListener('tool_use', (event) => {
  const data = JSON.parse(event.data);
  showToolExecution(data.tool, data.args);
});

eventSource.addEventListener('permission_request', (event) => {
  const data = JSON.parse(event.data);
  showPermissionDialog(data.tool, data.details);
});

eventSource.addEventListener('error', () => {
  console.log('SSE connection lost, reconnecting...');
});

Plugin System

OpenACP supports plugins that extend its functionality. Plugins are npm packages installed to ~/.openacp/plugins/ and loaded automatically at startup.

Installing Plugins

# Install a plugin from npm
npm install --prefix ~/.openacp/plugins @openacp/plugin-github-issues

# Or install from a local path
npm install --prefix ~/.openacp/plugins ./my-custom-plugin

# Plugins are auto-detected at startup
openacp --daemon restart

Plugin Directory Structure

~/.openacp/
  plugins/
    node_modules/
      @openacp/
        plugin-github-issues/
          index.js
          package.json
      my-custom-plugin/
        index.js
        package.json

Plugins can add new chat commands, new API endpoints, custom message formatters, or integration with external services. The plugin API is documented in the plugins and contributing guide.

Structured Logging with Pino

OpenACP uses Pino for structured JSON logging. Every log entry includes a timestamp, log level, component name, and contextual data. This makes logs easy to parse with tools like jq, Grafana Loki, or the ELK stack.

# Default log output (JSON, one entry per line)
{"level":30,"time":1711440600000,"msg":"Telegram adapter connected","component":"TelegramAdapter","botUsername":"MyACPBot"}
{"level":30,"time":1711440601000,"msg":"Session created","component":"SessionManager","sessionId":"sess_abc123","agent":"claude","userId":"123456789"}
{"level":20,"time":1711440602000,"msg":"Agent tool use","component":"Session","sessionId":"sess_abc123","tool":"bash","args":{"command":"ls -la"}}
{"level":30,"time":1711440603000,"msg":"Permission approved","component":"PermissionGate","sessionId":"sess_abc123","tool":"bash","responseTime":2341}

Log Levels

Level Value Usage
fatal 60 Application crash, cannot continue
error 50 Errors that need attention
warn 40 Potential issues, deprecations
info 30 Normal operations (default)
debug 20 Detailed operational info
trace 10 Very detailed debug info
# Set log level via environment variable
LOG_LEVEL=debug openacp --daemon start

# Pretty-print logs for local development
openacp --daemon logs | npx pino-pretty

# Filter logs with jq
openacp --daemon logs | jq 'select(.component == "PermissionGate")'

# Count sessions per hour
openacp --daemon logs | jq 'select(.msg == "Session created") | .time' \
  | xargs -I {} date -r {} +%H | sort | uniq -c

Automation Examples

The REST API enables powerful automation workflows. Here are some practical examples:

Automated Code Review Bot

#!/bin/bash
# Trigger AI code review when a PR is opened
# (called from a GitHub webhook handler)

PR_DIFF="$1"
PR_NUMBER="$2"

# Create a new session
SESSION=$(curl -s -X POST http://127.0.0.1:21420/api/sessions/new \
  -H "Authorization: Bearer $OPENACP_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{
    \"agent\": \"claude\",
    \"cwd\": \"/home/openacp/repos/main-app\"
  }" | jq -r '.sessionId')

# Send the review request
curl -s -X POST "http://127.0.0.1:21420/api/sessions/$SESSION/send" \
  -H "Authorization: Bearer $OPENACP_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{
    \"message\": \"Review this pull request diff and provide feedback on code quality, potential bugs, and improvements:\\n\\n$PR_DIFF\"
  }"

# Wait for response and post as PR comment
# (use SSE stream to collect the full response)
REVIEW=$(curl -s -N "http://127.0.0.1:21420/api/sessions/$SESSION/stream" \
  -H "Authorization: Bearer $OPENACP_API_TOKEN" \
  | timeout 120 jq -r 'select(.type == "text") | .content' \
  | tr -d '\n')

gh pr comment "$PR_NUMBER" --body "$REVIEW"

Scheduled Maintenance Script

#!/bin/bash
# Daily maintenance: cleanup, health check, and report

# Health check
HEALTH=$(curl -s http://127.0.0.1:21420/api/health \
  -H "Authorization: Bearer $TOKEN")

STATUS=$(echo "$HEALTH" | jq -r '.status')
SESSIONS=$(echo "$HEALTH" | jq -r '.activeSessions')
MEMORY=$(echo "$HEALTH" | jq -r '.memory.rssMB')

# Cleanup expired sessions
CLEANUP=$(curl -s -X POST http://127.0.0.1:21420/api/cleanup \
  -H "Authorization: Bearer $TOKEN")
CLEANED=$(echo "$CLEANUP" | jq -r '.cleanedSessions')

# Send summary to Slack
curl -X POST "$SLACK_WEBHOOK" \
  -H "Content-Type: application/json" \
  -d "{
    \"text\": \"OpenACP Daily Report:\\n- Status: $STATUS\\n- Active sessions: $SESSIONS\\n- Memory: ${MEMORY}MB\\n- Cleaned sessions: $CLEANED\"
  }"

Integration Test Runner

// Node.js script: use the API to run integration tests via AI agent
import fetch from 'node-fetch';

const API_URL = 'http://127.0.0.1:21420';
const TOKEN = process.env.OPENACP_API_TOKEN;

async function runTestWithAgent(testDescription: string) {
  // Create session
  const { sessionId } = await fetch(`${API_URL}/api/sessions/new`, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${TOKEN}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      agent: 'claude',
      cwd: '/home/openacp/repos/main-app'
    })
  }).then(r => r.json());

  // Send test instruction
  await fetch(`${API_URL}/api/sessions/${sessionId}/send`, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${TOKEN}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      message: `Write and run a test for: ${testDescription}. Use vitest. Auto-approve all file writes.`
    })
  });

  // Collect results via SSE...
  console.log(`Test session ${sessionId} started for: ${testDescription}`);
}

// Run multiple test sessions in parallel
await Promise.all([
  runTestWithAgent('user authentication flow'),
  runTestWithAgent('payment processing webhook'),
  runTestWithAgent('file upload and validation')
]);

Production Deployment Checklist

Before deploying OpenACP's daemon mode in a production or team environment, verify these items:

  1. API token configured: Never run the API without authentication in non-localhost environments
  2. Firewall rules: Port 21420 should not be publicly accessible unless you have a specific reason
  3. systemd/launchd configured: Auto-restart on failure ensures uptime
  4. Log rotation: Configure logrotate or equivalent for ~/.openacp/logs/
  5. Monitoring: Set up health check polling on /api/health
  6. Backups: Back up ~/.openacp/config.json (not the logs)
  7. Resource limits: Set appropriate maxConcurrentSessions for your server's capacity
  8. Update plan: Schedule regular updates via npm update -g @openacp/cli

Wrapping Up

Daemon mode and the REST API transform OpenACP from a personal development tool into a team-scale platform for AI-assisted coding. The daemon ensures always-on availability, the API enables programmatic automation, SSE streaming supports real-time monitoring, and the plugin system allows unlimited extensibility.

Whether you are running OpenACP on a single developer laptop or a shared team server, the daemon and API layer provides the operational foundation you need for reliable, automated AI agent access.

Deploy OpenACP for Your Team

Install OpenACP, start the daemon, and give your whole team AI agent access from chat.

npm install -g @openacp/cli && openacp --daemon start