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

# HappyOyster tutorial

> Build a React app that creates, reopens, streams, and controls HappyOyster worlds.

This tutorial builds a React client with the typed HappyOyster SDK. By the end, the app can:

* open an Adventure or Directing session
* create a world and show build progress
* save and reattach a world
* stream the live world into a video element
* expose controls appropriate to the selected experience

For a complete project, see the
[HappyOyster example](https://github.com/reactor-team/js-sdk/tree/main/examples/happy-oyster).

## Install the SDK

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

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

The package exports the plain client at `@reactor-models/happy-oyster` and React bindings at
`@reactor-models/happy-oyster/react`.

## Mint a client token

Keep the Reactor API key on your server. Exchange it for a short-lived JWT and return only the JWT
to the browser:

```typescript theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
// app/api/reactor/token/route.ts
import { NextResponse } from "next/server";

export async function GET() {
  const response = await fetch("https://api.reactor.inc/tokens", {
    method: "POST",
    headers: {
      "Reactor-API-Key": process.env.REACTOR_API_KEY!,
    },
  });

  if (!response.ok) {
    return NextResponse.json({ error: "Token exchange failed" }, { status: response.status });
  }

  const { jwt } = await response.json();
  return NextResponse.json({ jwt });
}
```

Use a resolver in the client so the SDK can request a fresh token for later Coordinator calls:

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

## Mount the provider

The mode belongs on the provider, not on `createWorld()`. Use `"adventure"` for movement controls or
`"directing"` for text steering:

```tsx theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
"use client";

import { HappyOysterProvider, HappyOysterVideo } from "@reactor-models/happy-oyster/react";

export function HappyOysterExperience() {
  return (
    <HappyOysterProvider mode="adventure" jwt={getJwt} autoConnect>
      <main>
        <HappyOysterVideo className="aspect-video w-full" />
        <WorldController />
      </main>
    </HappyOysterProvider>
  );
}
```

The provider owns one client for its lifetime. To switch from Adventure to Directing, remount it:

```tsx theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
<HappyOysterProvider key={mode} mode={mode} jwt={getJwt} autoConnect>
  <Experience />
</HappyOysterProvider>
```

## Create a world

`createWorld()` accepts parameters for the provider's mode and resolves when the world reaches
`ready`.

```tsx theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
import { useHappyOyster } from "@reactor-models/happy-oyster/react";

function WorldController() {
  const { createWorld, worldState } = useHappyOyster();

  async function build() {
    const world = await createWorld({
      prompt:
        "A misty ancient forest with towering moss-covered trees and a narrow lantern-lit trail.",
      perspective: "third_person",
    });

    localStorage.setItem("happy-oyster-world-id", world.encrypted_world_id);
  }

  return (
    <section>
      <p>World phase: {worldState?.phase ?? "connecting"}</p>
      {worldState?.first_frame && <img src={worldState.first_frame} alt="World preview" />}
      <button onClick={build}>Build world</button>
    </section>
  );
}
```

Adventure parameters:

```typescript theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
await createWorld({
  prompt: "A mountain path above a sea of clouds.",
  perspective: "first_person",
  firstFrameImage, // optional File or Blob
});
```

Directing parameters:

```typescript theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
await createWorld({
  prompt: "A courier enters a floating city as a storm approaches.",
  resolution: "720p",
  layout: "Stable",
  narrative: "Dramatic",
  firstFrameImageUrl, // optional publicly reachable URL
});
```

The client mode determines which parameter set is valid. `createWorld()` does not take a `mode`
field.

## Reopen a saved world

Worlds persist beyond a session. Reopen one by its encrypted id:

```tsx theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
const { attachWorld } = useHappyOyster();

const worldId = localStorage.getItem("happy-oyster-world-id");
if (worldId) {
  await attachWorld(worldId);
}
```

The world must belong to the provider's selected mode. Attach an Adventure world through an
Adventure provider and a Directing world through a Directing provider.

## Start and stop a travel

Mount `<HappyOysterVideo />` before starting. Once the current world is ready:

```tsx theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
const { startTravel, endTravelSession, streaming } = useHappyOyster();

return streaming ? (
  <button onClick={() => void endTravelSession()}>End travel</button>
) : (
  <button onClick={() => void startTravel()}>Enter world</button>
);
```

`endTravelSession()` ends the live travel but keeps the Reactor session and world ready.
`disconnect()` closes the session; the world remains available for a later `attachWorld()`.

## Add Adventure controls

Adventure input is held state. Call `move`, `look`, or `interact` when an input begins, then release
the corresponding axis when it ends:

```tsx theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
const { move, look, interact, release, stop } = useHappyOyster();

const MOVE: Record<string, Parameters<typeof move>[0]> = {
  KeyW: "Front",
  KeyS: "Back",
  KeyA: "Left",
  KeyD: "Right",
};

useEffect(() => {
  const keyDown = (event: KeyboardEvent) => {
    if (event.repeat) return;
    const direction = MOVE[event.code];
    if (direction) void move(direction);
    if (event.code === "Space") void interact("Jump");
  };

  const keyUp = (event: KeyboardEvent) => {
    if (MOVE[event.code]) void release({ translation: true });
    if (event.code === "Space") void release({ interaction: true });
  };

  window.addEventListener("keydown", keyDown);
  window.addEventListener("keyup", keyUp);
  window.addEventListener("blur", stop);

  return () => {
    window.removeEventListener("keydown", keyDown);
    window.removeEventListener("keyup", keyUp);
    window.removeEventListener("blur", stop);
    void stop();
  };
}, [move, interact, release, stop]);
```

The SDK maintains the resend cadence required for held controls. Send transitions instead of sending
commands every animation frame.

## Add Directing controls

Directing uses text instructions and playback controls:

```tsx theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
const { instruct, pause, resume, rewind, travelState } = useHappyOyster();

async function submitInstruction(text: string) {
  const { accepted } = await instruct(text);
  if (!accepted) {
    showError("Instruction not accepted");
  }
}

await pause();
const { resumedAtSec } = await rewind(8);
```

Instructions and chapters arrive in the authoritative travel snapshot:

```tsx theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
return (
  <aside>
    {travelState?.user_instructions.map((entry, index) => (
      <p key={index}>{entry.instruction}</p>
    ))}
    {travelState?.chapters.map((chapter, index) => (
      <p key={chapter.chapter_id ?? index}>{chapter.title}</p>
    ))}
  </aside>
);
```

Use `useHappyOysterTravelStatus()` to track whether a Directing travel is running, paused, or
completed.

## Handle errors

Awaited setup calls reject on failure. Gate and upstream failures use `HappyOysterActionError`:

```tsx theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
import { HappyOysterActionError } from "@reactor-models/happy-oyster";

try {
  await createWorld(params);
} catch (error) {
  if (error instanceof HappyOysterActionError) {
    showError(`${error.code}: ${error.message}`);
  } else {
    showError("Could not build the world");
  }
}
```

Useful codes include:

| Code              | Meaning                                                  |
| ----------------- | -------------------------------------------------------- |
| `TRAVELING`       | End the active travel before creating or attaching       |
| `NO_WORLD`        | Create or attach a world before starting a travel        |
| `WORLD_NOT_READY` | Wait for `worldState.phase === "ready"`                  |
| `MODE_MISMATCH`   | Reopen the world through the other experience's provider |
| `403001`          | The world id is unknown or belongs to another account    |
| `403004`          | The prompt was rejected                                  |
| `403005`          | The starting image was rejected                          |

Runtime stream errors are available through `useHappyOysterTravelError()`.

## Next steps

* Read the [schema reference](/model-api-reference/happy-oyster/schema) for the complete typed
  surface.
* Use the [prompt guide](/model-api-reference/happy-oyster/prompt-guide) to author more stable
  worlds.
* Clone the
  [HappyOyster example](https://github.com/reactor-team/js-sdk/tree/main/examples/happy-oyster) for
  a complete Next.js implementation.
