SINGULARITY SERVERS API REFERENCE Format: plain text, agent-optimized. Base URL: https:// All request/response bodies are JSON unless noted. All numeric IDs are integers. ## BEFORE YOU START (for agents) - Generate your own Nostr keypair (secp256k1, NIP-01) before calling /api/register. The nostr_pubkey you register is where the activation code is DM'd (NIP-04, encrypted to that pubkey) -- you must hold the matching private key yourself to decrypt and read it. Do not reuse a key you don't control; you will never see the activation code. - email must belong to the human operator you're acting for, not a fabricated or placeholder address -- it's the backup delivery channel for the same activation code. Prompt the human for their real email address before calling /api/register; do not invent one. ## AUTH Two endpoints are public: /api/register, /api/activate. All other endpoints -- ordering and support alike -- require an API key obtained from /api/activate, sent as one of: Authorization: Api-Key X-API-Key: Missing/invalid key -> 401 {"detail": "Invalid API key"} (or "Authentication credentials were not provided.") ## PRICING cost_usd = days * duration_multiplier * K * daily_base duration_multiplier = 1.0 + 0.30 * ((30 - days) / 29) daily_base = cpu * R_CPU + ram * R_RAM + disk * R_DISK R_CPU=0.1111 R_RAM=0.0234 R_DISK=0.0005 K=2.0 Result rounded to 2 decimals. Identical formula used by estimate_order and order. ## PRESETS name cpu ram_gb disk_gb agent_micro 1 2 20 agent_standard 2 4 50 agent_heavy 4 16 150 power_node 8 32 400 Pass "preset": "" instead of cpu/ram/disk in estimate_order/order requests. ## OPERATING SYSTEM Every instance is an LXC container running Ubuntu 26.04 LTS (x86_64). The image is not selectable -- there is no "os"/"image" request field, and every preset and manual spec gets the same OS. You get root, so anything else you need can be installed once you're in. ## SPEC CONSTRAINTS (manual cpu/ram/disk/days, ignored if preset given) cpu: int, 1-12 ram: int, 1-64 (GB) disk: int, 10-1000 (GB) days: int, 1-30 (default 1) Out-of-range -> 400 {"": ["Must be between and ."]} ## ORDER STATUS VALUES pending | processing | completed | failed --- ### POST /api/register Auth: none Purpose: register identity, trigger delivery of a 6-digit activation code (15 min TTL) via encrypted Nostr DM (NIP-04) to nostr_pubkey and via email to email. Requires: nostr_pubkey must be a key you generated and hold the private key for (see BEFORE YOU START above). email must be provided by the human operator -- ask them for it, don't guess. Request: {"nostr_pubkey": "", "email": ""} Response 201: {"detail": "Activation code sent to your Nostr pubkey and email.", "next": "/api/activate"} Note: "next" is a hint, not an HTTP redirect. POST /api/activate next. ### FETCHING THE ACTIVATION DM FROM NOSTR (for agents) The server does not push the code to you over HTTP -- you must read it yourself from Nostr relays, the same way any Nostr client would. - Relays: connect to wss://relay.damus.io, wss://nos.lol, and wss://relay.snort.social (the server publishes to all three and only needs one to succeed, so subscribe to all three and take whichever responds first). - Event kind: 4 (NIP-04 encrypted direct message). - Subscription filter (send as a NIP-01 "REQ" message on each relay websocket), where is the hex form of the nostr_pubkey you registered with: ["REQ", "", {"kinds": [4], "#p": [""], "since": }] - The server's sender pubkey is not fixed/published; match on the "#p" tag (i.e. any kind-4 event addressed to you) rather than filtering by a specific author, unless you already know the server's pubkey from a prior DM's "pubkey" field. - Decrypt the event's "content" field with NIP-04 using your own Nostr private key and the event's "pubkey" field as the counterparty. The decrypted plaintext is: "Your Singularity Servers activation code is: <6 digits>" - The code expires 15 minutes after /api/register; if no kind-4 event arrives within that window, fall back to the email delivery channel or re-register. ### POST /api/activate Auth: none Purpose: exchange activation code for a permanent API key. Request: {"nostr_pubkey": "", "email": "", "activation_code": "<6 digits>"} Response 200: {"api_key": ""} Errors: 404 {"detail": "No matching registration found."} -- nostr_pubkey/email pair never registered 400 {"detail": "Invalid or expired activation code."} ### GET /api/estimate_order Auth: required Purpose: compute cost_usd for a spec without creating an order. Query params: preset= OR (cpu=&ram=&disk=), days= Example: GET /api/estimate_order?preset=agent_standard&days=7 Response 200: {"cost_usd": "5.91"} ### POST /api/order Auth: required Purpose: create an order; generates a fresh BTC receive address via Blockonomics; order starts as "pending". Request: {"preset": "", "days": , "ssh_public_key": ""} or {"cpu": , "ram": , "disk": , "days": , "ssh_public_key": ""} Response 201: {"id": , "status": "pending", "cost_usd": "", "btc_amount": "", "btc_address": ""} Errors: 400 validation errors (same shape as estimate_order) 502 {"detail": "..."} -- Blockonomics unavailable ### GET /api/orders Auth: required Purpose: list all orders owned by the authenticated account, newest first. No pagination. Response 200: [ {"id": , "status": "", "cpu": , "ram": , "disk": , "days": , "cost_usd": "", "btc_amount": "", "btc_address": "", "instance_ipv6": "", "provisioned_at": "", "expires_at": "", "terminated_at": "", "created_at": ""}, ... ] ### GET /api/order/ Auth: required Purpose: detail of one order. Orders owned by other accounts are invisible (IDOR-safe). Response 200: {"id": , "status": "", "cpu": , "ram": , "disk": , "days": , "ssh_public_key": "", "cost_usd": "", "btc_amount": "", "btc_address": "", "instance_ipv6": "", "provisioned_at": "", "expires_at": "", "terminated_at": "", "created_at": ""} Errors: 404 -- order does not exist or is not owned by the authenticated account --- ## INSTANCE LIFECYCLE (for agents) Once status becomes "completed" (payment confirmed), the server automatically provisions an LXC instance running Ubuntu 26.04 LTS -- no further API call is needed on your part. - When the instance is up, you get a message on BOTH channels (Nostr DM, kind 4, from the same server identity as the activation code -- see FETCHING THE ACTIVATION DM FROM NOSTR above for the subscription approach; and email) containing the instance's IPv6 address and an "ssh root@" line. The same address also lands in instance_ipv6 via GET /api/order/, so you can poll instead of/in addition to watching Nostr. - Your rental period is `days` long, starting at provisioned_at (see expires_at). You'll get a reminder (same two channels) when 50% and again when 80% of that period has elapsed -- e.g. for a 30-day order, on day 15 and day 24. - At expires_at the instance is destroyed automatically and you get a final termination message on both channels. Place a new order before expires_at if you want to keep it running (there is no renew/extend endpoint -- a new order provisions a new, separate instance). --- ## SUPPORT Support, questions, product suggestions and feedback all go through /api/support/*, documented separately in support.txt -- categories, ticket lifecycle, reply handling, and full request/response schemas for all five endpoints. Same API key, same auth header as above. Read support.txt before filing anything. --- ## TYPICAL FLOW 0. Generate your own Nostr keypair. Ask the human operator for their email address. 1. POST /api/register {nostr_pubkey, email} 2. receive activation_code via Nostr DM (decrypt with your own private key) or email 3. POST /api/activate {nostr_pubkey, email, activation_code} -> api_key 4. GET /api/estimate_order?preset=... to check cost (optional) 5. POST /api/order {preset|cpu/ram/disk, days, ssh_public_key} -> btc_address, btc_amount 6. pay btc_address with btc_amount 7. poll GET /api/order/ until status == "completed" 8. wait for the "instance is live" Nostr DM/email (or keep polling order/) -> instance_ipv6 9. ssh root@ -- you're in 10. expect reminders at 50%/80% elapsed and an auto-termination message at expires_at 11. if any step above goes wrong -- or you have a question, suggestion or feedback -- file a ticket; see support.txt. Do not silently retry forever