> ## Documentation Index
> Fetch the complete documentation index at: https://docs.reactor.inc/llms.txt
> Use this file to discover all available pages before exploring further.

# X2 Tutorial

> An end-to-end X2 walkthrough.

A guided tour of building with X2. By the end you'll know how to publish a source stream (a webcam,
a clip, or a still image) and arm generation with a prompt. You'll also know how to anchor a swap to
a reference image, steer the edited subject with a drag pointer, and surface model errors.

This page draws its patterns from the X2 reference frontend in the js-sdk repo. To start from that
app instead of from scratch, scaffold it:

<Tabs>
  <Tab title="npm">
    ```shell theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
    npx create-reactor-app my-x2-app --model=x2
    ```
  </Tab>

  <Tab title="pnpm">
    ```shell theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
    pnpm create reactor-app my-x2-app --model=x2
    ```
  </Tab>
</Tabs>

The scaffolded app's `skill/SKILL.md` documents the same patterns for coding agents building on top.

## Installation and setup

You will need:

* Node.js 18+ (and React 18+ if you use the provider and hooks)
* a [Reactor API key](/authentication) (starts with `rk_`)

Install the typed SDK; it pulls in the base `@reactor-team/js-sdk` as a dependency:

<Tabs>
  <Tab title="npm">
    ```shell theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
    npm install @reactor-models/x2
    ```
  </Tab>

  <Tab title="pnpm">
    ```shell theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
    pnpm add @reactor-models/x2
    ```
  </Tab>
</Tabs>

## How X2 works

The mental model that the rest of this page builds on:

* You stream video in; the model streams edited video back. The client publishes a `source` video
  track, and the model publishes `main_video`. Everything else is control surface.
* There is no start command. Generation begins on its own once a non-empty prompt is set and source
  frames are arriving. To stop and clear, call `reset`.
* The model is the source of truth. It broadcasts a full `state_update` snapshot on connect and
  after every observable change. Reduce that snapshot into your UI state instead of tracking your
  own copy of what you sent.
* Everything lands on block boundaries. X2 generates one block at a time; prompt swaps apply from
  the next block, and the model samples the pointer once per block.

## Authentication

Your `rk_…` API key must never reach the browser. Keep it server-side and mint a short-lived JWT the
client connects with; see [Authentication](/authentication) for the token route pattern. Hand the
provider a `getJwt` resolver rather than a static token. The SDK calls it before every Reactor API
request (connect, ICE refresh, uploads, clip manifests), so a fresh token is always in reach.

```tsx theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
async function fetchToken(): Promise<string> {
  const res = await fetch("/api/reactor/token");
  if (!res.ok) throw new Error(`Token fetch failed: ${res.status}`);
  const { jwt } = (await res.json()) as { jwt: string };
  return jwt;
}

return (
  <X2Provider getJwt={fetchToken} connectOptions={{ autoConnect: true }}>
    {children}
  </X2Provider>
);
```

Because the resolver runs on every hop, make the token route itself do the caching rather than
adding a localStorage layer or parsing the JWT client-side:

* Expose it as GET, not POST. Browsers do not cache POST responses; a GET route whose handler still
  POSTs to the Reactor `/tokens` endpoint lets the browser's HTTP cache serve repeat calls without a
  network round trip.
* Send `Cache-Control: private`. JWTs are per-user; `private` keeps CDNs and shared proxies from
  storing them.
* Derive `max-age` from the response's `expires_at`, minus a small skew, instead of hardcoding a
  number. The cache window then always tracks the lifetime the server granted.

An inline `getJwt` function is fine: the provider stabilizes it through a ref, so a parent re-render
does not tear the session down. Do not wrap it in `useCallback`.

## Streaming a source

The model edits whatever arrives on the `source` track; the app never uploads source media, it
streams it. Three source shapes cover most apps, and all three end in the same call, publishing a
`MediaStreamTrack`:

* For a webcam, call `getUserMedia({ video: true })` and publish the video track.
* For a clip, play a video element (muted, looped) and publish the video track from
  `captureStream()`.
* For a still image, draw it to a canvas and publish `canvas.captureStream(24)`. The capturer only
  emits when the canvas repaints, so keep repainting the same frame on an interval at the stream
  rate. From the model's side this is a video of a motionless scene, which is the drag-to-animate
  setup.

