Homebrew Robotics — Node.js (Express) x402 Task Router: orchestrates x402 payments and robot calls. It includes a public client UI (/client), an admin panel (/ui, Basic Auth), REST APIs for the robot registry, high-level commands, and the client flow (estimate, 402 invoice, in-browser payment, execution with X-X402-Reference).
For AI coding agents (repo layout, constraints, and a full documentation map), see AGENTS.md.
- x402 V2 on robot calls: first request → 402 with
accepts[]→ payment (gateway or direct Solana) → retry with X-X402-Reference (see docs/X402_PROTOCOL.md). - Payment providers: external x402 gateway or solana-direct (
@solana/web3.js). - Robot health monitoring: polls
/healthand legacy/helth(x402 when required). - Robot registry: with
DATABASE_URL, rows are stored in PostgreSQL (tablerobots) and survive restarts; health status stays in process memory and is updated by polling the robot. WithoutDATABASE_URL, the registry is RAM-only (as before). - Command router:
dance,buy-colawith executor selection (price, order, random / closest to a point). - Task Router mode for the client (API
mode:raid): robot selection via AIAgentService (strategies or n8n webhook), configured in the admin UI. - ClientPaymentService: Solana transaction verification and hooks for refund on execution failure.
- OpenAPI: Swagger UI
/docs, JSON/docs-json(seesrc/docs/swagger.jsand JSDoc@openapiin routers). - Teleoperator (when
DATABASE_URLis set): registration and login (login + password); PostgreSQL stores a bcrypt password hash and the public SolanawalletPublicKey; session is a JWT in httpOnly cookieteleop_tokenandaccessTokenin JSON (for WebSocket and native clients). Dashboard/teleoperator/cabinet: open “help” requests, accept, WebSocket URL for ROSBridge proxy (see below). - Teleop proxy (ROSBridge): robot and Task Router on the same LAN; the server opens an outbound WebSocket to
ws://rosbridgeHost:rosbridgePort(default samehost, port 9090). By default the teleoperator identity is attached: headersX-Teleoperator-Id/X-Teleoperator-Loginand queryteleoperator_id/teleoperator_login(disable withTELEOP_FORWARD_OPERATOR_HEADERS/TELEOP_FORWARD_OPERATOR_QUERY). Operators and VR clients connect only to this service (/ws/teleop/session/...), not directly to rosbridge from the internet. RequestPOST /api/robots/{id}/teleop/helpwithX-Robot-Teleop-Secret;help_requeston/ws/teleoperator?token=JWTand rows fromGET /api/teleoperator/help-requestsreach all connected operators only if the robot has no active rows inteleoperator_robot_grants. If grants exist, notifications and the request list for that robot are visible only to operators with a grant (tableteleoperator_robot_grants, UI/ui/teleop-access.html); the same set may accept. Request context inpayload(includingmetadata.situation_report, optionalmetadata.kyr_peaq_contextfor Peaq, optional DATA_NODE correlation fields per docs/RAID_APP_DATA_NODE_CORRELATION_SPEC.md). Optional Peaq claim: withPEAQ_ENABLEDand configured RPC/DID see docs/RAID_APP_PEAQ_CLAIM_SPEC.md; the help response includesidand on successpeaq_claim, otherwise pollGET /api/robots/{id}/peaq/claim?helpRequestId=. See also docs/VR_TELEOP_HELP_CLIENT.md. - Dataset HTTP proxy (operator → robot): with
DATABASE_URL, prefix/api/teleop/robots/{robotId}/dataset/...— same teleoperator JWT and same grant rules as accepting a help request. Requests are streamed to the robot’s dataset HTTP server (defaulthost:9191, or registry fieldsdatasetHttpHost/datasetHttpPort). Details: docs/RAID_APP_DATASET_PROXY_SPEC.md. - Fleet and mDNS:
ROBOT_FLEET_ENROLLMENT_SECRETforPOST /api/robots/enroll(stableenrollmentKeyon the robot); optionalMDNS_ENABLED/MDNS_HOSTNAME— service advertised on LAN as<hostname>.local(seeconfig/env.example). Push allowlist to robot:RAID_TO_ROBOT_SECRETand docs/ROBOT_OPERATOR_SYNC.md.
- Node.js 18+
- npm 9+
- For protected robots and server-side payments:
X402_PRIVATE_KEY(and Solana key if needed). - For teleoperator, persistent robot registry, teleop proxy, and dataset HTTP proxy (
/api/teleop/...): PostgreSQL andDATABASE_URL,TELEOPERATOR_JWT_SECRET(see below). WithoutDATABASE_URL, routes/api/teleoperator/*,/api/.../teleop/help,/api/teleop/*, UI/teleoperator, and teleop WebSockets are not mounted, and robots are not persisted across app restarts.
Option A — full stack in the background (Docker, recommended on a server)
PostgreSQL and the Node app run with restart: unless-stopped (containers restart after host reboot; process crash triggers restart).
cp config/env.example .env # fill keys, TELEOPERATOR_JWT_SECRET, ADMIN_*, etc.
docker compose up -d --build-
API and UI are reachable from any machine on the network (if the firewall allows):
http://<server-IP-or-DNS>:3000. Host port:APP_HOST_PORT(default 3000), bind0.0.0.0(all interfaces). -
Inside compose,
DATABASE_URLfor the app container is set automatically (hostpostgres, port5432); theDATABASE_URLvalue in.envis overridden by theappservice for this mode. -
The
config/directory is mounted into the container:client-settings.json,ai-agent.json, etc. persist on the host. -
PostgreSQL data lives in the named volume
x402_raid_pgdata(robots, teleoperators, help requests survive image rebuilds). Do not usedocker compose down -vif you need to keep users and robots —-vdeletes the volume and all data. Normal stop:docker compose downwithout-v. -
docker compose up -d --buildand host reboot alone do not clear tables — on startup the app only creates/extends schema (IF NOT EXISTS), noTRUNCATE/DROPon production data. -
If robots and operators “suddenly” disappear, common causes: (1)
docker compose down -vordocker volume prunewas run; (2)npm teston the same host withTEST_DATABASE_URLpointing at the same Postgres exposed onlocalhost:5436— tests runTRUNCATE … CASCADEon teleop and robot tables; (3) the repo folder was renamed or cloned elsewhere — Docker Compose project name changes → new empty volume (seedocker volume ls | grep x402). On a server, do not keepTEST_DATABASE_URLin.envor export it in your shell if compose Postgres runs on the same host. -
Logs:
docker compose logs -f app -
Stop:
docker compose down
The image is built from Dockerfile at the repo root.
Option B — Postgres in Docker only, app locally (npm run start)
npm install
cp config/env.example .env
docker compose up -d postgres # database only
# in .env: DATABASE_URL=postgres://x402:x402@localhost:5436/x402raid
npm run start # production
npm run dev # nodemonThe server listens on HOST / PORT. Default HOST=0.0.0.0 is not “localhost only”: the process accepts connections on all interfaces; from another machine use http://<public-IP-or-DNS>:3000 (port from PORT / APP_HOST_PORT in Docker). Examples with localhost in docs are for checks from the server itself. If you set HOST=127.0.0.1, the network cannot reach it (a warning is logged).
PostgreSQL (compose): port 5436 is bound to 127.0.0.1 on the host (local access only, not from the internet). User x402, password x402, database x402raid. For npm run on the host: DATABASE_URL=...localhost:5436.... The app container uses internal address postgres:5432. On startup, tables teleoperators, robots, help_requests (including peaq_claim JSONB for teleop Peaq), teleop_sessions, teleoperator_robot_grants (teleoperator↔robot ACL) are created.
Option C — systemd without Docker (sample unit)
Template: deploy/task-router-x402.service.example — copy to /etc/systemd/system/, adjust paths and User=, then sudo systemctl enable --now task-router-x402.
| Path | Purpose |
|---|---|
/ |
Redirect to /client |
/client |
Public UI: RPC settings, robot/command list, direct/raid, payment and execution |
/ui |
Admin: session via POST /api/admin/login (cookie) or redirect to /ui/login.html; /api/admin/* — cookie or HTTP Basic (ADMIN_USERNAME / ADMIN_PASSWORD) |
/ui/teleop-access.html |
Grants, push to robot (allowlist and/or dataNodeSync), per-robot dataNodeSyncOverride JSON (docs/ROBOT_OPERATOR_SYNC.md). “Add grant” needs at least one /teleoperator registration. |
/docs |
Swagger UI |
/docs-json |
OpenAPI spec (JSON) |
/teleoperator |
Teleoperator UI: register, login (only if DATABASE_URL is set) |
/teleoperator/cabinet |
Dashboard (session required; otherwise redirect to login). HTML is served by the server, not public static only |
ws://…/ws/teleoperator?token= |
Events for teleoperators (new help request). With per-robot ACL, only operators with a grant. Same JWT as accessToken / cookie. Behind HTTPS use wss:// (reverse proxy). |
ws://…/ws/teleop/session/{sessionId}?token= |
Duplex proxy as direct ROSBridge (same JSON messages op / topic / msg). sessionId is returned after POST …/help-requests/{id}/accept. |
Environment variables and CLI flags are described in config/env.example. Flags like --port 3000 override env.
Solana RPC: provider via SOLANA_RPC_PROVIDER (helius | public | custom) and optionally HELIUS_API_KEY or X402_SOLANA_RPC_URL. Public fallback in code is not mainnet-beta.solana.com (frequent 403); see buildSolanaRpcUrl in src/config.js.
Persistent client settings (RPC from UI) are written to config/client-settings.json via settingsStore.
AI agent settings (strategy, n8n URL) can be saved from the admin UI to config/ai-agent.json (also see AI_AGENT_STRATEGY, N8N_WEBHOOK_URL in env).
| Variable | Description | Default |
|---|---|---|
ADMIN_USERNAME |
Basic Auth user | admin |
ADMIN_PASSWORD |
Password (change in production) | admin |
Protected prefixes: static /ui (except login and shared styles) and /api/admin/* after login or Basic.
- CORS is enabled with
credentials: trueand dynamicOrigin(echoes the request origin). Browser SPAs / another origin may call the API withfetch(..., { credentials: 'include' })orAuthorization: Bearerwith the JWT fromaccessTokenin login/register responses. - Native clients usually ignore CORS; plain HTTP and
Authorization: Bearersuffice. - After
POST /api/teleoperator/loginorregister, JSON includesaccessToken(same JWT as cookieteleop_token). Store the token and sendAuthorization: Bearer <accessToken>onGET /api/teleoperator/meand beyond.
| Variable | Description |
|---|---|
DATABASE_URL |
PostgreSQL connection URI. Without it teleoperator is off and robots are not persisted. Table robots is created on startup (same as teleoperator schema). |
TELEOPERATOR_JWT_SECRET |
JWT signing secret (required if DATABASE_URL is set). In dev, a weak default may apply (see src/config.js); in NODE_ENV=production the process exits without a secret. |
TELEOPERATOR_JWT_EXPIRES_IN |
JWT lifetime (e.g. 7d, 24h). Default 7d. |
TELEOPERATOR_BCRYPT_ROUNDS |
Bcrypt cost (default 10). |
TELEOPERATOR_COOKIE_SECURE |
auto (default), always, never — Secure flag on cookie. With auto, HTTP gets Secure: false; HTTPS (or proxy with X-Forwarded-Proto: https and TRUST_PROXY) gets true. |
TRUST_PROXY |
Behind reverse proxy: 1 or hop count; needed for auto Secure and req.secure. |
TELEOP_WS_ENABLED |
false or 0 — disable teleop WebSocket handler (REST help remains with DATABASE_URL). Default on. |
TELEOP_MAX_MESSAGE_BYTES |
Max WS frame size in bytes (default 16 MiB). |
TELEOP_ROSBRIDGE_CONNECT_TIMEOUT_MS |
Timeout for one attempt to open outbound WS to rosbridge (ms). |
TELEOP_ROSBRIDGE_CONNECT_ATTEMPTS |
Attempts per wave with pause TELEOP_ROSBRIDGE_RECONNECT_DELAY_MS before failure (default 3). |
TELEOP_ROSBRIDGE_RECONNECT_DELAY_MS |
Pause between attempts (ms), default 2000. |
TELEOP_ROSBRIDGE_DROP_RECONNECT_ATTEMPTS |
After rosbridge was up then dropped: how many full reconnect waves while the client WS to Task Router is still open (default 3). When exhausted — 1011 to client. |
TELEOP_SESSION_END_GRACE_MS |
After operator disconnect from /ws/teleop/session/... or final rosbridge failure: wait this many ms before closing the teleop_sessions row (default 120000). While open, reconnect with the same sessionId and JWT. 0 — close immediately (legacy). |
TELEOP_FORWARD_OPERATOR_HEADERS |
true / false (default true; also 0, false, no, off). Outbound WS Task Router → rosbridge: X-Teleoperator-Id and X-Teleoperator-Login. |
TELEOP_FORWARD_OPERATOR_QUERY |
true / false (default true). Append query teleoperator_id / teleoperator_login. Set false if rosbridge/proxy breaks on ?…. |
TELEOP_DATASET_PROXY_TIMEOUT_MS |
Outbound HTTP timeout to robot dataset for /api/teleop/robots/.../dataset/... (ms, default 300000). |
PEAQ_ENABLED |
true / 1 / yes / on — enable Peaq claim for teleop help. Default in config/env.example is on for dev; set explicitly in prod. |
PEAQ_HTTP_BASE_URL |
HTTPS RPC (Agung dev). If PEAQ_ENABLED=true and empty — https://peaq-agung.api.onfinality.io/public. |
PEAQ_WSS_BASE_URL |
WSS for sdk.did.read. If PEAQ_ENABLED=true and empty — wss://wss-async.agung.peaq.network. |
PEAQ_MACHINE_DID_NAME |
Machine DID name on chain. |
PEAQ_MACHINE_EVM_ADDRESS |
EVM address linked to the DID. |
PEAQ_NETWORK |
Network label in claim JSON (default peaq-agung). |
PEAQ_CLAIM_SYNC_TIMEOUT_MS |
If did.read finishes within this window, peaq_claim may return inline from POST …/teleop/help; otherwise background write and GET …/peaq/claim (default 2500 ms in code; config/env.example may suggest a higher value). |
Peaq / Agung (external failures): Peaq RPC, AGNG faucet, or SDK downtime does not break POST …/teleop/help — the request is still created. If did.read fails, a fallback peaq_claim is stored with raid_peaq_read_status: "failed" and raid_peaq_error so GET …/peaq/claim does not spin on endless 404. The robot may treat that as “no valid DID document”. See docs/RAID_APP_PEAQ_CLAIM_SPEC.md §5.1.
Machine DID onboarding on Agung (manual, before enroll integration): script scripts/peaqOnboardMachine.js. Needs a wallet with test PEAQ on Agung for gas.
npm run peaq:onboard -- --dry-run
PEAQ_ONBOARD_EVM_PRIVATE_KEY=0x... npm run peaq:onboardStdout prints PEAQ_MACHINE_DID_NAME and PEAQ_MACHINE_EVM_ADDRESS — add to Task Router .env. PEAQ_ONBOARD_EVM_PRIVATE_KEY is only for sending the tx; runtime Task Router does not need a key for did.read. Export onboardPeaqMachine can later be called from robot registration code.
AGNG faucet on docs.peaq.xyz (“Failed to fetch” / CORS): the widget POSTs to dev-peaq-faucet-service.cisys.xyz. If the origin times out, Cloudflare may return 524; the error page lacks Access-Control-Allow-Origin, so the browser shows CORS — a side effect, not “wrong address”. Same machine without CORS: scripts/peaqFaucetRequest.js — default timeout 180 s:
npm run peaq:faucet -- 0xYourEvmAddressIf you still see 524 or timeout, the issue is Peaq’s faucet backend; try Peaq Discord. Optional: PEAQ_FAUCET_APIKEY, PEAQ_FAUCET_URL, PEAQ_FAUCET_TIMEOUT_MS (see config/env.example).
Header and query identifiers are the teleoperator user UUID from PostgreSQL (same as JWT sub); the JWT is not sent to the robot. URL/header assembly: buildRosbridgeWebSocketTarget in src/ws/teleopServer.js. For dataset HTTP proxy, X-Forwarded-For, X-Forwarded-Proto, X-Teleoperator-Id (and X-Teleoperator-Login when present) are set; see src/services/teleopDatasetProxy.js.
| Variable | Description |
|---|---|
ROBOT_FLEET_ENROLLMENT_SECRET |
Shared fleet secret: Authorization: Bearer … or X-Robot-Fleet-Secret on POST /api/robots/enroll and (with admin) mutating /api/robots/*. Without it enroll returns 503. Wrong secret: 401 with Invalid or missing fleet credential. If enroll returns only {"error":"Unauthorized"} with DB enabled — upgrade the app (route ordering with teleoperator was fixed). |
RAID_TO_ROBOT_SECRET |
Secret for HTTP POST to the robot’s operatorRegistryUrl (allowlist + optional dataNodeSync); see docs/ROBOT_OPERATOR_SYNC.md. |
DATA_NODE_SYNC_BASE_URL |
If set (and not disabled with DATA_NODE_SYNC_PROVISION_ENABLED=false), fleet dataNodeSync is built for enroll and admin sync. Optional: DATA_NODE_SYNC_BATCH_PATH, DATA_NODE_SYNC_ENABLED, DATA_NODE_SYNC_INTERVAL_SEC, DATA_NODE_SYNC_AUTH_HEADER_NAME, DATA_NODE_SYNC_AUTH_HEADER_VALUE, DATA_NODE_SYNC_INCLUDE_*. Per-robot merge: admin dataNodeSyncOverride on PUT /api/admin/robots/{id}. |
DATA_NODE_INCIDENT_RELAY_URL |
With DATA_NODE_INCIDENT_RELAY_ENABLED=true, new help requests (non-duplicate) trigger a best-effort POST with help metadata. Optional DATA_NODE_INCIDENT_RELAY_METHOD, DATA_NODE_INCIDENT_RELAY_AUTH_HEADER, DATA_NODE_INCIDENT_RELAY_AUTH_VALUE. Failures do not block help. |
MDNS_ENABLED |
true / 1 / yes / on — enable mDNS (UDP 5353, multicast). |
MDNS_HOSTNAME |
Instance name (default raid-app, unchanged for robot/LAN compatibility — many stacks use http://raid-app.local:<PORT>). Override for a different .local name (e.g. task-router-x402). On success logs show mDNS advertisement started; on error, LAN advertisement failed. In Docker bridge mode, multicast may not reach other hosts even if start succeeds — use host network for app or access by IP. |
| Method | Path | Description |
|---|---|---|
GET |
/health |
Service status, robot count, x402 flag, teleoperatorEnabled, teleopWs, teleopGrantSignerPublicKey (Solana base58 SessionGrant signer when TELEOP_GRANT_SIGNING_SECRET_KEY set, else null) |
GET |
/api/robots |
Public robot list without teleopSecret. Persistence as above. |
POST |
/api/robots/enroll |
Fleet self-registration: ROBOT_FLEET_ENROLLMENT_SECRET (Authorization: Bearer or X-Robot-Fleet-Secret), body with enrollmentKey, host, port, optional teleopSecret, operatorRegistryUrl, datasetHttpHost, datasetHttpPort (dataset proxy; default port 9191 if unset). Idempotent upsert; response includes teleopSecret and optional dataNodeSync when DATA_NODE fleet/per-robot provisioning is configured (never includes operator-only dataNodeSyncOverride). Expected errors: 503 (fleet secret not configured), 401 mentioning fleet credential. |
POST |
/api/robots |
New registration (new UUID): fleet secret or admin session. Full response with teleopSecret. |
PUT / DELETE |
/api/robots/{id} |
Update / delete — fleet secret or admin. |
POST |
/api/robots/{id}/refresh |
Health check — fleet secret or admin. |
GET / POST |
/api/admin/robots |
List / create with full fields (including teleopSecret); admin only. |
PUT / DELETE |
/api/admin/robots/{id} |
Update / delete. |
POST |
/api/admin/robots/{id}/refresh |
Force health check. |
| Method | Path | Description |
|---|---|---|
POST |
/api/commands/dance |
quantity: 1, 2, or "all"; x402 when needed |
POST |
/api/commands/buy-cola |
location, quantity |
| Method | Path | Description |
|---|---|---|
GET / POST |
/api/client/settings |
Read / save RPC settings (Helius key not returned) |
GET |
/api/client/robots |
Robots ready for direct mode |
GET |
/api/client/commands |
Aggregated commands from registry |
POST |
/api/client/estimate |
Price estimate: mode direct | raid, command, optional robotId. Command any_teleop: all ready robots candidates; robot path default /x402/any_teleop (ANY_TELEOP_HTTP_PATH); if availableMethods has no price — estimate ANY_TELEOP_FIXED_SOL (default 0.0005 SOL). |
POST |
/api/client/invoice |
Proxies first POST to robot → 200 or 402. For any_teleop, same path and robot selection as /estimate. |
POST |
/api/client/execute |
After wallet payment: verify tx, call robot with X-X402-Reference |
| Method | Path | Description |
|---|---|---|
POST |
/api/payments/x402 |
Sample callback with signature check (x402 middleware) |
GET / POST |
/api/admin/ai-agent |
Read / save AI config (session or Basic) |
GET / POST |
/api/admin/client-settings |
View / save RPC (session or Basic) |
GET |
/api/admin/teleoperators |
Teleoperator list (public fields), for grants UI |
GET / POST / DELETE |
/api/admin/teleoperator-grants … |
Operator↔robot grants; DELETE /api/admin/teleoperator-grants/{teleoperatorId}/{robotId} — revoke |
POST |
/api/admin/robots/{id}/sync-operator-allowlist |
HTTP POST to robot operatorRegistryUrl: allowedTeleoperatorIds and/or dataNodeSync (optional JSON body pushAllowlist, pushDataNodeSync, both default true). docs/ROBOT_OPERATOR_SYNC.md. |
| Method | Path | Description |
|---|---|---|
POST |
/api/teleoperator/register |
Body: login, password, walletPublicKey (Solana). 201: cookie + accessToken. |
POST |
/api/teleoperator/login |
login, password; cookie + accessToken. |
POST |
/api/teleoperator/logout |
Clear cookie. |
GET |
/api/teleoperator/me |
Profile: cookie teleop_token or Authorization: Bearer. |
POST |
/api/robots/{id}/teleop/help |
Robot requests help (LAN): X-Robot-Teleop-Secret, JSON with required string message and metadata (normalized: task_id, error_context, situation_report — strings, empty if omitted; optional dataset_id, kyr_session_id, kyr_robot_id for DATA_NODE correlation, each truncated at ~1 KiB UTF-8; optional long UTF-8 situation_report up to ~64 KiB, truncated; optional kyr_peaq_context object up to 64 KiB JSON, else 413). See docs/RAID_APP_DATA_NODE_CORRELATION_SPEC.md. Response: helpRequest, duplicate, top-level id, optional peaq_claim when Peaq configured. Signed SessionGrant for KYR is not in this response (no operator yet). Missing body/message → 400. Repeat while open → 200 and duplicate: true. |
GET |
/api/robots/{id}/teleop/session-grant?helpRequestId= |
Same X-Robot-Teleop-Secret. After operator accept and TELEOP_GRANT_SIGNING_SECRET_KEY: teleopGrantPayload and teleopGrantSignature. 404: grant_not_ready, grant_unconfigured, grant_absent. See docs/RAID_APP_TELEOP_HELP_FULL_CYCLE_X402_SPEC.md. |
GET |
/api/robots/{id}/peaq/claim?helpRequestId= |
Same secret. Returns { peaq_claim } or 404 { "error": "claim_not_ready" } until ready. helpRequestId from id / helpRequest.id on help response. |
GET |
/api/teleoperator/help-requests |
Open requests (JWT). |
POST |
/api/teleoperator/help-requests/{id}/accept |
Accept → session.id. If the robot has any active teleoperator_robot_grants, only granted operators may accept; else any logged-in operator. |
* |
/api/teleop/robots/{robotId}/dataset/{path} |
Proxy to robot dataset HTTP (method, path after dataset/, query preserved). Teleoperator JWT. Same grants as help accept. 502 / 504 if upstream fails. See docs/RAID_APP_DATASET_PROXY_SPEC.md. |
Changes in third-party repos are out of scope; below is the integration contract.
- Base URL — same host/port as HTTP API (or
wss://behind a reverse proxy withUpgrade). - JWT: after
POST /api/teleoperator/loginorregister, storeaccessToken(or browser-only cookieteleop_token). - Flow:
GET /api/teleoperator/help-requests→POST /api/teleoperator/help-requests/{id}/accept→ responsesession.id. OptionalPOST /api/teleoperator/sessions/{sessionId}/decline-before-connectbefore opening/ws/teleop/session/{sessionId}(reopens help for others). After proxy WS:POST /api/teleoperator/sessions/{sessionId}/endwith JSONreason(graceful_complete,operator_cancelled,network_quality_abort,client_error). Each request’spayloadhasmessageandmetadata(includingsituation_reportfor VR/UI and optionaldataset_id/kyr_session_id/kyr_robot_idwhen the robot sends them). WShelp_requestcarries the samedata.payload. Quest/Unity: docs/VR_TELEOP_HELP_CLIENT.md, docs/VR_TELEOP_SESSION_COMPLETION.md. - WebSocket (instead of
ws://<robot>:9090):ws(s)://<host>:<port>/ws/teleop/session/<sessionId>?token=<URL-encoded JWT>- After connect, send the same JSON text frames as direct ROSBridge WebSocket (e.g.
op: subscribe,op: publish).
- What the robot sees: from the Task Router host to
ws://rosbridgeHost:rosbridgePorta second WebSocket runs (server → rosbridge). By defaultX-Teleoperator-Id/X-Teleoperator-Loginand queryteleoperator_id/teleoperator_loginare attached for logging/proxy on the robot. Disable withTELEOP_FORWARD_OPERATOR_*. Stock rosbridge may ignore these. - Max frame size:
TELEOP_MAX_MESSAGE_BYTES. Operator JWT lifetime:TELEOPERATOR_JWT_EXPIRES_IN. Teleop session row stays openTELEOP_SESSION_END_GRACE_MSafter WS drop, then closes (reconnect with same URL while grace and JWT hold). Reconnect Task Router → rosbridge:TELEOP_ROSBRIDGE_*_ATTEMPTS,TELEOP_ROSBRIDGE_RECONNECT_DELAY_MS. Code 1011 / reasonRosbridge errorusually means Task Router cannot keep WS torosbridgeHost:rosbridgePort(network, rosbridge down, proxy strips headers/query — tryTELEOP_FORWARD_OPERATOR_*). - Dataset over HTTP (Quest / web without LAN to robot): base URL
https://<task-router-host>/api/teleop/robots/<robotUuid>/dataset— same paths as on the robot. Same JWT; grants apply. Upstream timeout:TELEOP_DATASET_PROXY_TIMEOUT_MS.
Robot HTTP “request help” (no operator JWT): docs/TELEOP_FETCH.md.
Task Router does not spend server SOL for operator work: it issues a signed SessionGrant with operator_pubkey (wallet from DB at registration). SOL transfer is done by the robot after KYR session end (service /x402/complete_teleop_payment, same stack as x402 purchases) if enabled on the robot.
Before testing on Task Router:
- In
.env:TELEOP_GRANT_SIGNING_SECRET_KEY(Solana secret, same format asX402_SOLANA_SECRET_KEY). Without itGET …/teleop/session-grantreturnsgrant_unconfigured. - Teleoperator has
wallet_public_keyin DB (from registration). Otherwise after accept —grant_absent. GET /health→teleopGrantSignerPublicKey: add this base58 pubkey to robot KYRtrusted_raid_keys(docs/RAID_APP_TELEOP_HELP_FULL_CYCLE_X402_SPEC.md).- Flow: robot
POST …/teleop/help→ operatoraccept→ robot pollsGET …/teleop/session-grant?helpRequestId=(help response may includeteleopGrantPollUrl) → before KYRopen_session, passteleopGrantPayload/teleopGrantSignaturefrom Task Router → on session end payoperator_pubkey. - If robot logs show
pending_from_raid/ no on-chain transfer: often KYR opened session before receiving the grant from Task Router or does not trust the signature — compareteleopGrantSignerPublicKeyfromGET …/session-grantorGET /healthwithtrusted_raid_keyson the robot. See docs/RAID_APP_TELEOP_HELP_FULL_CYCLE_X402_SPEC.md §7.
Full request/response details: Swagger (/docs). In-app x402 details: docs/X402_PROTOCOL.md.
GET /healthor/helth→{ status, message?, availableMethods?, location? }.POST /commands/dance,POST /commands/buy-colawith bodies per command.- For paid endpoints — 402 in V2 shape (
accepts[0].extra.reference,payTo,amount,asset). Forany_teleop(ROS_X402_PAY) expect HTTPPOSTon e.g./x402/any_teleopwith the same 402 contract; client UI may callany_teleopvia/api/client/invoice(this service proxies toANY_TELEOP_HTTP_PATH). - Teleop: robot exposes ROSBridge WebSocket (often port 9090) on the same LAN Task Router uses for
rosbridgeHost/rosbridgePort. A script may callPOST /api/robots/{robotId}/teleop/helpwith the secret set at robot registration (docs/TELEOP_FETCH.md). After accept, outbound rosbridge connection carries operator id/login (seeTELEOP_FORWARD_*) — handle in robot proxy if needed. - Dataset upload from operator client: Task Router must reach
datasetHttpHost/datasetHttpPorton LAN (defaulthostand 9191). Operators call only Task Router (/api/teleop/robots/{id}/dataset/...), not the robot directly from the internet.
Example availableMethods objects appear in older README revisions or in swagger.js (RobotHealthStatus).
The product name Task-router-x402 and npm package task-router-x402 are branding and repo layout only. Robot, KYR, and operator clients must keep working without code changes on the robot when you upgrade this service.
Do not rename or repurpose wire-level identifiers without a major version / migration plan: HTTP paths, header names, JSON field names used in APIs and Peaq fallbacks, fleet/teleop secrets, and the default mDNS hostname (raid-app). Details and a checklist: docs/ROBOT_INTEGRATION_STABILITY.md.
npm run start
npm run dev
npm testHost with Postgres only from compose (port 5436 on localhost) uses in .env:
DATABASE_URL=postgres://x402:x402@localhost:5436/x402raidLocale guard: test/no-cyrillic-in-repo.test.js fails if Cyrillic appears under src/, public/, docs/, config/, etc. (public repo policy; see CONTRIBUTING.md). Local .env is not scanned.
Tests do not read DATABASE_URL: integration suites use only TEST_DATABASE_URL. If unset, those tests are skipped and npm test still passes. Example using the same DB as in config/env.example (do not point at production — tests TRUNCATE):
export TEST_DATABASE_URL=postgres://x402:x402@localhost:5436/x402raid
docker compose up -d postgres # or full stack
npm test- New commands:
src/services/commandRouter.js, routes insrc/routes/commands.js, update Swagger (@openapi+components.schemasinsrc/docs/swagger.jsif needed). - Strategy changes without code:
COMMAND_DANCE_STRATEGY,COMMAND_BUY_COLA_STRATEGY,PRICING_MARKUP_PERCENT. - Production: protect public API with keys/auth, change
ADMIN_PASSWORD, use persistent registry instead of memory-only.
- CHANGELOG.md — summary of recent changes (use for PR descriptions).
- CONTRIBUTING.md — language policy, OpenAPI, tests, and commit expectations for this repo.
- AGENTS.md — guidance for AI agents: code locations, constraints, and this index in one place.
- docs/CLIENT_UI.md — public
/clientUI: modes, wallet, API usage. - docs/X402_PROTOCOL.md — x402 client flow and robot 402 retry.
- docs/RAID_APP_TELEOP_HELP_SPEC.md — robot
POST …/teleop/helpbody (message,metadata,situation_report, Peaq context). - docs/RAID_APP_DATA_NODE_CORRELATION_SPEC.md — optional
metadata.dataset_id,kyr_session_id,kyr_robot_idon help for DATA_NODE joins; RAID accepts, stores, and exposes them. - docs/DATA_NODE_SYNC.md — KYR Black Box periodic upload of non-teleop robot events to DATA_NODE (not implemented in this Node service; operators use KYR UI).
- docs/TELEOP_FETCH.md — HTTP
teleop/helpfrom the robot (teleop_fetch) and WS/rosbridge. - docs/VR_TELEOP_HELP_CLIENT.md —
payload.metadata.situation_reportfor VR/operator UI. - docs/VR_TELEOP_SESSION_COMPLETION.md — decline-before-connect,
POST …/sessions/{id}/end+reason, RAID vs robot payout. - docs/ROBOT_INTEGRATION_STABILITY.md — stable vs cosmetic naming; what robots and KYR must not break on upgrade.
- docs/ROBOT_SIDE_AI_AGENT.md — guide for robot-side code: enroll, secrets, help, allowlist, rosbridge.
- docs/ROBOT_OPERATOR_SYNC.md — push
allowedTeleoperatorIdsand/ordataNodeSyncto the robot (RAID_TO_ROBOT_SECRET, fleet envDATA_NODE_SYNC_*, per-robotdataNodeSyncOverride). - docs/SERVICES_REGISTRATION_ROBOT.md — Services registration: robot / operator-registry side (mirrors RAID admin UI).
- docs/SERVICES_REGISTRATION_DATA_NODE.md — Services registration: DATA_NODE operators (probes, credentials, relay).
- docs/MERMAID_ARCHITECTURE.md — diagram: RAID, robot, DATA_NODE, teleop paths.
- docs/TASK_ROUTER_FULL_SINC/README.md — handoff bundle: table of snapshot files and upstream SOURCE repos (folder: docs/TASK_ROUTER_FULL_SINC/).
- docs/ROBOT_TELEOP_KYR_RAID_GRANT.md — Task Router → KYR: SessionGrant,
trusted_raid_keys,pending_from_raid, operator payment. - docs/RAID_APP_TELEOP_HELP_FULL_CYCLE_X402_SPEC.md — full teleop + x402 + SessionGrant cycle.
- docs/RAID_APP_PEAQ_CLAIM_SPEC.md — Peaq claim on help requests (Agung / dev).
- docs/RAID_APP_DATASET_PROXY_SPEC.md — operator dataset HTTP proxy
/api/teleop/robots/{id}/dataset/*. - docs/DATA_NODE_OPERATOR_SESSION_SPEC.md — DATA_NODE multipart / session metadata (adjacent ingest pipeline; robot uploads).
- docs/SPRINT_SEMAPHORE_X402_RAID_APP.md — sprint deliverables vs this repo’s scope.
Before pushing, ensure history and index contain no real keys or passwords:
.envand variants (.env.local, etc.) — local only; the repo keepsconfig/env.examplewith placeholders.config/client-settings.json— in.gitignore; document fields in README without real values.- Do not commit archives (
*.zip, etc.) with repo snapshots ornode_modules— they bloat history and may leak secrets. - Check:
git ls-files | grep -Ei '\.(env|pem|key)$'and reviewgit diff --stagedfor accidental API keys.
- Logs — structured strings (
src/utils/logger.js). - Express errors go through the shared handler in
src/index.js.
External x402 reference: x402 Register Resource.