# xframes

xframes is a Cloudflare Worker where mentioning [@nanocodexyz](https://x.com/nanocodexyz) on X makes a nanocode agent build an app that runs inside the reply.

## Overview

A post that mentions the bot becomes a turn for a nanocode-wasm agent in a Durable Object. The agent publishes an HTML app with a host tool, and the bot replies with the app's link, which X plays inline.

```mermaid
flowchart LR
  post[/Post mentioning the bot/] --> bot[XBot]
  bot --> session[AgentSession: nanocode-wasm]
  session -->|publish_frame| frame[FrameStore]
  frame --> page[/f/id: app with a player card/]
  session -->|reply with the link| post
```

To try it, post on X with [@nanocodexyz](https://x.com/nanocodexyz) and describe an app, such as "a chessboard where everyone plays the next move together". The reply links to a page that shows the code as the model writes it, then the app. The [xframes landing page](https://xframes.nanocode.xyz) lists the newest apps.

## How It Works

* **One Durable Object per account:** `XBot` reads the bot's mentions every 15 seconds. Each mention becomes a turn with request ID `x:<post id>` in the session `x:<author id>`, so polling the same mention twice queues one turn.
* **The agent runs in the session:** `AgentSession` hosts the nanocode-wasm module with its SQLite journal in the object's storage, as in [Cloudflare Workers](/docs/guides/workers). Each turn starts a fresh SDK session, so a request is priced on its own small context, and the agent finds earlier work with tools.
* **Four host tools:** the agent can call only `publish_frame`, `edit_frame`, `read_frame`, and `list_frames`. `edit_frame` applies exact replacements to a published app, so a follow-up costs a few lines of model output instead of the whole document.
* **Apps are frames:** each app is a `FrameStore` Durable Object served at `/f/<id>` with X player card tags. A CSP sandbox gives every frame an opaque origin. Frames get a shared key-value store and a live socket for presence, which makes multiplayer games possible.
* **Drafts stream:** while the model writes a `publish_frame` call, the session decodes the `html` argument from the streaming response and shows the draft before the tool runs.
* **Replies close the loop:** when the turn finishes, the session replies to the post with the app's link. Each account also gets a Tempo wallet and a MACH grant, which can pay for models over MPP, as in [MPP Payments](/docs/guides/mpp-payments).

## The Publishing Tool

A host tool is a name, a description, and a JSON Schema, plus a handler the host runs. This sketch keeps frames in memory. xframes stores each one in its own Durable Object.

```ts title="frames.ts"
import type { HostTool, RuntimeOptions } from 'nanocode-wasm'

// The one tool the model sees: a whole HTML document per app
export const tools: HostTool[] = [
  {
    name: 'publish_frame',
    description:
      'Publish an app as one HTML document. Pass frame_id to replace an app you published.',
    parameters: {
      type: 'object',
      properties: {
        title: { type: 'string' },
        description: { type: 'string' },
        html: {
          type: 'string',
          description:
            'The whole document, with its scripts and styles inline',
        },
        frame_id: { type: 'string' },
      },
      required: ['title', 'description', 'html'],
      additionalProperties: false,
    },
  },
]

type Frame = {
  title: string
  description: string
  html: string
  revision: number
}

// Stores each published document and returns the link the bot replies
// with
export function host(
  origin: string,
  frames: Map<string, Frame>,
): NonNullable<RuntimeOptions['tools']> {
  return {
    async call(name, input) {
      if (name !== 'publish_frame')
        throw new Error(`no tool is named ${name}`)
      const { title, description, html, frame_id } = input as Frame & {
        frame_id?: string
      }
      if (html.length > 512 * 1024)
        throw new Error(
          'the document is over 512 KiB; send a smaller one',
        )

      const id =
        frame_id ?? crypto.getRandomValues(new Uint32Array(2)).join('')
      const revision = (frames.get(id)?.revision ?? 0) + 1
      frames.set(id, { title, description, html, revision })
      return { id, revision, url: `${origin}/f/${id}` }
    },
  }
}
```

Pass `tools` as `config.tools` and `host(origin, frames)` as `options.tools` when the runtime opens. [Tools](/docs/guides/tools) covers the contract.

## Cost

Under X's pay-per-use API pricing, a build costs about $0.02 to $0.03 in X credits for reading the mention and replying, plus the model requests.

## More

- [Cloudflare Workers](/docs/guides/workers): Run nanocode-wasm in a Durable Object.

- [Tools](/docs/guides/tools): Declare host tools and return their results.

- [MPP Payments](/docs/guides/mpp-payments): Pay for model requests per call.
