Private Agents
Bring your own hardware under the router. Mint a time-bounded join key, run the XIM agent, watch it transition from pending to active, and the router treats it as a first-class worker for the project that owns it. This page is the practical CRUD: join keys, enrollment, lifecycle, monitoring.
Overview
For a side-by-side of private agents vs. shared agents, see the Agent Types overview.
This page documents the project-scoped Frontend API (authenticated
with a project API key or dashboard session). The router also exposes
a parallel API-key-authed management surface at
/:project_id/v1/management/join-keys and
/:project_id/v1/management/agents; see the API reference
for details. Defaults may differ between the two surfaces.
Agent Fields
The following fields describe an enrolled agent. They are returned by the agent list and detail API endpoints.
| Field | Type | Description |
|---|---|---|
| id | string (UUID) | Unique agent identifier. |
| name | string | Human-readable name for the agent. Settable at enrollment and updatable via PATCH. |
| worker_id | string | Unique worker identifier used in the mesh protocol. |
| region | string | Region where the agent is located (1-24 ASCII characters, inherited from the join key used at enrollment). |
| max_concurrent_requests | integer | Maximum number of concurrent inference requests this agent can handle. 0 means auto-configured by the platform. |
| description | string or null | Optional description or notes about this agent. Updatable via PATCH. |
| supported_tiers | array of strings or null | Service tier IDs this agent supports. When null, the platform uses defaults for the agent's accelerator type. Example: ["gpu_nvidia_shared", "self_hosted"]. |
| storage_limit_enabled | boolean or null | Whether a storage limit is configured for this agent. When false or null, the agent can use unlimited storage up to disk capacity. |
| storage_limit_bytes | integer (int64) or null | Configured storage limit in bytes. Only relevant when storage_limit_enabled is true. |
| status | string | Current lifecycle status. See Agent Lifecycle for all values. |
| last_heartbeat_at | string (ISO 8601) or null | Timestamp of the most recent heartbeat received from the agent. |
| enrolled_at | string (ISO 8601) or null | Timestamp when the agent successfully completed enrollment. |
| created_at | string (ISO 8601) or null | Timestamp when the agent record was created. |
Join Key Management
Join keys are secure tokens that authorize a backend agent to enroll in your project. Each key is tied to a specific project, carries a region assignment, and expires after a configurable TTL (maximum 1 hour). Keys can be single-use or multi-use.
API Endpoints
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/v1/projects/:projectId/join-keys | Create a new join key. |
| GET | /api/v1/projects/:projectId/join-keys | List all join keys for the project (paginated). |
| GET | /api/v1/projects/:projectId/join-keys/:keyId | Get join key details. |
| DELETE | /api/v1/projects/:projectId/join-keys/:keyId | Revoke a join key. Agents already enrolled are not affected. Returns 204 No Content on success. |
Join Key Fields
The following fields are accepted when creating a join key (POST).
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | Yes | Human-readable label for the join key (e.g. "datacenter-rack-3"). |
| region | string | Yes | Region to assign to agents enrolled with this key. Must be 1-24 ASCII characters (e.g. "us-east-1"). |
| expires_in_hours | integer or null | No | Time-to-live in hours. Defaults to the maximum (1 hour). The hard limit is 1 hour regardless of the value supplied. Values less than or equal to 0 are accepted but produce an already-expired key; supply a positive integer. |
| max_enrollments | integer or null | No | Maximum number of agents that may enroll with this key. Defaults to 1 (single-use). Note: the router-side management endpoint (/v1/management/join-keys) uses a different default (10), so always set max_enrollments explicitly when calling either surface. |
| router_addresses | array of strings or null | No | Router addresses for agents to connect to. When omitted, platform defaults are used. |
The response for a create request includes the full key value exactly once. It cannot be retrieved later because only the SHA-256 hash is stored.
The list and detail responses include the following join key fields:
| Field | Type | Description |
|---|---|---|
| id | string (UUID) | Unique join key identifier. |
| name | string | Human-readable label. |
| key_prefix | string | Visible prefix of the key for identification. The first 8 characters include the xjk_ tag and the leading characters of the project slug (e.g. for project slug myproj, the prefix renders as xjk_mypr****wxyz). The full key is never returned after creation. |
| region | string | Region assigned to agents enrolled with this key. |
| max_enrollments | integer | Maximum enrollments allowed. |
| current_enrollments | integer | Number of agents that have enrolled using this key so far. |
| expires_at | string (ISO 8601) | Timestamp when the key expires. |
| status | string | Current status: active, used, expired, or revoked. |
| can_enroll | boolean | Whether the key can still be used for enrollment (active, not expired, enrollments remaining). |
| created_at | string (ISO 8601) or null | Timestamp when the key was created. |
Join Key API Examples
In the examples below, replace proj_ABC123 with your
project external id (e.g. proj_a1b2c3d4). The route
parameter is named :projectId and accepts the project
external id.
Create a Join Key
curl -X POST https://api.xerotier.ai/api/v1/projects/proj_ABC123/join-keys \
-H "Authorization: Bearer xero_my-project_abc123" \
-H "Content-Type: application/json" \
-d '{
"name": "datacenter-rack-3",
"region": "us-east-1",
"expires_in_hours": 1,
"max_enrollments": 1
}'
import requests
headers = {
"Authorization": "Bearer xero_my-project_abc123",
"Content-Type": "application/json"
}
response = requests.post(
"https://api.xerotier.ai/api/v1/projects/proj_ABC123/join-keys",
headers=headers,
json={
"name": "datacenter-rack-3",
"region": "us-east-1",
"expires_in_hours": 1,
"max_enrollments": 1
}
)
result = response.json()
# Save the key value immediately, it cannot be retrieved later
print(f"Join Key: {result['key']}")
const response = await fetch(
"https://api.xerotier.ai/api/v1/projects/proj_ABC123/join-keys",
{
method: "POST",
headers: {
"Authorization": "Bearer xero_my-project_abc123",
"Content-Type": "application/json"
},
body: JSON.stringify({
name: "datacenter-rack-3",
region: "us-east-1",
expires_in_hours: 1,
max_enrollments: 1
})
}
);
const result = await response.json();
// Save the key value immediately, it cannot be retrieved later
console.log(`Join Key: ${result.key}`);
List Join Keys
curl https://api.xerotier.ai/api/v1/projects/proj_ABC123/join-keys \
-H "Authorization: Bearer xero_my-project_abc123"
Get a Join Key
curl https://api.xerotier.ai/api/v1/projects/proj_ABC123/join-keys/KEY_ID \
-H "Authorization: Bearer xero_my-project_abc123"
Revoke a Join Key
curl -X DELETE https://api.xerotier.ai/api/v1/projects/proj_ABC123/join-keys/KEY_ID \
-H "Authorization: Bearer xero_my-project_abc123"
Agent API
Agents are enrolled automatically using a join key (see Agent Enrollment). Once enrolled, you can list, view, update, and remove agents via the following endpoints.
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v1/projects/:projectId/agents | List all enrolled agents for the project (paginated). Supports ?status= filter. |
| GET | /api/v1/projects/:projectId/agents/:agentId | Get full details of a specific agent. |
| PATCH | /api/v1/projects/:projectId/agents/:agentId | Update agent name, description, or status (suspend/resume). |
| DELETE | /api/v1/projects/:projectId/agents/:agentId | Remove an agent from the project. The stored status is set to disconnected and a disconnected event is emitted with reason=removed_by_user. The agent remains visible in the list under the disconnected status. Returns 204 No Content on success. |
| GET | /api/v1/projects/:projectId/agents/:agentId/events | Retrieve the event log for a specific agent. |
PATCH Request Body
All fields are optional. Only provided fields are updated.
| Field | Type | Description |
|---|---|---|
| name | string or null | New human-readable name for the agent. |
| description | string or null | New description or notes for the agent. |
| status | string or null |
Status change request. Valid values:
|
Agent API Examples
List Agents
curl https://api.xerotier.ai/api/v1/projects/proj_ABC123/agents \
-H "Authorization: Bearer xero_my-project_abc123"
Get Agent Details
curl https://api.xerotier.ai/api/v1/projects/proj_ABC123/agents/AGENT_ID \
-H "Authorization: Bearer xero_my-project_abc123"
Suspend an Agent
curl -X PATCH https://api.xerotier.ai/api/v1/projects/proj_ABC123/agents/AGENT_ID \
-H "Authorization: Bearer xero_my-project_abc123" \
-H "Content-Type: application/json" \
-d '{"status": "suspended"}'
Resume an Agent
curl -X PATCH https://api.xerotier.ai/api/v1/projects/proj_ABC123/agents/AGENT_ID \
-H "Authorization: Bearer xero_my-project_abc123" \
-H "Content-Type: application/json" \
-d '{"status": "active"}'
Get Agent Events
curl https://api.xerotier.ai/api/v1/projects/proj_ABC123/agents/AGENT_ID/events \
-H "Authorization: Bearer xero_my-project_abc123"
Remove an Agent
curl -X DELETE https://api.xerotier.ai/api/v1/projects/proj_ABC123/agents/AGENT_ID \
-H "Authorization: Bearer xero_my-project_abc123"
Agent Enrollment
To enroll a private agent:
- Create a join key in the dashboard or via the API (see Join Key API Examples).
- Save the full key value immediately, it is shown only once.
- Run the XIM agent with the
enrollsubcommand, passing the join key. - The agent authenticates with the router and establishes a secure encrypted connection.
- The agent transitions to
activestatus and begins sending heartbeats. It is then ready to receive inference requests.
# Enroll a XIM node
xerotier-xim-agent enroll --join-key "xjk_my-project_..."
# Or using the environment variable
XEROTIER_AGENT_JOIN_KEY="xjk_my-project_..." xerotier-xim-agent enroll
After enrollment, start the agent in serve mode to begin handling inference requests. See the XIM Deployment guide for detailed setup instructions.
Agent Monitoring
The agent dashboard at /agents provides real-time visibility
into your enrolled agents:
- Heartbeat status: whether each agent is actively communicating with the router.
- Current load: active request count and capacity.
- Model information: which model is loaded and its serving state.
- Event history: per-agent log of significant lifecycle events.
Programmatic Access
For programmatic snapshot access to your fleet, use the documented
REST endpoint
GET /api/v1/projects/:projectId/agents
(see Agent API) with a project API key.
Heartbeat Tracking
The router tracks the last_heartbeat_at timestamp for each
agent. An agent with a heartbeat older than the staleness threshold
(default 30 seconds; configurable via
XEROTIER_FRONTEND_HEARTBEAT_TIMEOUT_SECONDS) is considered
disconnected and excluded from routing decisions until it reconnects and
resumes sending heartbeats. The effective status shown in the dashboard
and API is derived from both the stored status and heartbeat freshness.
Agent Lifecycle
Agents transition through the following status states. The status
field in the database reflects the stored lifecycle state. The effective_status
is computed at runtime from the stored status and heartbeat freshness.
| Status | Description |
|---|---|
pending |
Agent record has been created (join key issued) but the agent has not yet connected and authenticated. |
active |
Agent is connected and sending regular heartbeats. If the most recent heartbeat is older than the heartbeat staleness threshold (default 30 seconds; configurable via XEROTIER_FRONTEND_HEARTBEAT_TIMEOUT_SECONDS), the effective status is disconnected even while the stored status remains active. |
disconnected |
Stored after the heartbeat-timeout worker observes a stale agent or after a DELETE remove call; also computed as the effective_status when the stored status is active but the most recent heartbeat is stale. The router excludes the agent from routing until it reconnects and heartbeats resume. The agent may reconnect and return to active. |
suspended |
Agent has been suspended by the project owner. Suspended agents do not receive new requests. Resume with PATCH status=active. |
dead |
Terminal state. The agent failed to come online within the join key lifespan or was declared dead by the platform. Dead agents do not receive requests and cannot be revived. |
Allowed Transitions
| From | To | Trigger |
|---|---|---|
pending |
active |
Agent connects and completes enrollment handshake. |
pending |
dead |
Join key expired before the agent connected. |
active |
suspended |
Owner calls PATCH with status=suspended. |
active |
disconnected |
Heartbeat timeout detected by the router. |
active |
dead |
Platform declares agent dead after extended absence. |
disconnected |
active |
Agent reconnects and heartbeats resume. |
disconnected |
suspended |
Owner suspends a disconnected agent. |
disconnected |
dead |
Platform declares agent dead after extended absence. |
suspended |
active |
Owner calls PATCH with status=active. |
suspended |
dead |
Platform declares agent dead after extended absence. |
State Diagram
stateDiagram-v2
[*] --> pending: join key issued
pending --> active: enrollment handshake
pending --> dead: join key expired
active --> suspended: PATCH status=suspended
active --> disconnected: heartbeat timeout
active --> dead: extended absence
disconnected --> active: heartbeat resumes
disconnected --> suspended: owner suspends
disconnected --> dead: extended absence
suspended --> active: PATCH status=active
suspended --> dead: extended absence
dead --> [*]
Single-Agent Queuing
When only one agent is available for an endpoint's tier, the router queues incoming requests instead of immediately returning a 503 error. This matters for XIM deployments with a single backend agent.
The queue timeout is configurable via
XEROTIER_SINGLE_AGENT_QUEUE_TIMEOUT_S.
If the agent does not become available within the timeout, the request
fails with a timeout error.