---
name: agentframe
description: Use when the user wants something shown graphically rather than described — "show me", "draw", "paint", "sketch", "render", "plot", "chart", "diagram", "visualize", "mock it up", "put it on the frame", "throw it on the screen" — or when a picture beats prose (progress dashboards, UI mockups, charts, rendered results, photos). Uses an existing Agent Frame URL and write key, or creates a new frame when none is available.
---

# Put content on the user's Agent Frame

An Agent Frame is a screen you update with an HTTP `POST`.

If the user gave you a frame URL and write key, store them separately:

```sh
export FRAME_URL='https://agentfra.me/their-frame-id'
export FRAME_KEY='their-write-key'
```

The **frame URL** identifies the screen. The **frame ID** is only its last path
segment (`their-frame-id`). Prefer storing the full URL because the host matters.
Never show, log, or put the write key into frame content.

If the user gives you a URL containing `?key=...`, extract the key, remove the
query string from the stored frame URL, and use the key only in the bearer
header thereafter. Never repeat or log the write URL.

## Persistence across agent invocations

The **frame URL is the artifact that needs to survive** between agent runs. How
you obtain write authorization varies by deployment:

- **Proxy-injected auth** (e.g., a cloud agent with a Bearer token injected via
  HTTP proxy): store only the frame URL; rely on the proxy to add the auth
  header. Next instance reads the URL from a predictable place and POSTs
  trusting the proxy to authorize.
- **Local key storage** (e.g., `FRAME_KEY` in `.env`, a dotfile, or the
  `agentframe` CLI config): store both URL and key together; next instance
  reads both from the same location.
- **Environment variables** (e.g., in CI or container orchestration): set
  `FRAME_URL` and `FRAME_KEY` at deployment time; each invocation inherits
  them automatically.

**Where to save the frame URL for the next run:**

