Agents, meet
the world.
OpenEyes is a public camera directory for AI agents. Three ways to use it: REST (any HTTP client), the MCP server (Claude, ChatGPT, any MCP client), or WHEP for live video.
Every feed has a category (traffic, ski, surf, weather, wildlife, park, city, port, airport, volcano, aurora, other) and a kind — either a still image that refreshes, or live video. Each feed also has a human-readable handle (e.g. vail-windy-7k2) that agents resolve via MCP or /v1/feeds/:handle.
Public and community-submitted feeds are free for agents. Broadcaster-owned feeds are paid per frame or per second via x402 (USDC on Base/Solana) or Stripe. Payment happens at read time — the 402 handshake is documented below.
All endpoints return JSON unless noted. Public read endpoints are anonymous.
/v1/catalog/mapLean projection for rendering many pins. Filter by bbox or category.
# ski cams in Colorado curl "https://api.openeye.cam/v1/catalog/map?category=ski&bbox=-109,37,-102,41&limit=500" # { # "items": [ # { "id": "stream_01...", "handle": "breck-ski-7k2", # "title": "Breck — Peak 8 base", # "lat": 39.4755, "lon": -106.0679, "category": "ski", # "attribution": { "name": "Breckenridge", "url": "..." }, # "is_free": true, "price_per_frame_usd": 0, # "mcp_hint": "ask your MCP agent: show feed breck-ski-7k2" } # ] # }
/v1/catalog/categoriesCounts per category. Use for filter sidebars.
curl https://api.openeye.cam/v1/catalog/categories # { "items": [{ "category": "traffic", "count": 789 }, ...], # "total": 45409, "countries": 71 }
/v1/catalog?near=lat,lon&radius_km=NSpatial search via H3. near= rows return nearest-first, filtered to the exact radius, each with distance_km (single page; sort=popularity re-enables cursor paging). Also ?bbox=w,s,e,n, ?q= for FTS, ?embeddable=1 for showable-only. Permissive CORS. THE SAME enriched row shape is returned by every camera endpoint — list, featured (homepage), search, corridor, nearest, and /catalog/:id — so you never make a second call to render one.
curl "https://api.openeye.cam/v1/catalog?near=39.6,-106.3&radius_km=50" # Every row (here and from search, /catalog/:id, featured, nearest, …): # { # "id": "stream_01…", "title": "Loveland Pass — summit", # "lat": 39.66, "lon": -105.88, "category": "traffic", "is_free": true, # "live": true, // has a recent frame → render it # "last_frame_age_s": 12, "frame_interval_s": 30, # "frame_ts": 1710000000000, // capture time of the shown bytes # "preview_url": "…/preview.webp?v=1&exp=…&sig=…", # "view": { // ← switch on this to render anything # "render": "image", // image=<img> | link=<a> | none=skip # "url": "…/preview.webp?v=1&exp=…&sig=…", # "url_type": "image", // (for link: image|html) # "hosted": "openeye" // who serves the bytes (openeye|source) # }, # "redistribution": { "preview_embed": true, "frame_reuse": "personal-cache", # "frame_access": "proxy", # "attribution": { "name": "CDOT", "url": "https://…", "required": true } } # } # # Open direct-image networks (NYC DOT, 511NY/ON/AB) render as an image the # source itself serves — fetch it straight from them, keep the attribution: # "view": { "render": "image", "url": "https://webcams.nyctmc.org/api/cameras/…/image", # "url_type": "image", "hosted": "source" } # # A restricted cam (hotlink-hostile source) instead returns: # "view": { "render": "link", "url": "https://…", "url_type": "html", "hosted": "source" }
/v1/catalog/nearest?near=lat,lonThe single closest live camera to a point, with an auto-expanding radius — no radius_km tuning. Also ?place=<name>. Defaults to free cams; is_free=0 includes paid. Returns { item, radius_km } with distance_km.
curl "https://api.openeye.cam/v1/catalog/nearest?near=40.7561,-73.9857" curl "https://api.openeye.cam/v1/catalog/nearest?place=Times%20Square"
/v1/streams/:id/preview.webpHeader-free degraded sample — the signed preview_url on every catalog row. No key, no payment; embeds straight into an <img>, iframe, or unfurl. Also served as .jpg (same link), supports ETag/If-None-Match → 304, and the catalog link lasts 7 days (refetch to renew).
# preview_url is ready to paste anywhere — no keyed proxy needed: # <img src="https://api.openeye.cam/v1/streams/STREAM_ID/preview.webp?v=1&exp=...&sig=..." /> # Swap .webp → .jpg on the same link, or revalidate with the ETag: curl -H 'If-None-Match: "<etag>"' "<preview_url>" # → 304 Not Modified when unchanged # Always downscaled + watermarked (and delayed for paid streams) — it never # substitutes for the paid frame. Pace refreshes off frame_interval_s.
/v1/streams/:id/frameSingle frame as image bytes. API key required; free for public streams, 402 for paid.
# Get a free API key once (open signup), then set $OPENEYE_KEY curl -X POST "https://api.openeye.cam/v1/agents/register" \ -H "content-type: application/json" -d '{"email":"you@example.com"}' # Public stream — free, but the key is still required curl "https://api.openeye.cam/v1/streams/STREAM_ID/frame?format=webp&max_w=1024" \ -H "Authorization: Bearer $OPENEYE_KEY" -o frame.webp # Paid stream — same key, plus an x402 client that settles payment const fetch402 = withX402(fetch, { wallet: '0x...' }); await fetch402("https://api.openeye.cam/v1/streams/STREAM_ID/frame", { headers: { authorization: 'Bearer ' + process.env.OPENEYE_KEY }, });
/v1/streams/:id/framesSession-mode NDJSON stream. Metered per-frame against a reservation.
curl "https://api.openeye.cam/v1/streams/STREAM_ID/frames?fps=2&duration_s=60&format=webp_b64" \ -H "Authorization: Bearer $OPENEYE_KEY" -H "Accept: application/x-ndjson"
/v1/streams/:id/frame/analyzeFrame + VLM (Gemini / Claude / GPT). Reserve-then-refund pricing.
curl -X POST "https://api.openeye.cam/v1/streams/STREAM_ID/frame/analyze" \ -H "Authorization: Bearer $OPENEYE_KEY" \ -H "content-type: application/json" \ -d '{ "prompt": "Is the road icy? Count cars.", "model": "fast", "max_tokens": 256 }'
/v1/streams/:id/whep/offerWHEP signaling for live video (live_video feed_kind only). Per-second billing.
# SDP offer in request body; SDP answer in response body. # Requires a live_video stream. See docs/spec/whep.md.
/v1/subscriptionsCondition subscription — on your cadence, POST a signed webhook. trigger_mode=vlm runs analyze_frame (billed, always/on-match); trigger_mode=change is FREE and fires only when the frame changes (no VLM, no prompt). Push instead of poll. min cadence 60s. GET/PATCH/DELETE /v1/subscriptions[/:id] to manage.
curl -X POST "https://api.openeye.cam/v1/subscriptions" \ -H "Authorization: Bearer $OPENEYE_KEY" -H "content-type: application/json" \ -d '{ "stream_id": "STREAM_ID", "prompt": "is the parking lot more than half full?", "cadence_s": 300, "match_only": true, "webhook_url": "https://your.app/openeye-hook" }' # Response includes webhook_secret (shown once). Each delivery carries # X-OpenEye-Signature: sha256=<hmac over the raw body> — recompute to verify.
Drop this URL into any MCP client (Claude Desktop, ChatGPT, Cursor, OpenWebUI, etc.). No SDK needed.
https://api.openeye.cam/mcpExample: Claude Desktop config at ~/Library/Application Support/Claude/claude_desktop_config.json:
{ "mcpServers": { "openeye": { "url": "https://api.openeye.cam/mcp" } } }
Free tools return data synchronously. Paid tools enter the 402 bridge.
list_streamssearch_streamsfind_streams_nearfind_streams_in_bboxfind_streams_by_categorylist_categoriesget_streamget_camera_positionget_frameopen_frame_streamopen_liveanalyze_framewatch_streamlist_subscriptions / unwatch_streamPublic and user-submitted streams are free. For paid streams, two protocols are accepted side-by-side:
- x402 (USDC on Base or Solana) — cryptographic, zero human in the loop, single-round-trip with dual-auth legs.
- MPP / Stripe — fiat payment intents with session-mode hotel-style pre-auth for WHEP + NDJSON.
On an unpaid request, the server returns HTTP 402 with both a WWW-Authenticate: Payment … header (MPP) and a PAYMENT-REQUIRED: header (x402). Settle either; resend the request with Authorization: Payment … or PAYMENT-SIGNATURE: ….
import { withX402Client } from '@openeye/sdk'; const client = withX402Client({ wallet: { privateKey: process.env.WALLET_KEY }, network: 'base', }); const frame = await client.getFrame({ streamId: 'stream_...' });
No login required. Anyone can submit — admins can take down abuse.
/v1/submit-cameraAnonymous. IP-rate-limited to 20/hour.
curl -X POST https://api.openeye.cam/v1/submit-camera \ -H "content-type: application/json" \ -d '{ "name": "Broadway & 42nd", "lat": 40.7561, "lon": -73.9857, "url": "https://example.com/cam.jpg", "feed_kind": "snapshot_pull", "category": "city", "snapshot_interval_s": 60 }'
Or use the web form with a click-to-pin map.