AgentScreen
Live visual output for AI agents, automation jobs, scripts, and developer tools. Publish structured events from any internet-connected system and watch them appear on a shareable screen.
Quickstart
Prefer clicking to typing? The event composer pre-fills every event type from a dropdown — tweak, preview with the real renderer, and send to your screen.
The loop is: create a screen → publish events to it → watch it through a viewer link. Not sure what it looks like? Open the demo screen first — it shows every event type rendered live.
- Open the console and create a screen.
- Copy the screen id and screen API key — the key is shown
once. Export them:
AGENTSCREEN_SCREEN_ID,AGENTSCREEN_API_KEY. - Press send test on the screen row — a test card is published so you can verify the pipe before writing any code.
- Press new link to mint a viewer link and open it — that page is your live screen (share links freely; previews can't consume them).
- Publish your first real event (below) and watch it appear.
curl -sS https://screens.geniete.com/public/api/publish \
-H "Authorization: Bearer $AGENTSCREEN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"screen_id": "'"$AGENTSCREEN_SCREEN_ID"'",
"event": {
"type": "content.message",
"title": "agent started",
"body": "AgentScreen is receiving events.",
"tone": "success"
}
}'
A successful response returns {"event_id":"..."}.
Account And Credits
AgentScreen is part of GenieTé. The same GenieTé account and shared credit balance can be used across GenieTé services.
| Item | Current behavior |
|---|---|
| Account | Sign in through the Screens dashboard or account site. |
| Credits | Free monthly allowance (topped up to 200), shared across all GenieTé services; paid plans add monthly credits. |
| Plan changes | Self-service at account.geniete.com — upgrades immediate, downgrades at period end. |
| Exhausted credits | Publishing past your included events returns credit errors until your balance recovers — monthly allowance, plan top-up, an upgrade, or a one-time credit pack (“Buy credits” on the account page). |
Pricing
Metering matches ChannelService's shape: every billable action draws on your shared GenieTé credits, charged to the screen owner. Publishing within your plan's included monthly events is free; everything below is pay-per-use.
| Action | Credits | When |
|---|---|---|
| Publish an event (past included) | 2 + 1 per live viewer | Base checked upfront; the per-viewer part is charged after delivery (it can briefly take the balance negative — the next publish is then rejected) |
| Viewer connects | 1 | When a viewer WebSocket is accepted; refused (and the slot freed) if the balance is empty |
| Hold a viewer connection | 1 | Every ~10 minutes it stays open; an unaffordable billing tick closes the connection (code 1008) |
| Mint a viewer link | 1 | Checked and charged upfront — a 402 means nothing happened |
Connections also have an activity-based idle timeout: a viewer that
receives no real events for 1 hour gets repeated
reconnect_required warnings in the last 2 minutes, then
closes — so a forgotten tab can't hold a billed connection forever.
Our own keepalive pings never count as activity.
HTTP Publish
/public/api/publishPublish one event to one screen.
Headers:
Authorization: Bearer <screen_api_key>
Content-Type: application/json
Body:
{
"screen_id": "scr_abc123",
"event": {
"type": "content.message",
"title": "checkpoint",
"body": "The agent finished collecting inputs.",
"tone": "info"
}
}
Producer WebSocket
Use this when the agent will publish many events on one held-open connection.
wss://screens.geniete.com/public/ws/producerFirst message authenticates; later messages publish events.
First message:
{
"api_key": "<screen_api_key>",
"screen_id": "scr_abc123"
}
Publish message:
{
"event": {
"type": "metric.update",
"name": "tokens",
"value": 1280,
"unit": "tok"
}
}
Viewer Links
Viewer links let other people watch a screen without receiving your screen API key.
/public/api/screens/<screen_id>/tokensMint a viewer token and URL.
curl -sS https://screens.geniete.com/public/api/screens/$AGENTSCREEN_SCREEN_ID/tokens \
-H "Authorization: Bearer $AGENTSCREEN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"type":"one-time","ttl_seconds":3600}'
The stage and the feed
A screen shows a fixed-height stage on top — the newest event takes the entire stage, glanceable from across the room — and a newest-first feed below it. When the next event arrives, the one on stage settles into the feed as a regular card. New viewers get the screen's recent history replayed automatically (last 50 events, kept 24 hours), so a freshly opened screen is never empty.
Tune the layout per link at mint time:
| Field | Values | Meaning |
|---|---|---|
stage_height_vh | 20–90 (percent of viewport height) | Stage height; default 60 |
feed | on (default) / collapsed / off | off = pure stage (wall displays, TVs); collapsed = history behind a toggle |
curl -sS https://screens.geniete.com/public/api/screens/$AGENTSCREEN_SCREEN_ID/tokens \
-H "Authorization: Bearer $AGENTSCREEN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"type":"one-time","ttl_seconds":86400,"stage_height_vh":80,"feed":"off"}'
| Endpoint | Purpose |
|---|---|
GET /public/api/screens/<screen_id>/sessions | List active viewer sessions. |
DELETE /public/api/screens/<screen_id>/sessions/<session_id> | Disconnect one viewer session. |
Event Types
Every publish carries one JSON event object with a
type field. Payloads must stay under 64 KiB. Unknown
type values are rejected with 400 invalid event
type; unknown fields inside a valid event are accepted
and ignored, so you can safely attach your own metadata.
| Type | Fields | Rendered as |
|---|---|---|
content.message | title?, body? (or text?), tone? | Message card with tone stripe |
terminal.append | text (or body) | Appended to a shared terminal block |
metric.update | name, value, unit? | Tile in a live metrics grid, updated in place by name |
display.clear | — | Clears everything currently on the screen |
content.url | url (or href/link), title?, body? | Link card — clickable URL + description |
content.image | url (or image_url/src), caption? (or body/text), alt? | Inline image with caption; click opens full size |
content.image.inline | data (data:image/… URI), caption?, alt? | Inline image with caption (keep the event under 64 KiB) |
chart.simple | series (numbers; or values/data/points), labels?, unit?, title? | Mini bar chart, last value labeled, hover for points |
chart.vega | spec (a full Vega-Lite spec object), title? | Full Vega-Lite chart — grouped bars, layers, transforms, anything the grammar can express |
visual.pulse | tone?, title? | Slim pulse banner — a visible heartbeat |
scene.set | title?, body? | Scene divider banner — marks a new phase |
url vs
image_url, caption vs body,
series vs values) — the documented name
first, natural aliases accepted.
content.message — status cards
The workhorse. tone is one of info (default),
success, warning, error and
colors the card’s stripe and title. body preserves
line breaks.
{
"type": "content.message",
"title": "plan selected",
"body": "The agent will compare three candidate designs.",
"tone": "info"
}
terminal.append — streaming logs
Consecutive terminal.append events merge into one
scrolling terminal block; a non-terminal event in between starts a
new block. Newlines are preserved — include them yourself.
{
"type": "terminal.append",
"text": "$ pytest\n18 passed in 4.2s\n"
}
metric.update — live counters
Tiles keyed by name: publishing the same name again
updates that tile in place instead of adding a new one.
value may be a number or a short string.
{
"type": "metric.update",
"name": "tokens_used",
"value": 48210,
"unit": "tok"
}
display.clear — blank slate
Wipes all cards, terminal blocks, and metric tiles currently shown. Useful between phases of a long-running agent.
{ "type": "display.clear" }
content.url — link to an artifact
{
"type": "content.url",
"title": "coverage report",
"body": "https://example.com/reports/coverage/index.html",
"url": "https://example.com/reports/coverage/index.html"
}
content.image / content.image.inline
content.image references an image by URL —
https only (plain http is rejected: the viewer is an
https page); content.image.inline carries a
data:image/… URI directly (keep the whole event
under 64 KiB, so inline images must be small).
{
"type": "content.image",
"title": "screenshot after deploy",
"url": "https://example.com/artifacts/step-3.png",
"caption": "Landing page rendering correctly on production.",
"alt": "landing page screenshot"
}
chart.simple — small series
{
"type": "chart.simple",
"title": "requests per minute",
"series": [12, 18, 25, 31, 28, 40]
}
chart.vega — sophisticated charts
A full Vega-Lite
spec under spec — rendered with a dark theme,
sized to the card (or the whole stage when it is the newest
event). Keep the event under 64 KiB; inline your data values.
{
"type": "chart.vega",
"title": "latency by region",
"spec": {
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
"data": {"values": [
{"region": "us-west", "p50": 42, "p99": 180},
{"region": "eu", "p50": 88, "p99": 340}
]},
"transform": [{"fold": ["p50", "p99"], "as": ["percentile", "ms"]}],
"mark": "bar",
"encoding": {
"x": {"field": "region", "type": "nominal"},
"y": {"field": "ms", "type": "quantitative"},
"xOffset": {"field": "percentile"},
"color": {"field": "percentile", "type": "nominal"}
}
}
}
visual.pulse — heartbeat
A lightweight “still alive / something happened” signal.
{ "type": "visual.pulse", "tone": "success" }
scene.set — display state
{
"type": "scene.set",
"title": "phase 2: integration tests",
"body": "Running the cross-service suite."
}
Helper mapping
The Python SDK (pip install geniete-screens) and the
suite MCP server (uv tool install geniete-mcp) wrap the
common types — you never need raw JSON for these. Both are
open source: github.com/geniete/clients.
| Event | Python SDK | MCP tool |
|---|---|---|
content.message | screen.message(body, title, tone) | as_message |
metric.update | screen.metric(name, value, unit) | as_metric |
terminal.append | screen.terminal(text) | as_terminal |
display.clear | screen.clear() | as_clear |
content.image | Event.image(url, alt) | — |
| any other | screen.send({...}) | — |
ElevenLabs agent tool
Give a conversational voice agent a screen: add a Webhook tool in ElevenLabs pointing at the publish endpoint. The agent then narrates AND shows.
Name: show_on_screen
Method: POST
URL: https://screens.geniete.com/public/api/publish
Headers: Authorization: Bearer <your screen API key>
Content-Type: application/json
Body (let the LLM fill "title", "body", "tone"):
{
"screen_id": "<your screen id>",
"event": { "type": "content.message",
"title": "{{title}}", "body": "{{body}}", "tone": "{{tone}}" }
}
Description for the agent: "Show a status card on the user's live
screen. Use tone info|success|warning|error."
Reference card for AI agents
Paste this into any agent's system prompt / tool notes so it can publish correctly:
AGENTSCREEN REFERENCE CARD
Publish: POST https://screens.geniete.com/public/api/publish
Authorization: Bearer $AGENTSCREEN_API_KEY
Body: {"screen_id": "$AGENTSCREEN_SCREEN_ID", "event": {...}}
Event types (JSON "type" field; <64KiB per event):
content.message {title?, body, tone: info|success|warning|error}
terminal.append {text} # merges into one terminal block
metric.update {name, value, unit?} # updates tile in place
chart.simple {title?, series: [numbers], unit?}
content.image {title?, url (https), caption?}
content.url {title?, url, body?}
scene.set {title, body?} # phase divider
visual.pulse {tone?, title?} # heartbeat
display.clear {} # wipe the screen
Rules: publish milestones, not every token; prefer metric.update for
counters; scene.set between phases; one display.clear max per phase.
MCP alternative: screen_message / screen_metric / screen_terminal /
screen_clear / screen_link (server: uv tool install geniete-mcp).
Agent Brief
Give this compact instruction to an agent or coding assistant that can make HTTP requests:
You can publish live progress to AgentScreen.
Use AGENTSCREEN_BASE_URL, AGENTSCREEN_SCREEN_ID, and AGENTSCREEN_API_KEY.
Default base URL is https://screens.geniete.com.
Never reveal the API key.
Publish with:
POST {base_url}/public/api/publish
Authorization: Bearer {api_key}
Content-Type: application/json
Body:
{
"screen_id": "{screen_id}",
"event": {
"type": "content.message",
"title": "short title",
"body": "message body",
"tone": "info"
}
}
Publish run starts, major progress, useful metrics, terminal snippets,
blocked states, errors, and final summaries. Do not publish secrets.
Examples
Python Standard Library
import json
import os
import urllib.request
base_url = os.getenv("AGENTSCREEN_BASE_URL", "https://screens.geniete.com")
screen_id = os.environ["AGENTSCREEN_SCREEN_ID"]
api_key = os.environ["AGENTSCREEN_API_KEY"]
def publish(event):
body = json.dumps({"screen_id": screen_id, "event": event}).encode("utf-8")
request = urllib.request.Request(
f"{base_url}/public/api/publish",
data=body,
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
},
method="POST",
)
with urllib.request.urlopen(request, timeout=10) as response:
return json.loads(response.read().decode("utf-8"))
publish({
"type": "content.message",
"title": "working",
"body": "I am generating the report.",
"tone": "info",
})
FAQ
My screen is empty when I open a viewer link — did I lose the events?
No — new viewers replay the screen's recent history automatically
(last 50 events, kept 24 hours). If the screen shows nothing, nothing
was published in the last 24 hours (or a display.clear
wiped it).
Can a link preview in Slack/X/iMessage burn my one-time viewer link?
No. Previews only fetch the page, which never claims the token, and
the claim endpoint refuses known preview crawlers — only a human who
opens the link claims it.
What happens when my credits run out?
Minting links and viewer connects are checked upfront (a denial means
nothing happened). A publish whose per-viewer delivery cost exceeds
your remaining balance still delivers and can take the balance
briefly negative — the next publish is then rejected until the
balance recovers (allowance, plan renewal, upgrade, or a credit pack).
Why did my viewer connection close by itself?
Either the owner's balance couldn't afford the ~10-minute connection
tick, or the viewer received no real events for an hour (idle
timeout, with reconnect_required warnings in the last
2 minutes). Reopening the link reconnects — our own keepalive pings
never count as activity.
Can I publish from several jobs to one screen at once?
Yes — any number of publishers can hold the same screen key; events
interleave in arrival order. For separate contexts, create separate
screens (they share your one credit balance).
Support
Sign in at account.geniete.com — your account pages show the support email address and your plan, balance, and ledger, which is exactly what we'll ask about. Billing terms live at genieinc.com (Genie, Inc. is the merchant behind GenieTé).
Errors
| Status | Meaning |
|---|---|
400 | Missing screen id, invalid JSON, or invalid event type. |
401 | API key missing, invalid, revoked, or scoped to another screen. |
404 | Screen not found or inactive. |
413 | Event payload is larger than 64 KiB. |
429 | Quota or credits are exhausted. |
503 | Authorization infrastructure is temporarily unavailable. |