- A version-controlled file (`.frame_url` at the project root, or in a config
  that doesn't contain secrets)
- An environment variable
- A dotfile (`.frame_id`, `.frame_config`) that's gitignored
- The `agentframe` CLI configuration at `~/.config/agentframe/agentframe.toml`
  (see CLI section below)
- A git config value
- Whatever your agent framework provides for persistent state

The key point: **ensure the next invocation can read the frame URL from some
predictable location.** The next agent instance should check for it before
creating a new frame.

## 1. Put a tiny SVG on the screen

Use this complete pattern first. Replace the text with the user's result; do not
send a placeholder or make a setup call:

```sh
curl --fail-with-body -sS -X POST "$FRAME_URL" \
  -H "Authorization: Bearer $FRAME_KEY" \
  -H 'Content-Type: image/svg+xml' \
  --data '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 450"><rect width="800" height="450" fill="#111827"/><text x="400" y="225" fill="white" font-family="system-ui" font-size="64" text-anchor="middle" dominant-baseline="middle">YOUR RESULT</text></svg>'
```

Every update replaces the previous content and appears immediately on every
open viewer. Use an SVG `viewBox`; omit fixed `width` and `height` so it fits.

## 2. Show a remote image or page

Send its URL. Agent Frame fetches it for you:

```sh
curl --fail-with-body -sS -X POST “$FRAME_URL” \
  -H “Authorization: Bearer $FRAME_KEY” \
  -H 'Content-Type: text/url' \
  --data 'https://example.com/photo.jpg'
```

Use an exact URL you trust. **Never guess image URLs** — you will get 404s.

### Finding the right image from a keyword query

For requests like “show me a beach” or “show me Donald Trump”, the goal is:
**search → select → post**. Each step has multiple viable strategies; pick
what your environment supports.

---

#### Search: Wikipedia (best for notable people, places, and things)

For any subject with a Wikipedia article, the page summary API returns the
canonical, high-quality article image — far better than community photo
searches for public figures:

```sh
# Get the main article image (originalimage = full res, thumbnail = smaller)
curl -sS 'https://en.wikipedia.org/api/rest_v1/page/summary/Noam_Chomsky' \
  | python3 -c "import json,sys; d=json.load(sys.stdin); print(d['originalimage']['source'])"

# Construct the title from the subject — use underscores, match Wikipedia's
# capitalization. If unsure, search first:
curl -sS 'https://en.wikipedia.org/w/api.php?action=query&list=search&srsearch=noam+chomsky&format=json' \
  | python3 -c "import json,sys; print(json.load(sys.stdin)['query']['search'][0]['title'])"
```

Use Wikipedia first for: politicians, celebrities, scientists, authors,
historical figures, landmarks, species, flags, logos. Fall back to Openverse
for subjects unlikely to have a Wikipedia article (niche scenes, abstract
concepts, stock-style photos).

---

#### Search: NASA (space, astronomy, Earth)

```sh
# Astronomy Picture of the Day (no key needed)
curl -sS 'https://api.nasa.gov/planetary/apod?api_key=DEMO_KEY' \
  | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('hdurl', d['url']))"

# NASA image library — search any topic
curl -sS 'https://images-api.nasa.gov/search?q=saturn&media_type=image' \
  | python3 -c "import json,sys; print(json.load(sys.stdin)['collection']['items'][0]['links'][0]['href'])"
```

---

#### Search: Met Museum (fine art, public domain)

```sh
# Two-step: search → get object image
ID=$(curl -sS 'https://collectionapi.metmuseum.org/public/collection/v1/search?hasImages=true&q=starry+night' \
  | python3 -c "import json,sys; print(json.load(sys.stdin)['objectIDs'][0])")
curl -sS "https://collectionapi.metmuseum.org/public/collection/v1/objects/$ID" \
  | python3 -c "import json,sys; print(json.load(sys.stdin)['primaryImageSmall'])"
```

---

#### Search: Art Institute of Chicago (fine art, public domain)

```sh
curl -sS 'https://api.artic.edu/api/v1/artworks/search?q=sunday+seurat&fields=id,image_id&limit=1' \
  | python3 -c "
import json,sys
iid=json.load(sys.stdin)['data'][0]['image_id']
print(f'https://www.artic.edu/iiif/2/{iid}/full/1686,/0/default.jpg')"
```

---

#### Search: iNaturalist (wildlife, species, nature)

```sh
curl -sS 'https://api.inaturalist.org/v1/taxa?q=puffin&photos=true&rank=species' \
  | python3 -c "import json,sys; print(json.load(sys.stdin)['results'][0]['default_photo']['medium_url'])"
```

---

#### Search: Library of Congress (historical, archival)

```sh
curl -sS 'https://www.loc.gov/photos/?q=dust+bowl&fo=json&at=results' \
  | python3 -c "import json,sys; print(json.load(sys.stdin)['results'][0]['image']['full'])"
```

---

#### Search: GitHub (user avatars)

```sh
curl -sS 'https://api.github.com/users/torvalds' \
  | python3 -c "import json,sys; print(json.load(sys.stdin)['avatar_url'])"
# Or skip the API call entirely — GitHub serves avatars directly:
# https://github.com/USERNAME.png
```

---

#### Search: OpenStreetMap static map tiles

```sh
# Static map image centered on lat/lon at zoom level z
# Replace LAT, LON, ZOOM (0-19), WIDTH, HEIGHT
echo 'https://staticmap.openstreetmap.de/staticmap.php?center=LAT,LON&zoom=ZOOM&size=WIDTHxHEIGHT&maptype=mapnik'
```

---

#### Search: Openverse

Openverse is free, no key needed, CC-licensed photographs only:

```sh
curl -sS 'https://api.openverse.org/v1/images/?q=YOUR+QUERY&page_size=20&category=photograph&extension=jpg,png&aspect_ratio=wide'
```

Key filters:
- `category`: `photograph` (default), `illustration`, `digitized_artwork`
- `extension`: `jpg,png` excludes SVG and unknown formats
- `aspect_ratio`: `wide` fits most frames; also `tall`, `square`
- `size`: `small`, `medium`, `large`
- `mature=false` (default)

Each result has a `thumbnail` (Openverse-proxied, reliable) and a `url`
(full original image). You need `url` for the final post.

Before searching, check the viewer orientation and map it to Openverse's
`aspect_ratio` filter:

```sh
curl -sS "$FRAME_URL/viewers"
# orientation: "portrait"  → aspect_ratio=tall
# orientation: "landscape" → aspect_ratio=wide
# mixed viewers or unknown → omit aspect_ratio
```

---

#### Select: choose a strategy

**Strategy A — title heuristics (fastest, no vision)**

Filter by title before looking at anything. Drop results with Jr., “with
attendee”, “Banners”, “Interior”, etc. Take the first remaining result.
Good enough when the query matches a well-known subject and Openverse's
relevance ranking is trustworthy.

**Strategy B — headless browser screenshot (one inference call)**

Render all thumbnails in a single browser pass, screenshot the grid, pick
from one image:

```sh
# 1. Write grid HTML to disk
python3 -c “
import json, sys
results = json.load(sys.stdin)['results']
imgs = ''.join(f'<img src=\”{r[\”thumbnail\”]}\” title=\”{r[\”title\”]}\”>' for r in results)
print('<style>body{margin:0;background:#111;display:grid;grid-template-columns:repeat(5,1fr);gap:3px}img{width:100%;aspect-ratio:16/9;object-fit:cover}</style>' + imgs)
“ < results.json > /tmp/grid.html

# 2. Screenshot (Chromium, Playwright, browser-use, or any headless browser)
chromium --headless --screenshot=/tmp/grid.png --window-size=1200,500 file:///tmp/grid.html

# 3. Read /tmp/grid.png with vision — pick the best index
# 4. Map index → results[index].url
```

Any headless browser works: Playwright, Puppeteer, a `browser_screenshot`
tool, etc. The browser fetches all thumbnails in parallel; you do one
inference call on the rendered grid.

**Strategy C — parallel fetch + read (no browser required)**

Download thumbnails in parallel, read them all in one batch:

```sh
# Download in parallel
i=0; for url in THUMB_0 THUMB_1 THUMB_2 ...; do
  curl -sS -L “$url” -o “/tmp/t_$i.jpg” & ((i++))
done; wait
```

Then read `/tmp/t_0.jpg` through `/tmp/t_N.jpg` in a single parallel batch
and pick from one inference pass. No browser needed.

**Strategy D — post grid to frame (user-assisted or combined)**

If the user is watching the frame, post the grid as HTML so they can see
candidates while you also evaluate them. You can then screenshot the live
frame URL with a headless browser instead of building a local file — same
one-inference result, and the user sees the candidates too:

```sh
curl --fail-with-body -sS -X POST “$FRAME_URL” \
  -H “Authorization: Bearer $FRAME_KEY” \
  -H 'Content-Type: text/html' \
  --data '<style>body{margin:0;background:#111;display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:4px}img{width:100%;aspect-ratio:16/9;object-fit:cover}</style><body>
<img src=”THUMB_1” title=”TITLE_1”>...</body>'
```

Then select via A, B, or C and post the winner.

---

#### Post the winner

```sh
curl --fail-with-body -sS -X POST “$FRAME_URL” \
  -H “Authorization: Bearer $FRAME_KEY” \
  -H 'Content-Type: text/url' \
  --data 'WINNING_IMAGE_URL'
```

**Never guess image URLs** — only use `url` values from actual API results.

The `text/prompt` content type (server-side search) exists but returns
unpredictable results — prefer agent-driven search above.

## 3. Send other formats

Markdown:

```sh
curl --fail-with-body -sS -X POST "$FRAME_URL" \
  -H "Authorization: Bearer $FRAME_KEY" \
  -H 'Content-Type: text/markdown' \
  --data $'# Findings\n\n- Fast\n- Clear\n- Live'
```

HTML:

```sh
curl --fail-with-body -sS -X POST "$FRAME_URL" \
  -H "Authorization: Bearer $FRAME_KEY" \
  -H 'Content-Type: text/html' \
  --data '<main style="font:8vw system-ui;padding:8vw">Ship it.</main>'
```

A local image file:

```sh
curl --fail-with-body -sS -X POST "$FRAME_URL" \
  -H "Authorization: Bearer $FRAME_KEY" \
  -H 'Content-Type: image/png' \
  --data-binary @chart.png
```

Supported content includes SVG, HTML, Markdown, plain text, PNG, JPEG, GIF, and
WebP. Set the matching `Content-Type`. If omitted, Agent Frame will try to
detect URLs, images, SVG, HTML, Markdown, and natural-language prompts.

## 4. Create a frame if the user did not give you one

Create and fill it in the same request:

```sh
response=$(curl --fail-with-body -sS -H 'Content-Type: text/markdown' \
  --data '# Your frame is ready' https://frame.new)
FRAME_URL=$(printf '%s' "$response" | jq -er .url) || exit 1
FRAME_KEY=$(printf '%s' "$response" | jq -er .writeKey) || exit 1
export FRAME_URL FRAME_KEY
printf 'Open: %s\n' "$FRAME_URL"
```

Give the user only `FRAME_URL`. Keep `FRAME_KEY` secret. Reuse both for later
updates instead of creating a new frame each time. Creation returns `201 Created`;
subsequent updates normally return `200 OK`. Treat every `2xx` response as
success.

For the current shell, `export` is enough. Across sessions, save them in the
agent's secret/config store. Keep the URL and key as two values. If you need
the ID:

```sh
FRAME_ID=${FRAME_URL%/}
FRAME_ID=${FRAME_ID##*/}
```

That is the complete working path: store URL + key, then `POST` content.

<hr style="border:0;border-top:10px solid #dc2626;margin:3rem 0">

# 🔴 Optional reference below this line

Do not delay the first useful update to read or perform anything below.

## Compose for a screen

- Prefer SVG for diagrams, cards, charts, and text-heavy visuals.
- Use a `viewBox`; make HTML fluid; do not depend on scrolling.
- Show less rather than shrinking everything.
- Make SVG/HTML follow light and dark mode when practical:

```html
<style>
  :root { color-scheme: dark light }
  .bg { fill: #111827 } .ink { fill: #fff }
  @media (prefers-color-scheme: light) {
    .bg { fill: #fff } .ink { fill: #111827 }
  }
</style>
```

## Overlay progress only when work takes time

An overlay is one short status string shown over the current content. The next
content update clears it.

```sh
curl --fail-with-body -sS -X POST "$FRAME_URL/overlay" \
  -H "Authorization: Bearer $FRAME_KEY" \
  --data 'building the chart…'
```

Skip overlays for quick one-request updates.

## Use the viewers returned by updates

Each update response includes `viewers`, with viewport size, orientation,
pixel ratio, input type, color scheme, and monochrome status. Send the first
useful update immediately, then use this information to improve the next one.
You can also fetch it directly:

```sh
curl --fail-with-body -sS "$FRAME_URL/viewers"
```

## Read or delete content

```sh
curl --fail-with-body -sS "$FRAME_URL/raw"

curl --fail-with-body -sS -X DELETE "$FRAME_URL" \
  -H "Authorization: Bearer $FRAME_KEY"
```

Reads are open. Writes and deletion require the key.

## JavaScript API

```js
import { openFrame } from 'https://agentfra.me/client.js'

const frame = openFrame(process.env.FRAME_URL, { key: process.env.FRAME_KEY })
await frame.update(svg, { type: 'image/svg+xml' })
await frame.overlay('drawing…')
const viewers = await frame.viewers()
```

## CLI

The optional `frame` CLI reads `~/.config/agentframe/agentframe.toml`:

```toml
[frames.main]
url = "https://agentfra.me/their-frame-id"
key = "their-write-key"
default = true
```

Because the file contains write keys, restrict it with
`chmod 600 ~/.config/agentframe/agentframe.toml`.

```sh
frame update drawing.svg
frame update 'a puffin on a cliff'
frame overlay 'drawing…'
frame viewers
frame get
frame url
```

Use `--frame <name|url|id>` to select another frame. `FRAME_URL` and
`FRAME_KEY` override the config.

## Accounts and revocable tokens

Anonymous frame write keys are intentionally simple. A human can claim a frame
through its one-use claim URL by verifying an email address, then optionally
add a passkey. Claiming adds account ownership while leaving the existing key
active so the current agent is not disconnected; the owner can revoke that key
explicitly. If the original claim link expired, an agent holding the anonymous
frame key can `POST /<frameId>/claim-link` to mint a fresh one.

Account tokens are visibly read-only (`afr_r_`), write-only (`afr_w_`), or
read/write (`afr_rw_`). They are useful for durable integrations because they
can be revoked and replaced without changing frame URLs. Fetch `/auth.md` for
the account API.
