# Every page, agent‑operable. (https://affordances.vercel.app/) 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): `` 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 ``` # Start here, agents (https://affordances.vercel.app/agents) Everything on this site is readable without a browser. Start from llms.txt, read any page as Markdown, or ask the docs for their graph. To make an app agent-operable, hand your agent the prompt below. [Read llms.txt](https://affordances.vercel.app/llms.txt) [Quickstart](https://affordances.vercel.app/docs/quickstart) ## Set up your agent Every page on this site is also available as Markdown, and `/llms.txt` indexes them all. Paste this prompt into your agent to point it at the docs: ```text Help me make this Next.js app agent-operable with Affordances. Start by reading https://affordances.vercel.app/llms.txt, which lists every page of the Affordances docs. Any page is available as Markdown by adding .md to its URL (for example https://affordances.vercel.app/docs/quickstart.md), and https://affordances.vercel.app/llms-full.txt has the whole site in one file. 1. Follow https://affordances.vercel.app/docs/guides/nextjs/getting-started.md: install @affordances/next, @affordances/react, and zod, then run `vlx @affordances/cli init`. It writes the route handler, wraps next.config.ts with withAffordances, and generates the manifest. 2. Next to each page an agent should operate, add a state.ts (what the page shows) and an actions.ts with defineAction. Set `expose` on every action an agent may call, and use 'confirm' for anything that spends money or can't be undone. 3. Check your work with curl -H 'Accept: application/vnd.affordances+json' on the page's URL, and read https://affordances.vercel.app/docs/guides/nextjs/sessions.md and https://affordances.vercel.app/docs/guides/security.md before exposing anything that needs a session. Only use APIs that appear in the docs. ``` - [llms.txt](/llms.txt): An index of every page on this site, for agents to read before anything else. - [Markdown pages](/llms-full.txt): Add .md to any page's URL for a plain Markdown copy, or read the whole site in one file. - [This site’s graph](/docs/quickstart#read-the-graph): These docs run on Affordances: ask / for application/vnd.affordances+json, and search and read pages as actions. - [MCP](/docs/guides/mcp): Serve any Affordances site to Claude, Cursor, and other MCP clients as two tools. ## Guides Walkthroughs for the things teams set out to do first. - [Make a page agent-operable](/docs/quickstart): Init, a state file, an action, and curl. - [Share your session with agents](/docs/guides/nextjs/sessions): One auth resolver for the UI and the agent path. - [Ask before an order goes through](/docs/concepts/expose): Confirm actions run only after the person approves. - [Give browser agents tools](/docs/guides/react): WebMCP tools from the page’s own graph. - [Connect Claude to your site](/docs/guides/mcp): Two MCP tools over any Affordances site. - [Guard what agents can do](/docs/concepts/guards): Preconditions agents see before they call. - [Answer with what changed](/docs/concepts/deltas): Deltas: changed entities, output, and next steps. - [Catch unmarked controls](/docs/guides/lint): Lint rules for Affordance and action names. ## Concepts - [Actions](/docs/concepts/actions): One defineAction call describes something a person or an agent can do on a page, and returns the server action that does it. - [State](/docs/concepts/state): A route's state.ts says what the page shows, as entities. It's loaded for the graph and again after every action. - [Expose](/docs/concepts/expose): Nothing reaches agents until an action says who may call it: public, session, or confirm. - [Guards and confirm](/docs/concepts/guards): The precondition an action checks before it runs. Agents see it before they call, and get its reason when it fails. - [Deltas](/docs/concepts/deltas): Calling an action returns what changed on the page, so the agent doesn't have to read it again. ## Reference - [Protocol](/docs/reference/protocol): The normative Affordances protocol, version 0: discovery, graphs, calls, deltas, and error codes. - [Security](/docs/guides/security): Default deny, explicit confirmation, Origin checks for cookies, and a fixed order of checks on every call. - [Options](/docs/reference/options): Every option of defineAction, createHandler, and withAffordances, in one place. - [API reference](/docs/reference/api-reference): Every export of every package, with its types, generated from the source. - [Benchmarks](/docs/benchmarks): A browser agent and an Affordances agent on the same tasks, with committed results. # Why Affordances (https://affordances.vercel.app/docs) Agents operate websites by reading pixels and guessing at forms. Affordances gives them the page's state and its actions directly, at the same URL, with the same session. An agent that wants to add a shirt to a cart today has two options. It can drive a browser: take a screenshot, find the size picker, click it, find the button, click that, and take another screenshot to see whether anything happened. Or it can use an API, if the site has one, which usually means a second set of endpoints with their own auth, their own validation, and their own drift from what the page actually does. Affordances takes a third path. Every page that opts in answers in two formats at the same URL: HTML for browsers, and a small JSON document for agents. The JSON says what the page shows (entities), what can be done there (actions, each with an input schema and whether it's available right now), and where to go next. Calling an action returns a delta: just the parts of the page that changed. ## One definition The action a person triggers by clicking a button is the same action an agent calls. You write it once with `defineAction`, next to the page, and it becomes: - a server action your components call, the same as any other; - an HTTP endpoint at the page's own URL, for headless agents; - a WebMCP tool, registered in the browser while the page is open; - an MCP tool, through `@affordances/mcp`, for Claude, Cursor, and other MCP clients. There's no second API to keep in sync. If the button changes, the agent sees the change. ## Same URL, same session Agents don't get a parallel world. A graph is built for whoever asked, with their cookies or their bearer token, so it never contains anything the HTML page wouldn't show the same user. A cart an agent fills is the cart the person sees when they open the page. ## Safe by default Nothing is reachable by agents until you say so. An action without `expose` stays a plain server action. Actions that spend money or can't be undone use `expose: 'confirm'`, which refuses to run until the caller resends with `confirm: true`, after showing the person a summary. Every agent call is validated, then guarded, then run, in that order. See [Security](https://affordances.vercel.app/docs/guides/security). ## What Affordances is not - It isn't a browser automation tool. Agents that can't speak Affordances still see your HTML, and `` marks the controls for them. - It isn't a replacement for your API. It describes pages, so it's shaped like your UI. - It isn't tied to Next.js. The [protocol](https://affordances.vercel.app/docs/reference/protocol) is plain HTTP and JSON, and `@affordances/core` is framework-agnostic. Next.js is the first adapter. # Quickstart (https://affordances.vercel.app/docs/quickstart) Give one page of a Next.js 16 app a graph and an action, then read and call it with curl. You need a Next.js 16 app using the App Router, and zod 4 (or any schema library that implements Standard Schema and Standard JSON Schema). ## Install and initialize ```bash vlt install @affordances/next @affordances/react zod vlx @affordances/cli init ``` `init` does four things, and skips any that are already done: - writes the route handler at `app/api/affordances/graph/[[...path]]/route.ts` (or under `src/app`); - wraps the default export of `next.config.ts` with `withAffordances`; - adds `.affordances` to `.gitignore`; - generates `.affordances/manifest.ts` from your `actions.ts` and `state.ts` files. The route handler it writes is one line: ```ts title="app/api/affordances/graph/[[...path]]/route.ts" export { GET, POST } from '@affordances/next/route' ``` ## Wrap the config If your config's default export isn't a plain identifier, `init` asks you to wrap it yourself: ```ts title="next.config.ts" import type { NextConfig } from 'next' import { withAffordances } from '@affordances/next/config' const nextConfig: NextConfig = {} export default withAffordances(nextConfig) ``` `withAffordances` regenerates the manifest when Next loads the config, and again in `next dev` whenever an `actions` or `state` file changes. It also adds the rewrites that send agent requests for a page to the route handler. Your pages and their HTML are untouched. ## Describe the page Affordances reads two files next to a page. `state.ts` returns what the page shows: ```ts title="app/guestbook/state.ts" import { defineState } from '@affordances/next' import { entries } from '@/lib/db' export default defineState(async () => ({ entries: await entries.list(), })) ``` `actions.ts` is a regular server actions file. Each `defineAction` export is a server action your components can call, and, because it sets `expose`, an action agents can call too: ```ts title="app/guestbook/actions.ts" 'use server' import { refresh } from 'next/cache' import { z } from 'zod' import { defineAction } from '@affordances/next' import { entries } from '@/lib/db' import guestbook from './state' export const sign = defineAction({ name: 'sign', description: 'Add a message to the guestbook', input: z.object({ name: z.string().min(1), message: z.string().max(280) }), state: guestbook, expose: 'public', effects: ['entries'], run: async (input, ctx) => { await entries.add(input) // a form submit re-renders the page; agents get a delta instead if (ctx.via === 'ui') refresh() }, message: ({ input }) => `Signed as ${input.name}`, }) ``` Use it in the page like any server action. Wrapping the form in `` marks it for agents that read the DOM: ```tsx title="app/guestbook/page.tsx" import { Affordance } from '@affordances/react' import { sign } from './actions' export default function Page() { return (