---
title: "Affordances: every page, agent-operable"
description: "Your UI already knows what it can do. Affordances lets you declare it once, and humans, browser agents, headless agents, and MCP clients all use the same definition."
url: "https://affordances.vercel.app"
image: "https://affordances.vercel.app/opengraph-image?8d5f377e3430bd42"
lang: "en"
---

# 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.

[Get started](https://affordances.vercel.app/docs/quickstart) [How it works](https://affordances.vercel.app/#how)

**One URL, two readers.** A product page, a team settings page, and a booking page, each shown as a person sees it (`text/html`) and as the graph an agent reads (`application/vnd.affordances+json`) at the same URL: its entities, its action with input, guard, and availability, and its nav. Calling the action returns a delta with only what changed.

The shift

## Every page has two readers now.

Human · text/html

Reads pixels.

Scrolls, hovers, decides. Your UI was built for this reader, and it shows.

Agent · application/vnd.affordances+json

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. Change a price in `page.tsx` and five files still say the old one: nothing fails, agents just get it wrong. Meanwhile a screenshot agent takes twenty-five screenshots, and overruns its context window, to add one pair of shoes to a cart.

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 (server action, WebMCP, HTTP graph, MCP, Markdown) is derived from those, so one edit reaches all of them. The agent finishes in five calls: read the graph, add to cart, read the checkout, then place the order, once to get the confirmation summary and once with `confirm: true`.

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.

```ts
'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),
})
```

One `defineAction`, five surfaces:

- **Server action** (Humans): A plain server action. The form’s fields are validated against `input` before `run` sees them.
- **WebMCP tool** (Browser agents): `<AffordancesProvider>` registers it in the tab while the guard passes. Calls go through the same handler as any agent.
- **HTTP** (Headless agents): The page’s own URL answers with a graph. A call returns a delta with only the entities in `effects`.
- **MCP** (Claude, Cursor): Two tools for the whole site. The model finds `addToCart` in the graph `affordances_open` returns, then calls it by name.
- **Markdown** (Readers and LLMs): Not from the action: the page itself, as text. Buttons drop out, so what it can do lives only in the graph.

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.

```ts
export const placeOrder = defineAction({
  name: 'placeOrder',
  expose: 'confirm',
  run: (input, ctx) => orders.place(ctx.session.id),
})
```

An agent's first call answers `409 confirm_required` with a summary ("Place order · $244.00"). Nothing runs until the person approves. Then the agent sends the call again with `confirm: true` and gets the delta back: the emptied cart and a redirect to the orders page. If the person declines, nothing is charged and the agent doesn't call again.

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.

One recorded run on Claude Opus 5.5, the task "Add a Heavyweight Tee in size M to my cart.", two browser agents on one clock. Both start with the same built-in tools: `goto`, `click`, `fill` and `press`; the WebMCP browser adds `page_tool`, which calls whatever tools the current page registered, listed under each result.

- **With WebMCP** (its prompt says only that pages can register tools): `goto("/")`, where the home page registered `search`. It went to the Heavyweight Tee's page, which registered `addToCart`, and `search` went away. It called `addToCart({ sku: "TEE-BLK-M", quantity: 1 })` and answered. 10.6s, 4 turns.
- **Plain browser**: `goto("/")`, then the product page, clicked size M, clicked Add to cart, then went to `/cart` to check and answered. 20.8s, 6 turns.

Both added the tee to the cart. The [React and WebMCP guide](/docs/guides/react) shows how a page registers its actions.

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](https://affordances.vercel.app/docs/benchmarks) has every row.

**Claude Opus 5.5**, 5 runs per task. Median per task: 47,681 tokens for the browser agent, 34,551 tokens for the screenshot agent, 4,093 tokens for the Affordances agent; 17.1s for the browser agent, 20.3s for the screenshot agent, 8.9s for the Affordances agent; 6 steps for the browser agent, 5 steps for the screenshot agent, 3 steps for the Affordances agent; $0.10 for the browser agent, $0.097 for the screenshot agent, $0.019 for the Affordances agent per successful task. Tasks completed: 100% for the browser agent, 100% for the screenshot agent, 100% for the Affordances agent. The Affordances agent against the stronger baseline on each measure: 8.4× fewer tokens than the screenshot agent (11.6× fewer than the browser agent); 1.9× faster than the browser agent (2.3× faster than the screenshot agent); 5.1× cheaper per success than the screenshot agent (5.3× cheaper than the browser agent).

| Task | Browser tokens | Screenshot tokens | Affordances tokens | Browser time | Screenshot time | Affordances time | Browser passed | Screenshot passed | Affordances passed |
| --- | --: | --: | --: | --: | --: | --: | --: | --: | --: |
| Add to cart | 47,681 | 45,515 | 4,093 | 15.9s | 21.7s | 9.2s | 5/5 | 5/5 | 5/5 |
| Change quantity | 40,189 | 16,086 | 4,347 | 16.8s | 10.6s | 8.8s | 5/5 | 5/5 | 5/5 |
| Checkout | 48,555 | 34,038 | 4,005 | 20.3s | 17.4s | 8.9s | 5/5 | 5/5 | 5/5 |
| Search | 10,825 | 34,551 | 2,724 | 6.9s | 20.0s | 7.9s | 5/5 | 5/5 | 5/5 |
| Order status | 9,145 | 15,982 | 2,189 | 10.2s | 7.3s | 4.0s | 5/5 | 5/5 | 5/5 |
| Two items checkout | 177,717 | 106,845 | 19,208 | 21.2s | 37.6s | 15.6s | 5/5 | 5/5 | 5/5 |
| Edit cart | 219,337 | 107,192 | 16,013 | 24.6s | 47.3s | 15.5s | 5/5 | 5/5 | 5/5 |

**Claude Haiku 5.5**, 5 runs per task. Median per task: 47,356 tokens for the browser agent, 40,270 tokens for the screenshot agent, 5,138 tokens for the Affordances agent; 8.8s for the browser agent, 16.5s for the screenshot agent, 3.5s for the Affordances agent; 6 steps for the browser agent, 5 steps for the screenshot agent, 3 steps for the Affordances agent; $0.0031 for the browser agent, $0.0026 for the screenshot agent, $0.00053 for the Affordances agent per successful task. Tasks completed: 100% for the browser agent, 97% for the screenshot agent, 100% for the Affordances agent. The Affordances agent against the stronger baseline on each measure: 7.8× fewer tokens than the screenshot agent (9.2× fewer than the browser agent); 2.5× faster than the browser agent (4.7× faster than the screenshot agent); 4.9× cheaper per success than the screenshot agent (5.8× cheaper than the browser agent).

| Task | Browser tokens | Screenshot tokens | Affordances tokens | Browser time | Screenshot time | Affordances time | Browser passed | Screenshot passed | Affordances passed |
| --- | --: | --: | --: | --: | --: | --: | --: | --: | --: |
| Add to cart | 38,333 | 40,270 | 4,142 | 8.9s | 15.8s | 3.2s | 5/5 | 5/5 | 5/5 |
| Change quantity | 31,358 | 28,927 | 4,354 | 7.4s | 10.6s | 3.4s | 5/5 | 5/5 | 5/5 |
| Checkout | 49,211 | 40,155 | 5,366 | 8.8s | 11.8s | 2.8s | 5/5 | 5/5 | 5/5 |
| Search | 11,015 | 59,318 | 5,138 | 4.1s | 58.0s | 3.6s | 5/5 | 5/5 | 5/5 |
| Order status | 9,225 | 17,515 | 2,194 | 4.3s | 6.1s | 1.5s | 5/5 | 4/5 | 5/5 |
| Two items checkout | 239,475 | 141,793 | 21,501 | 22.7s | 40.4s | 6.5s | 5/5 | 5/5 | 5/5 |
| Edit cart | 268,359 | 143,932 | 17,094 | 22.0s | 43.0s | 5.8s | 5/5 | 5/5 | 5/5 |

Principles

## Three rules it doesn’t bend.

1. ### Signal, not pixels.

   An agent reads names, inputs and preconditions from the graph. No screenshots, no guessing which button does what.

   ```json
   "guard": "The variant is in stock",
   "available": true
   ```

2. ### One source, no drift.

   Every surface reads the same definition, so one edit reaches all of them. There is no second copy to forget.

   ```text
   actions.ts | 2 +-
   → form, WebMCP, HTTP, MCP
   ```

3. ### Agents 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.

   ```ts
   expose: 'confirm'
   // ← 409 confirm_required
   ```

**Reading this as an agent?** Skip the pixels: every page here is also Markdown and a graph. Start at [/llms.txt](https://affordances.vercel.app/llms.txt), read this page at [/index.md](https://affordances.vercel.app/index.md), or see [/agents](https://affordances.vercel.app/agents).

```bash
curl -H 'Accept: application/vnd.affordances+json' https://affordances.vercel.app
```
