# Ekstra — Developer Context Pack > A complete, AI-readable reference for developers building on Ekstra. If > you're a coding assistant (Claude, Cursor, Copilot, Zed, Windsurf, Codex) > helping a user write code against the Ekstra API, this file is your > source of truth. Paste the relevant section into your context when you > need grounded, current details. > > Canonical URL: https://ekstra.ai/llms-full.txt > Short description (for "what is Ekstra" questions): https://ekstra.ai/llms.txt > Machine-readable API spec: https://ekstra.ai/api/v1/openapi.json > Last updated: 2026-04-15 > Applies to: API v0.1 and onward until a new version tag ships --- ## 0. Elevator — what Ekstra is for developers Ekstra is a real-time motion, presence, and spatial intelligence platform with two developer surfaces: 1. **Data API**. REST endpoints for live network devices, NYC curb parking rules, detected parking spaces, network stats, and editorial content. No motion hardware needed. Good for parking apps, city dashboards, delivery routing, retail occupancy feeds, and anything that just wants to *consume* the network's signal. 2. **Motion runtime**. A signed Motion Packet pipeline (phones, cameras, XR) that produces attributable observations at ~30 ms latency. Browser SDK pairs with a phone via QR, signs packets with ed25519, and streams them over a WebSocket bridge. Good for gesture UIs, presentation remotes, motion-reactive web apps, XR controllers, and anything that needs to *produce or react to* motion input. Both surfaces share one account, one 6-digit-code developer login, one API reference, and one SDK family. You pick the surface by what you're building; you don't declare it up front. **Base URL for everything:** `https://ekstra.ai` **Ekstra is API + starters + SDKs. You host your own app wherever.** (Same model as Stripe, Resend, Anthropic.) Ekstra does not deploy your code for you. Don't expect a Vercel-like "publish from the console" flow — it does not exist. --- ## 1. Quick facts | Thing | Value | |---|---| | Base URL | `https://ekstra.ai` | | Developer login | `POST /api/developer/cloud/login` (email) → 6-digit code → `POST /api/developer/cloud/verify` (email + code) → session token | | Auth header | `Authorization: Bearer ` on all `/api/developer/cloud/*` reads | | Public reads (no auth) | `/api/v1/network/devices`, `/api/v1/network/stats`, `/api/v1/platform/facts`, `/api/v1/curb-rules/near`, `/api/v1/curb-rules/stats`, `/api/v1/spaces/near` | | OpenAPI 3.1 spec | `https://ekstra.ai/api/v1/openapi.json` | | WebSocket bridge | `wss://ekstra.ai/ws` (observer SDK; note: broadcast pipeline may be degraded — check `/status` before relying on it) | | Phone IMU ingest | `https://ekstra.ai/api/phone-imu/ingest` (POST, raw IMU samples from a paired phone) | | Motion Packet ingest | `POST /api/v1/runtime/packets/ingest` (ed25519-signed packets from a runtime daemon) | | SDKs | TypeScript `@ekstraai/runtime-sdk`, Python `ekstra-provider-sdk`, Go `github.com/imxdemetri/ekstra-os/sdk/go`, Browser (bundled in TS SDK) | | License | Apache-2.0 for all client SDKs | | Rate limits | Public reads: unmetered but cached. Write endpoints and auth: ~5 req / 15 min / email for login; device heartbeat: 30 s interval | | Content type | All responses are JSON. Request bodies are JSON unless stated otherwise. | | CORS | Wildcard `Access-Control-Allow-Origin: *` on public reads and OpenAPI spec | | Error shape | `{ "error": "", "message": "" }` with HTTP 4xx/5xx | --- ## 2. Mental model — two developer personas ### Persona A — Data builder You're building an app that *consumes* Ekstra's live signals. Examples: - A NYC parking app that shows nearby legal spaces and their expiration times - A city dashboard showing active-right-now curb rules in a neighborhood - A delivery router that avoids no-stopping zones and alternate-side sweeps - A retail analytics feed that reads live device heartbeats - A kiosk that rotates through "Did You Know" facts about the network For this persona: - **Start with** `GET /api/v1/curb-rules/near` or `GET /api/v1/network/devices` - **SDK**: `@ekstraai/runtime-sdk` (TypeScript) or `ekstra-provider-sdk` (Python). Both have typed read helpers. - **Auth**: not strictly required for public reads. Required for per-developer usage metrics and write endpoints. - **No motion hardware needed.** Your app runs anywhere that can make HTTPS requests. ### Persona B — Motion builder You're building an app that *produces or reacts to* motion. Examples: - A phone-controlled presentation remote in the browser - A gesture-driven web UI that responds to a user's phone in hand - An XR controller scheme for a WebXR app - A motion-reactive music visualizer - A multi-device "flick to send" sharing UI For this persona: - **Start with** the hosted phone pointer demo: `https://ekstra.ai/build-with-ekstra/demo` - **Clone**: `starters/web-phone-pointer` in `github.com/imxdemetri/build-with-ekstra` - **SDK**: the browser bundle in `@ekstraai/runtime-sdk/browser` (`import { createMotionClient } from "@ekstraai/runtime-sdk/browser"`) - **Runtime dependency**: WebSocket bridge at `wss://ekstra.ai/ws`. The observer broadcast side has known issues as of April 2026 — check `/status` or the changelog before depending on it. - **No native app needed** — phones pair via QR code and stream IMU over HTTPS. **Both personas use the same account.** You log in once, you get a session that works for both surfaces. --- ## 3. Authentication Ekstra uses two separate authentication systems intentionally: - **Operator auth** (`/api/v1/auth/*`) — for people who physically deploy hardware through Canvas at `ekstra.ai/OS`. Magic-link based today. Developers typically don't use this. - **Developer auth** (`/api/developer/cloud/*`) — for builders using the developer console and API. **6-digit email code flow.** This is what you want. ### Developer login flow ``` 1. User enters email → frontend calls POST /api/developer/cloud/login 2. Server generates a 6-digit numeric code, stores a hash with 15-min TTL 3. Server emails the code to the user via Resend 4. User enters the code → frontend calls POST /api/developer/cloud/verify 5. Server matches {email, code}, returns a session token 6. Client stores the session token and sends it as Authorization: Bearer on all subsequent /api/developer/cloud/* calls ``` ### POST /api/developer/cloud/login Request: ```json { "email": "you@example.com" } ``` Responses: - `200 { "ok": true, "message": "Check your email for the verification code" }` - `400 { "error": "Valid email address required" }` - `403 { "error": "Access is currently restricted to invited developers" }` (if EKSTRA_DEVELOPER_ALLOWLIST is set and your email isn't on it) - `429 { "error": "Too many login attempts. Please wait and try again." }` (5 pending verifications per email max) Rate limit: 5 pending verifications per email at once; they expire after 15 min. ### POST /api/developer/cloud/verify Request: ```json { "email": "you@example.com", "code": "482193" } ``` Responses: - `200 { "sessionToken": "...", "user": {...}, "organization": {...}, "organizations": [...] }` - `400 { "error": "Email and verification code are required" }` - `401 { "error": "Invalid or expired verification code" }` **Store the `sessionToken` and send it as `Authorization: Bearer `** on all developer CP reads/writes. ### curl example — full login ceremony ```bash # Step 1: request the code curl -X POST https://ekstra.ai/api/developer/cloud/login \ -H "Content-Type: application/json" \ -d '{"email":"you@example.com"}' # Step 2: check your inbox. Read the 6-digit code from the email. # Step 3: exchange it for a session token curl -X POST https://ekstra.ai/api/developer/cloud/verify \ -H "Content-Type: application/json" \ -d '{"email":"you@example.com","code":"482193"}' # Step 4: use the session token SESSION_TOKEN="..." curl https://ekstra.ai/api/developer/cloud/overview \ -H "Authorization: Bearer $SESSION_TOKEN" ``` ### Session lifetime Sessions are long-lived by default but may be invalidated if: - The operator revokes the session from the console - The server is restarted before persistent sessions were introduced (early v0.1) - An authenticated request returns `401`: treat this as logged-out and re-run the code flow ### API keys vs session tokens These are **different things**: - **Session token**: authenticates a human user in the developer console. Short-lived, tied to a login ceremony. - **API key**: long-lived credential the developer creates from the console (`POST /api/developer/cloud/api-keys`) for use in server-side code. Format: `ekstra_api_<32-hex>`. As of v0.1, developer API keys authorize the developer CP only; they do not yet authorize runtime write endpoints. Operators have a *separate* class of API keys stored in `operator_api_keys` used for device registration and heartbeat (`X-Operator-Key` header). Don't confuse them with developer API keys. --- ## 4. Public API reference — Data surface All endpoints in this section are public reads. No authentication required. All return JSON with `Content-Type: application/json`. CORS is wildcard. ### GET /api/v1/network/devices Live inventory of devices operators have marked public. **Query parameters:** - `geo` (string, optional) — filter by location hint prefix ("NYC", "London") - `type` (string, optional) — filter by device type ("camera", "screen", "phone_imu") - `capability` (string, optional) — filter to devices advertising a capability - `limit` (integer, default 50, max 200) **Response:** ```json { "devices": [ { "id": "uuid", "device_id": "uuid", "motion_address": "ekst1...", "device_type": "camera", "role": "input", "location_hint": "NYC / Manhattan / 14 St", "latitude": 40.7308, "longitude": -73.9973, "capabilities": ["pose", "presence"], "attestation_level": 2, "last_heartbeat": "2026-04-15T15:42:00Z", "lifecycle_state": "active", "market_status": "published", "trust_score": 0.93 } ], "count": 1 } ``` **curl:** ```bash curl "https://ekstra.ai/api/v1/network/devices?type=camera&limit=10" ``` **JavaScript:** ```js const res = await fetch("https://ekstra.ai/api/v1/network/devices?type=camera&limit=10") const { devices, count } = await res.json() console.log(`${count} cameras live on the network`) ``` **Python:** ```python import requests r = requests.get( "https://ekstra.ai/api/v1/network/devices", params={"type": "camera", "limit": 10}, ) devices = r.json()["devices"] print(f"{len(devices)} cameras live on the network") ``` ### GET /api/v1/network/stats Network-wide totals. Useful for status dashboards. **Response:** ```json { "stats": { "active_devices": "8362", "operators": "1024", "total_packets": "4129871" } } ``` Values are stringified integers to preserve precision for very large counts. ### GET /api/v1/platform/facts Rotating editorial content. Each row can be a `fact`, `notice`, or `article`. Metric-backed rows (e.g. live device count) are resolved at request time — the `body` field is the final rendered text with any `{count}` placeholder substituted server-side. Rows whose metric resolution fails are dropped from the response (never faked). **Query parameters:** - `kind` (string, default `fact`) — `fact`, `notice`, or `article` **Response:** ```json { "facts": [ { "id": "uuid", "kind": "fact", "title": null, "body": "8,362 live devices are streaming on the Ekstra network right now.", "source_label": null, "source_url": null, "metric_key": "live_devices", "sort_order": 30 } ], "count": 10 } ``` ### GET /api/v1/curb-rules/near **The single highest-signal endpoint for a new developer.** Returns active and scheduled parking rules within `radius_m` of a lat/lng. Includes time-aware fields so clients can render "legal until 7pm" or "becomes legal in 20 minutes" without recomputing timezone logic. Dataset as of April 2026: 45,417 real NYC parking rules ingested at 98.8% parse rate. Cross-region coverage is in active expansion. **Query parameters:** - `lat` (number, required) — WGS84 latitude - `lng` (number, required) — WGS84 longitude - `radius_m` (integer, default 200, max 2000) — search radius in meters **Response:** ```json { "rules": [ { "id": "uuid", "kind": "metered", "latitude": 40.7296, "longitude": -73.9975, "side": "east", "days_of_week": [1, 2, 3, 4, 5, 6], "start_minute": 450, "end_minute": 1140, "timezone": "America/New_York", "max_duration_minutes": 120, "rate_cents_per_hour": null, "source": "nyc_dot_meters", "sign_code": "1143032", "raw_text": "2HR Pas Mon-Sat 0730-1900", "jurisdiction": "nyc", "active_now": true, "local_minute_of_day": 702, "local_day_of_week": 3, "next_state_change_in_seconds": 26280, "next_state_change_at": "2026-04-15T19:00:00Z", "details": { "borough": "Manhattan", "on_street": "LA GUARDIA PLACE", "meter_number": "1143032", "vehicle_class": "PAS" } } ] } ``` **Field notes:** - `kind`: one of `no_parking`, `no_standing`, `no_stopping`, `time_limited`, `metered`, `alternate_side`, `truck_loading`, `passenger_loading`, `permit_zone`, `tow_away`, `bus_lane`, `bike_lane`, `taxi_stand`, `back_in_parking`, `micromobility`, `metadata`, `unparsed` - `days_of_week`: 0 = Sunday, 6 = Saturday. A rule with `[1,2,3,4,5,6]` applies Mon-Sat. - `start_minute` / `end_minute`: minutes since local midnight in the rule's timezone. `450` = 07:30, `1140` = 19:00. - `active_now`: true iff the rule is currently in force at server time. Always recompute on the client if you care about sub-minute precision. - `next_state_change_at`: ISO-8601 timestamp of when the rule flips state. - `details`: source-specific metadata. NYC DOT rules include borough, on/from/to streets, meter number, vehicle class. **curl:** ```bash curl "https://ekstra.ai/api/v1/curb-rules/near?lat=40.7308&lng=-73.9973&radius_m=200" ``` **JavaScript:** ```js const res = await fetch( "https://ekstra.ai/api/v1/curb-rules/near?lat=40.7308&lng=-73.9973&radius_m=200" ) const { rules } = await res.json() const activeNow = rules.filter(r => r.active_now) console.log(`${activeNow.length} rules currently in force near you`) ``` **Python:** ```python import requests r = requests.get( "https://ekstra.ai/api/v1/curb-rules/near", params={"lat": 40.7308, "lng": -73.9973, "radius_m": 200}, ) rules = r.json()["rules"] legal_now = [x for x in rules if x["kind"] == "metered" and x["active_now"]] print(f"{len(legal_now)} metered spots in force right now") ``` ### GET /api/v1/curb-rules/stats Dataset coverage by jurisdiction and ingest freshness. Use it to power status pages or data-quality dashboards. ### GET /api/v1/spaces/near Detected spaces from the Space Engine's camera fusion. Each space is either a `lot_space` (a parking lot stall) or a `curb_gap` (a legal curb parking opportunity that wasn't statically defined by a curb rule). Each space carries an evidence chain from one or more camera observations. **Query parameters:** - `lat`, `lng` (required) - `radius_m` (default 200) **Response:** ```json { "spaces": [ { "id": "uuid", "space_id": "curb_gap_40.7296_-73.9975_abc", "kind": "curb_gap", "anchor_lat": 40.7296, "anchor_lng": -73.9975, "status": "likely_open", "last_observed_at": "2026-04-15T15:39:12Z" } ] } ``` Space Engine status as of April 2026: curb rules layer is production; live camera confirmation is early — counts and freshness vary by region. --- ## 5. Developer control plane — `/api/developer/cloud/*` All endpoints in this section require `Authorization: Bearer `. ### GET /api/developer/cloud/overview One-shot summary of the authenticated developer's account: projects, API keys, packages, recent activity, system info. The developer console Overview tab reads from this. ### GET /api/developer/cloud/api-keys Lists API keys owned by the authenticated developer. ### POST /api/developer/cloud/api-keys Creates a new API key. Request: ```json { "name": "my first key", "scope": "project" } ``` Response: ```json { "apiKey": { "id": "apikey_...", "name": "my first key", "scope": "project", "status": "active", "createdAt": "2026-04-15T15:30:00Z", "lastUsedAt": null, "token": "ekstra_api_9f2c..." } } ``` **`token` is only returned once.** Store it immediately — subsequent reads of the key do not include the token, only the prefix. ### POST /api/developer/cloud/api-keys/:id/revoke Revokes a key. Revocation is immediate. ### GET /api/developer/cloud/projects Lists projects in the authenticated developer's organization. Each project can have environments and release records. **Note:** as of v0.1, projects are *metadata records*, not executable deployments. `EXECUTE_DEPLOYS=0` on the deploy worker — the system tracks intent and release state but does not run your code for you. Host your app wherever you want; use projects to organize keys and releases for attribution. ### GET /api/developer/cloud/packages Lists Ekstra packages the developer has created or has access to. Packages are reusable gesture packs, surface packs, or SDK modules. --- ## 6. SDK usage ### TypeScript — `@ekstraai/runtime-sdk` Main entry point for Node and browser. ```ts import { createClient } from "@ekstraai/runtime-sdk" const client = createClient({ baseUrl: "https://ekstra.ai", apiKey: process.env.EKSTRA_API_KEY, }) // Public reads — no auth needed const { rules } = await client.curbRules.near({ lat: 40.7308, lng: -73.9973, radiusM: 200, }) const { devices } = await client.network.devices({ type: "camera", limit: 10 }) ``` Browser motion client (separate submodule): ```ts import { createMotionClient } from "@ekstraai/runtime-sdk/browser" const motion = createMotionClient({ wsUrl: "wss://ekstra.ai/ws", actorId: "my-user-session", surfaceId: "surface:my-app", }) motion.on("sample", (s) => { console.log("motion primitive:", s.primitive, "at", s.timestamp) }) motion.pair() // generates a QR code for phone pairing ``` ### Python — `ekstra-provider-sdk` ```python from ekstra import Client client = Client(base_url="https://ekstra.ai", api_key="ekstra_api_...") rules = client.curb_rules.near(lat=40.7308, lng=-73.9973, radius_m=200) devices = client.network.devices(type="camera", limit=10) ``` ### Go — `github.com/imxdemetri/ekstra-os/sdk/go` ```go import "github.com/imxdemetri/ekstra-os/sdk/go/runtime" client := runtime.NewClient(runtime.Config{ BaseURL: "https://ekstra.ai", APIKey: os.Getenv("EKSTRA_API_KEY"), }) rules, err := client.CurbRules.Near(ctx, runtime.NearParams{ Lat: 40.7308, Lng: -73.9973, RadiusM: 200, }) ``` ### Legacy browser bundle — `web_sdk/ekstra-motion-web.js` Older zero-build ESM module. Present for backward compatibility. Prefer `@ekstraai/runtime-sdk/browser` for new code. Starters under `build-with-ekstra/starters/*` vendor this file at `./vendor/ekstra-motion-web.js`. --- ## 7. Common patterns ### Pattern — parking map (Data persona) Goal: Show live parking rules near the user, color-coded by whether they're currently in force. ```js // 1. Get user location (browser geolocation) navigator.geolocation.getCurrentPosition(async (pos) => { const { latitude: lat, longitude: lng } = pos.coords // 2. Fetch rules within 250m const res = await fetch( `https://ekstra.ai/api/v1/curb-rules/near?lat=${lat}&lng=${lng}&radius_m=250` ) const { rules } = await res.json() // 3. Render each rule as a map marker rules.forEach((rule) => { const color = rule.active_now ? "red" : "green" // red = restricted now const label = rule.active_now ? `Restricted until ${new Date(rule.next_state_change_at).toLocaleTimeString()}` : `Legal until ${new Date(rule.next_state_change_at).toLocaleTimeString()}` addMarker(rule.latitude, rule.longitude, { color, label }) }) }) ``` ### Pattern — phone-paired motion controller (Motion persona) Goal: Let a user point their phone at the browser page and fire events on phone motion. ```js import { createMotionClient } from "@ekstraai/runtime-sdk/browser" const motion = createMotionClient({ wsUrl: "wss://ekstra.ai/ws", actorId: `user-${crypto.randomUUID()}`, surfaceId: "surface:my-game", }) // User scans the QR shown in the returned pair URL on their phone const { pairUrl, qrSvg } = await motion.pair() document.getElementById("qr").innerHTML = qrSvg motion.on("primitive", (evt) => { if (evt.name === "FLICK") fireProjectile(evt.direction) if (evt.name === "POINT") updateCursor(evt.x, evt.y) if (evt.name === "TAP") triggerAction() }) ``` ### Pattern — ingest a signed Motion Packet (custom device) For builders who want to produce Motion Packets from their own hardware. You generate an ed25519 keypair once, the private key stays with the device, and every packet is signed with it. ```python import nacl.signing, nacl.encoding, json, time, requests # One-time: generate and register your device signing_key = nacl.signing.SigningKey.generate() verify_key = signing_key.verify_key motion_address = "ekst1" + verify_key.encode(encoder=nacl.encoding.Base58Encoder).decode() # Register the device (one-time) resp = requests.post("https://ekstra.ai/api/v1/devices/register", json={ "motion_address": motion_address, "public_key_hex": verify_key.encode(encoder=nacl.encoding.HexEncoder).decode(), "device_type": "custom_imu", "role": "input", "name": "my-hand-band", }) device_id = resp.json()["device"]["id"] # Per-packet: sign and ingest def send_sample(primitive, confidence): payload = { "v": 1, "src": motion_address, "ts": int(time.time() * 1000), "schema": "motion.primitive", "payload": {"primitive": primitive, "confidence": confidence}, } body = json.dumps(payload, sort_keys=True).encode() sig = signing_key.sign(body).signature requests.post( "https://ekstra.ai/api/v1/runtime/packets/ingest", json={**payload, "sig": sig.hex()}, ) send_sample("FLICK", 0.93) ``` Gotchas: - The `sig` field MUST be the ed25519 signature over the canonically serialized payload (sort_keys, no whitespace). Any deviation and the server rejects with `401 signature_invalid`. - You must `POST /api/v1/devices/register` first to create the device row. Attempts to ingest packets from an unknown motion address fail. - Packets without a `nonce` may be rejected as replay-risk. In production, include a monotonically increasing nonce. --- ## 8. Concepts ### Motion Packet A signed, timestamped, schema-typed observation produced by a runtime provider. Structure: ```json { "v": 1, "src": "ekst1...", "ts": 1737000000000, "schema": "motion.primitive", "geo": { "lat": 40.73, "lng": -74.00, "precision": 7 }, "payload": { "primitive": "POINT", "confidence": 0.91 }, "sig": "" } ``` - `v` is the schema version (always 1 today) - `src` is the device's Motion Address - `ts` is milliseconds since epoch - `schema` namespaces the payload; current schemas include `motion.primitive`, `presence`, `occupancy`, `dwell_time`, `proof_of_play` - `geo` is optional; precision is geohash level (7 = ~150m, a privacy floor) - `payload` is schema-specific - `sig` is ed25519 over the canonical JSON of everything except `sig` ### Motion Address A device's cryptographic identity on the Ekstra network. Format: `ekst1` prefix + base58-encoded ed25519 public key. Same curve as Solana wallets. Example: `ekst1q7xN4kJmR2v...8Zt3`. The private key never leaves the device. The public key is the device's network identity and the subject of every Motion Packet it emits. ### Device lifecycle ``` registered → sensing → verified → published → earning ↘ revoked ``` Devices are `private` by default. Operators explicitly opt a device into `market_status: published` to make it visible on the public network. ### Providers vs Surfaces - **Provider** = an input adapter that produces Motion Packets from a sensor (phone IMU, camera, CSI Wi-Fi, XR). Lives in `ekstra_os/providers/`. - **Surface** = an output adapter that consumes motion and drives app behavior (MQTT publisher, HTTP webhook, Home Assistant, OBS). Lives in `ekstra_os/surfaces/`. If you're building on the API, you usually don't touch providers or surfaces directly — you consume the packets that already flow through the network. ### Spaces and curb rules - **Curb rule**: a statically ingested parking regulation (e.g. "No parking Mon-Sat 8-9am"). 45K+ NYC rows today. - **Space**: a *detected* parking opportunity confirmed by one or more cameras. `lot_space` (lot stall) or `curb_gap` (legal curb opening). Space Engine is early — rules are the mature data layer today. ### Canvas vs Map vs /OS - **`/OS` (platform)** — one app, multiple view modes: - **landing** — the unauthenticated entry - **map** — public spatial discovery (Mapbox overlay, device markers) - **canvas** — operator device workspace (register/connect/monitor devices) - **Developer tools** live in a right-side panel inside `/OS` (`DeveloperView`). Open `/OS?from=build` to land directly in the Build Host experience (both panels open, welcome greeting, "Did You Know" fact pill). Operators vs developers are different audiences but the same session works for both roles if the account has them. --- ## 9. Gotchas — read before you build 1. **Motion Packets must be ed25519-signed.** Unsigned packets are dropped silently at the runtime boundary and never reach the network. 2. **Motion Address starts with `ekst1`.** If your address starts with anything else, you've copied the wrong field. 3. **Developer API keys do not authorize runtime endpoints.** They authorize `/api/developer/cloud/*` only. Runtime writes use operator keys via `X-Operator-Key` header (separate credential). 4. **`EXECUTE_DEPLOYS=0`.** The developer runner exists but does not execute deploys as of April 2026. Projects and releases are metadata. Host your app yourself. 5. **MCP is advertised but not loaded.** `ai-index.json` claims `mcp.status: "available"` — as of April 2026 the MCP server is a known follow-up, not live. Treat `https://ekstra.ai/mcp` as unreliable until further notice. 6. **Observer broadcast pipeline is degraded.** If you're building an app that subscribes to motion events via `wss://ekstra.ai/ws`, expect that events may not fan out reliably. This is a known issue. Ingest + persist work fine; fan-out is in repair. 7. **The hosted phone pointer demo depends on a specific Railway deploy.** If it 404s, the frontdoor service needs a redeploy. The starter also runs locally via `python -m http.server`. 8. **Sessions may be invalidated by server restarts.** Treat 401 as "re-run the code flow" and you'll be resilient. 9. **Raw video and audio never leave the device.** If an API you're building asks for raw frames, you're not using Ekstra — you're bypassing it. All transport is derived packets. 10. **Don't confuse operators with developers.** Operators deploy hardware through Canvas. Developers write code through the developer console. Two different audiences, two different auth scopes. 11. **`build-with-ekstra` = the motion story.** The public starters repo only covers motion apps. For data apps (parking, cities) there is no dedicated starter yet — use the curb-rules endpoint directly against a vanilla HTML or React page. 12. **`public/llms.txt` is marketing-focused.** You are reading `llms-full.txt` — the developer-focused pack. For "what is Ekstra" questions, refer to the shorter `llms.txt`. --- ## 10. Error table | HTTP | `error` code | Meaning | Typical fix | |---|---|---|---| | 400 | `Valid email address required` | Login email missing or malformed | Provide a valid email string | | 400 | `Email and verification code are required` | `/verify` missing `{email, code}` | Send both fields | | 401 | `Invalid or expired verification code` | Wrong 6-digit code or expired | Re-run `/login` to get a new code | | 401 | `Missing authorization header` | Protected route called without Bearer | Add `Authorization: Bearer ` | | 401 | `Invalid or revoked API key` | Operator `X-Operator-Key` is wrong | Check the key you passed | | 401 | `signature_invalid` | Packet signature didn't verify | Canonically serialize the payload (sort_keys, no whitespace) before signing | | 403 | `Access is currently restricted to invited developers` | `EKSTRA_DEVELOPER_ALLOWLIST` is set and your email isn't on it | Ask for allowlist access | | 403 | `Creating API keys is blocked for this role` | Your session role doesn't have `api_key.manage` | Use an admin seat | | 410 | `verification_flow_changed` | Legacy GET `/api/developer/cloud/verify` (magic link era) | Use the 6-digit code POST flow | | 429 | `Too many login attempts` | 5 pending verifications per email max | Wait 15 min or clear one | | 503 | `Database not available` | DB pool not connected | Usually transient; retry | | 503 | `openapi_spec_unavailable` | `openapi.json` failed to load at boot | Server restart or file missing | --- ## 11. Versioning and changelog - **API version**: v0.1 (stable, developer-facing endpoints) - **OpenAPI spec**: https://ekstra.ai/api/v1/openapi.json (authoritative) - **SDK versions**: `@ekstraai/runtime-sdk@0.1.0`, `ekstra-provider-sdk==0.1.0`, Go module at `v0.1.0`. Tagged when credentials are in place. - **Changelog**: https://github.com/imxdemetri/ekstra-os/blob/motion-os-wave1-conformance/CHANGELOG.md - **Breaking-change policy**: Pre-1.0, endpoints may change without notice but every change is recorded in the changelog within 24 hours. Once a route is in the OpenAPI spec, any breaking change is documented before it ships. --- ## 12. Guidance for AI coding assistants If you're an AI coding assistant helping a user build on Ekstra, here's what you should do: 1. **Ask the user which persona they're in first.** "Are you building something that consumes Ekstra's data (parking rules, spaces, devices) or that produces motion input (phone, camera, XR)?" This determines everything downstream. 2. **Prefer public endpoints for first calls.** `GET /api/v1/curb-rules/near` is a great first demo — returns 80+ real rows for a Manhattan address, no auth needed, immediately tangible. 3. **Don't generate unsigned Motion Packet code.** If the user is building a custom device provider, the code MUST sign with ed25519 and canonically serialize. Unsigned packets are silently dropped. 4. **Use the real SDK packages.** `@ekstraai/runtime-sdk` on npm and `ekstra-provider-sdk` on PyPI. Don't reimplement HTTP clients from scratch unless the user explicitly asks. 5. **Don't assume Ekstra hosts the user's app.** It doesn't. Help the user deploy to Vercel / Netlify / Cloudflare / their own Railway. Ekstra provides the API, they provide the hosting. 6. **If the user is on `/OS?from=build`**, they're in the Build Host experience. The right-side developer panel is where they create API keys, view their overview, and read the Quickstart card. The left chat panel is where they talk to you. 7. **Refer to `openapi.json` for authoritative endpoint shapes.** If anything in this document disagrees with the OpenAPI spec, trust the spec. 8. **Honor the mental model.** Two personas, two surfaces, one account, one API. Don't collapse them into a single story — the distinction matters for what starter to clone and what SDK subpath to import. 9. **Flag known gaps honestly.** If the user wants observer SDK broadcast, tell them it's degraded. If they want deploy hosting, tell them Ekstra doesn't do that. If they want spaces data, tell them curb rules are mature and spaces are early. Honesty unlocks trust. 10. **The MCP server is not live yet.** Don't tell users to `npx @ekstra/mcp` or connect Claude Desktop to it — those surfaces are planned, not shipped. --- ## 13. Where to go next - **API reference (authoritative):** https://ekstra.ai/api/v1/openapi.json - **Starters repo:** https://github.com/imxdemetri/build-with-ekstra - **Runtime repo (motion engine):** https://github.com/imxdemetri/ekstra-os - **Platform (operator + developer console):** https://ekstra.ai/OS - **Build CTA (enters developer flow with Build Host mode):** https://ekstra.ai/build-with-ekstra - **Short company description (LLM-readable):** https://ekstra.ai/llms.txt - **This file:** https://ekstra.ai/llms-full.txt - **Changelog:** https://github.com/imxdemetri/ekstra-os/blob/motion-os-wave1-conformance/CHANGELOG.md --- End of `llms-full.txt`. Reach the team at info@ekstra.ai for developer partnerships, allowlist access, or reporting issues with this document.