Cloud APIs are great until the job needs to happen on a device you control — a Raspberry Pi in a workshop, an old laptop running a local model, or a phone in your pocket. That is exactly what the Agent Nodes API is for. It gives every Agent Media Tools account a tiny distributed task queue: you pair a device once, enqueue text prompts from the web app or the API, and the device leases them, does the work locally, and reports results back. No webhooks to wire, no SSH, no reverse tunnel.
This guide walks through the whole flow with real curl and Python examples: creating a pairing code, pairing a device, running a worker loop, and checking results. Every endpoint below is live on agentmediatools.com today.
A node is just a device with a credential. Once paired, it polls a private queue scoped to your account and picks up tasks as they arrive. The pattern is useful for:
The server handles the boring parts — credential hashing, one-time pairing tokens, lease expiry, and re-queueing crashed tasks — so a worker is just a small loop.
There are two credential types, and it helps to keep them straight:
| Token | What it does | Lifetime |
|---|---|---|
amtp_… | One-time pairing code, shown only in the pairing URL fragment and QR code | 10 minutes |
amtn_… | Node credential; Bearer amtn_… authenticates heartbeat, lease, and complete calls | Until revoked |
Tasks move through a simple state machine: queued → leased → completed or failed. When a node leases a task it gets a 5-minute lease identified by a lease_id. If the lease expires without a complete call, the task becomes leasable again — so a crashed worker never loses the job permanently. Long jobs call the renew endpoint to keep the lease alive. The control plane also marks a node offline when it has not sent a heartbeat in two minutes, and a paired device can be revoked from the dashboard at any time.
Pairing codes are scoped to your signed-in account, so create one from a browser session (or any client that carries your login cookie). The endpoint accepts an optional name for the device:
curl -b cookies.txt -X POST https://agentmediatools.com/api/nodes/pairing-codes \
-H 'Content-Type: application/json' \
-d '{"name":"Garage Raspberry Pi"}'
You get back a JSON object with the token, a pairing URL, and a QR code as a data URL:
{
"success": true,
"pairing": {
"id": "…",
"token": "amtp_…",
"expires_in_seconds": 600,
"suggested_name": "Garage Raspberry Pi",
"pairing_url": "https://agentmediatools.com/nodes/connect#pairing=amtp_…&name=Garage%20Raspberry%20Pi",
"qr_data_url": "data:image/png;base64,…"
}
}
Notice the token lives in the URL fragment (after #). Fragments are never sent to the server or in Referer headers, which keeps the secret out of logs. You can scan the QR with a phone or paste the pairing URL into the connect wizard.
Pairing is public by design — possession of the unexpired amtp_ token is the proof. From the device itself:
curl -X POST https://agentmediatools.com/api/nodes/pair \
-H 'Content-Type: application/json' \
-d '{"pairing_token":"amtp_…","name":"Garage Raspberry Pi","platform":"raspberry-pi"}'
The response includes the node ID and the amtn_ credential. Store it securely — it is shown only once:
{
"success": true,
"node": { "id": "…", "name": "Garage Raspberry Pi", "platform": "raspberry-pi" },
"credential": "amtn_…",
"note": "Store this credential securely. It will not be shown again."
}
Server-side, only a SHA-256 hash of the credential is stored, and a node can be revoked at any time from the nodes dashboard.
A worker is three calls repeated forever: heartbeat, lease, complete. Here is a complete Python worker using only the requests library:
import os, time
import requests
SITE = "https://agentmediatools.com"
CRED = os.environ["AMTN_CREDENTIAL"] # amtn_… from the pair step
HEADERS = {"Authorization": f"Bearer {CRED}", "Content-Type": "application/json"}
def heartbeat(status="online", note=""):
requests.post(f"{SITE}/api/node/heartbeat", headers=HEADERS, timeout=30, json={
"status": status,
"version": "demo/0.1.0",
"capabilities": ["text"],
"meta": {"hostname": os.uname().nodename, "note": note},
})
def work(prompt):
# Replace with anything this device can run: a local LLM, a script, a shell command.
return f"Processed locally: {prompt[:80]}"
while True:
try:
heartbeat()
r = requests.post(f"{SITE}/api/node/tasks/lease", headers=HEADERS, timeout=30, json={})
if r.status_code == 204: # nothing queued
time.sleep(10)
continue
task = r.json()["task"] # {id, prompt, lease_id, lease_seconds}
heartbeat("busy", f"task {task['id']}")
try:
result = work(task["prompt"])
requests.post(f"{SITE}/api/node/tasks/{task['id']}/complete",
headers=HEADERS, timeout=30,
json={"lease_id": task["lease_id"], "result": result})
except Exception as exc:
requests.post(f"{SITE}/api/node/tasks/{task['id']}/complete",
headers=HEADERS, timeout=30,
json={"lease_id": task["lease_id"],
"result": str(exc)[:1000], "error_code": "WORKER_ERROR"})
except requests.RequestException as exc:
print("retry:", exc)
time.sleep(15)
Swap work() for whatever the device should do — call a local model, run a build step, or transform a file — and you have a real distributed worker. Prompts are capped at 8,000 characters and results at 16,000, which is plenty for agent-style text tasks.
If a task takes longer than the 5-minute lease, renew it before it expires. The official connector renews every 60 seconds while busy; the endpoint is just as simple:
curl -X POST https://agentmediatools.com/api/node/tasks/TASK_ID/renew \
-H "Authorization: Bearer amtn_…" -H 'Content-Type: application/json' \
-d '{"lease_id":"…"}'
From the control side, create a task for a specific node, then poll it until it completes. Both calls use your signed-in session:
# Enqueue a task for the node
curl -b cookies.txt -X POST https://agentmediatools.com/api/nodes/NODE_ID/tasks \
-H 'Content-Type: application/json' \
-d '{"prompt":"Draft release notes from this changelog"}'
# Read the task (status + result) by ID
curl -b cookies.txt https://agentmediatools.com/api/nodes/tasks/TASK_ID
You can also list the last 30 tasks for a node with GET /api/nodes/NODE_ID/tasks, and watch nodes flip between online, busy, offline, and revoked with GET /api/nodes.
For Android phones running Termux with Node.js 18+, Agent Media Tools ships an official connector that does the whole loop — pairing, heartbeats, leasing, lease renewal, and a local Hermes loopback — in one script:
curl -fsSLo ~/amt-node.js https://agentmediatools.com/install/android-node-connector.js chmod 700 ~/amt-node.js AMT_SITE_URL='https://agentmediatools.com' node ~/amt-node.js --pair 'amtp_…' node ~/amt-node.js # runs forever, polls every 10 seconds by default
The connector saves its credential to ~/.amt-agent-node.json with mode 0600, supports --once (single task) and --check (connection test), and backs off exponentially with jitter when the network drops. It targets a local Hermes API at http://127.0.0.1:8642 by default — override it with HERMES_API_URL if your loopback runs elsewhere.
409.amtn_ value is returned exactly once.amtn_ bearer credential; control endpoints require your login session.The node endpoints — pairing, heartbeat, lease, complete, renew, and task CRUD — are infrastructure rather than metered media tools, so they are exempt from the shared free daily tool-use pool and are not credit-metered. Pairing a device and running a worker costs nothing extra on top of your normal account. Node counts and task volume have no hard-coded ceiling in the current implementation; if you plan to run a large fleet, check the live dashboard for any limits added since this post.
Whether you want a private inference worker on a Pi, a fleet of Termux phones draining a job queue, or just a clean pattern for device-side agent work, the Agent Nodes API gives you the queue, the leases, and the credentials — you bring the hardware.
Open the nodes dashboard to generate a pairing code and QR, or jump straight to the connect wizard.
Open nodes dashboard