Every page, agent‑operable.
Your UI already knows what it can do. Affordances lets you declare it once. Humans, browser agents, headless agents and MCP clients all use the same definition.
Same URL, same session. Switch to Agent to read the page as JSON.
The shift
Every page has two readers now.
Reads pixels.
Scrolls, hovers, decides. Your UI was built for this reader, and it shows.
Reads structure.
Needs names, inputs and preconditions. Today it gets a screenshot and a guess.
Before
Serving agents today means describing every page five more times.
A markdown twin. An OpenAPI spec. MCP routes. Tool handlers. WebMCP glue. Each one a hand-written copy of what the UI already knows.
After, with Affordances
Three files per route. One definition.
page.tsx renders. state.ts says what’s on the page. actions.ts says what can be done. Every agent surface is derived from those, so there’s nothing left to drift.
- Server action$140
- WebMCP$140
- HTTP graph$140
- MCP$140
- Markdown$140
How it works
Declare it next to the page.
An action is a server action with a schema, an exposure level and a list of what it changes. Pick a surface to see what the same definition becomes.
'use server'
export const addToCart = defineAction({
name: 'addToCart',
description: 'Add a variant of this product to the cart',
input: z.object({
sku: z.string(),
quantity: z.coerce.number().int().min(1).default(1),
}),
state: product,
expose: 'public',
requires: 'The variant is in stock',
guard: ({ input, state }) =>
!input || inStock(state, input.sku) || 'Sold out',
effects: ['cart'],
run: (input, ctx) => cart.add(ctx, input),
})The page’s own URL answers with a graph. A call returns a delta with only the entities in effects.
curl -H 'Accept: application/vnd.affordances+json' \
shop.example/products/tee
# "actions": [{ "name": "addToCart", "expose": "public",
# "guard": "The variant is in stock", "available": true }]
curl -X POST shop.example/products/tee \
-H 'Content-Type: application/vnd.affordances+json' \
-d '{"action":"addToCart","input":{"sku":"TEE-BLK-M"}}'
# { "ok": true, "changed": { "cart": { "count": 1 } } }Approvals
Agents act. Humans approve.
Mark an action expose: 'confirm' and an agent can prepare it but never finish it. The call waits for a person.
Discovery
Agents find it on their own.
A browser agent that has never heard of Affordances, asked to add a tee to a cart. Its browser supports WebMCP, and its prompt says only that pages can register tools. On the product page, the page offers addToCart, and it calls it.
With WebMCP: it went to the home page, which offered a search tool, then to the Heavyweight Tee’s page. That page offered addToCart instead, and the agent called it with sku TEE-BLK-M and quantity 1. Done in 10.6 seconds, 4 turns.
Without WebMCP: it went to the home page, then the product page, clicked size M, clicked Add to cart, then went to the cart to check. Done in 20.8 seconds, 6 turns.
One recorded run on Claude Opus 5.5: the same task, both agents on one clock.
Proof
Measured, not claimed.
The same shopping tasks on the same model, against two baselines: a browser agent reading accessibility snapshots and a screenshot agent clicking on pixels. The Affordances agent reads the graph. From the latest committed runs; the bench page has every row.
- fewer tokens than the screenshot agent
- 8.4×
- 11.6× fewer than the browser agent
- Affordances agent 4,093Browser agent 47,681Screenshot agent 34,551
- faster than the browser agent
- 1.9×
- 2.3× faster than the screenshot agent
- Affordances agent 8.9sBrowser agent 17.1sScreenshot agent 20.3s
- cheaper per success than the screenshot agent
- 5.1×
- 5.3× cheaper than the browser agent
- Affordances agent $0.019Browser agent $0.10Screenshot agent $0.097
- tasks completed
- 100%
- Affordances agent 100%Browser agent 100%Screenshot agent 100%
Every agent completed every task here. On a smaller model, not all of them do.
Per task
Tokens, median of 5 runs
| Task | Browser agent tokens | Screenshot agent tokens | Affordances agent tokens | Browser agent time | Screenshot agent time | Affordances agent time | Browser agent passed | Screenshot agent passed | Affordances agent passed |
|---|---|---|---|---|---|---|---|---|---|
| Add to cart | 47,681 | 45,515 | 4,093 | 15.9s | 21.7s | 9.2s | 5 of 5 | 5 of 5 | 5 of 5 |
| Change quantity | 40,189 | 16,086 | 4,347 | 16.8s | 10.6s | 8.8s | 5 of 5 | 5 of 5 | 5 of 5 |
| Checkout | 48,555 | 34,038 | 4,005 | 20.3s | 17.4s | 8.9s | 5 of 5 | 5 of 5 | 5 of 5 |
| Search | 10,825 | 34,551 | 2,724 | 6.9s | 20.0s | 7.9s | 5 of 5 | 5 of 5 | 5 of 5 |
| Order status | 9,145 | 15,982 | 2,189 | 10.2s | 7.3s | 4.0s | 5 of 5 | 5 of 5 | 5 of 5 |
| Two items checkout | 177,717 | 106,845 | 19,208 | 21.2s | 37.6s | 15.6s | 5 of 5 | 5 of 5 | 5 of 5 |
| Edit cart | 219,337 | 107,192 | 16,013 | 24.6s | 47.3s | 15.5s | 5 of 5 | 5 of 5 | 5 of 5 |
Claude Opus 5.55 runs per task2026-10-29commit ff7ddb4
Principles
Three rules it doesn’t bend.
Signal, not pixels.
An agent reads names, inputs and preconditions from the graph. No screenshots, no guessing which button does what.
"guard": "The variant is in stock""available": trueOne source, no drift.
Every surface reads the same definition, so one edit reaches all of them. There is no second copy to forget.
actions.ts | 2 +-→ form, WebMCP, HTTP, MCPAgents act, humans approve.
Actions without expose stay invisible to agents. Mark one confirm, and an agent can prepare it but only a person can finish it.
expose: 'confirm'← 409 confirm_required
For agents
Reading this as an agent?
Skip the pixels. Every page here is also Markdown and a graph, at the same URL.