```typescript theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
const canvas = document.createElement("canvas");
canvas.width = 1280;
canvas.height = 720;
const ctx = canvas.getContext("2d")!;
const draw = () => ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
draw();

const track = canvas.captureStream(24).getVideoTracks()[0];
track.contentHint = "detail"; // hold resolution; adapt framerate instead
setInterval(draw, 1000 / 24); // keep the capturer emitting

await x2.publishSource(track);
```

`keep_backlog: false` (the default) reads the newest frames and keeps latency bounded, right for a
live webcam. `keep_backlog: true` consumes every frame in order for smoother motion at the cost of
growing delay, right for clips and drag-to-animate.

## Starting a run

Generation starts at the first moment a non-empty prompt exists while frames arrive. Set the
reference image before the prompt if the first block should already carry it:

```typescript theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
const ref = await x2.uploadFile(characterImageFile);
await x2.setReferenceImage({ reference_image: ref });
await x2.setPrompt({
  prompt: "replace the character in the video with the character from the reference image",
});
// Generation starts on its own; generation_started carries the output resolution.
```

The reference can be replaced mid-run: the stream restarts on its own and the new image conditions
the edit from its first block; details in the
[schema](/model-api-reference/x2/schema#set_reference_image).

Render the result by attaching the `main_video` track to a video element. Use `<X2MainVideoView />`
in React, or `onMainVideo` on the typed client.

## Reading the event stream

Reduce `state_update` into your UI state and treat it as authoritative:

```tsx theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
useX2StateUpdate((msg) => {
  setUi((s) => ({
    ...s,
    generating: msg.generating,
    activePrompt: (msg.prompt as string | null) ?? null,
    outputWidth: (msg.width as number | null) ?? null,
    outputHeight: (msg.height as number | null) ?? null,
    hasReference: msg.has_reference_image,
    keepBacklog: msg.keep_backlog,
  }));
});
```

Three discrete events are worth handling on top of the snapshot:

* `reference_image_accepted` is the one place to read the decoded image's width and height; the
  snapshot only says whether a reference is set.
* `generation_stopped` fires on any stop. Check the `reason` field: `reference_image_changed` is the
  automatic restart after a [reference swap](#starting-a-run), and `reset` means the run is over.
* `command_error` reports a rejected command, covered below.

One rendering gotcha: when generation stops, the WebRTC video element freezes on the last
transformed frame rather than going black. If your UI should read as "stopped", blank the output
pane on `generation_stopped` and lift the blank when the next `state_update` reports
`generating: true`.

## Steering with the drag pointer

The pointer steers the edited subject while a drag is held. Wire it to pointer events on the output
pane:

* On pointer-down, send `{ x, y, active: true }` and capture the pointer.
* On pointer-move, keep sending positions, throttled to about 30 Hz with a trailing send so the last
  position of a fast gesture lands. The model samples the pointer once per block, so faster sends
  buy nothing.
* On pointer-up or cancel, send `{ active: false }`. Also send it if the overlay unmounts or the
  session leaves ready mid-drag, so the model does not keep steering toward a stale point.

The pointer's coordinates map to the output frame, not your DOM element. If the pane letterboxes the
video (`object-fit: contain`), map from the pane's box to the visible content using the aspect ratio
from `generation_started`, and clamp to `0..1`.

The model echoes the pointer back in every `state_update` as `pointer_x`, `pointer_y`, and
`pointer_active`. These reflect the model's view of the pointer rather than your local gesture, so
surfacing them in a debug readout shows you the payload each `set_pointer` call delivered.

For drag-to-animate, combine the pieces. Stream a still image as the source, write a prompt that
binds motion to the drag, such as `the character in the video follows the drag trajectory`, and set
`keep_backlog: true` for smooth motion.

## Surfacing command\_error

A rejected command has no effect, and the rejection arrives as a `command_error` message rather than
a thrown error. Surface it as a transient banner and move on; the session stays healthy:

```tsx theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
useX2CommandError((msg) => {
  showBanner(`${msg.command}: ${msg.reason}`);
});
```

One error you may hit in practice is `set_reference_image` with a file the model cannot decode as an
image.

## Resetting and disconnecting

`reset` stops generation and clears the prompt, reference image, and pointer server-side. Mirror it
client-side by clearing prompt drafts and reference previews in the same motion (keying those
components on a reset counter is a clean way to do it). On a full disconnect, drop all local session
state so a reconnect starts clean; the model's first `state_update` re-seeds the UI.
