World Models, As They Should Be
Onairos and Reactor show how user-owned context can turn generated environments into personalized, explorable interfaces.
Most generative world tools begin the same way: a user types a prompt, the model generates an environment, and the experience begins from zero. This flow has been great for users to get a feel for world model capability, but they're not optimized for user experience and enjoyment. We recognize the difference between revealed vs. stated preferences, and that the best experiences should always feel like magic.
If generated worlds are really going to become interfaces for travel, commerce, games, entertainment, and education, they need to feel personal from the first interaction, without forcing users to explain themselves from scratch.
So we built daydreams
We think a great use case for personalized worlds are “daydreams”: exploratory experiences where users move through imagined worlds that continuously adapt around their tastes, interests, and intent.
Today’s world models still struggle with fully persistent, active environments, but daydreams can turn that constraint into a feature. They can shift, remix, and reveal places and scenes the way only a daydream can, letting your imagination wander until it settles on somewhere that feels unmistakably yours.1
1. Lawson, E., & Thompson, E. (2024). Daydreaming as spontaneous immersive imagination: A phenomenological analysis. Philosophy and the Mind Sciences, 5. doi:10.33735/phimisci.2024.9913
They should never be blank worlds either. Every daydream is seeded from a user's Onairos context, the permissioned profile built from the accounts they connect. Onairos turns who they are into the world's aesthetic, the mood, the palette, the kind of place, before they touch a thing.
We built ours on Reactor's low-latency world model, LingBot. It turns permissioned user context into these daydream-style worlds, and our first test case was travel.
As you explore these daydreams, they should call back to reality, connecting to real destinations, itineraries, and recommendations you can actually experience, with as little friction as possible. Here's what that looks like (skip to 0:35 for the travel recommendations):
Build it into your app
Three pieces make the interface work.
- Onairos brings the user-owned context.
- Gemini renders that context into a seed frame.
- Reactor turns the frame into a walkable world.
Here is the whole flow, end to end, the same pipeline behind the travel daydream above. Only the translate step changes from app to app. That is where you decide what a user's profile should become in your world; for us, it became a travel scene. Everything else, reading the profile, seeding the frame, spawning the world, and routing back to reality, stays identical.
1. Read the persona
A few lines drop the Onairos SDK consent flow into your app. When the user connects, onComplete hands you the permissioned profile.
import { OnairosButton, initializeApiKey } from 'onairos'
initializeApiKey({ apiKey: process.env.NEXT_PUBLIC_ONAIROS_API_KEY })
<OnairosButton
webpageName="Daydream"
requestData={['basic', 'personality']}
trainingScreenMode="fast"
onComplete={(result) => buildWorld(extractProfile(result))}
/>A user's Onairos profile is a plain object like this:
// extractProfile(result) →
{
archetype: "The Quiet Cartographer", // one-line identity
summary: "Drawn to depth over breadth, …", // a paragraph of nuance
topTraits: [{ name: "Curious Exploration", score: 92 }, …], // scored positives
lowTraits: [{ name: "Thrill-Seeking", score: 18 }, …], // what to avoid
mbti: "INFP", // light energy / social signal
platforms: ["spotify", "youtube", "reddit"] // where the read came from
}2. Translate the profile into a scene
This is the only part you shape. The profile becomes a structured scene: a prompt that seeds the world, and a realQuery that will bridge back to reality later.
// not a string template, a model call that returns a structured scene
const dream = await generateDaydream(profile)
// → { why, opening, drifts[] } each scene = { label, prompt, realQuery }
const scene = dream.opening // the scene you enter firstNot every field matters equally. Here is what each one drives as you translate.
| Field | What it shapes | Acts on |
|---|---|---|
topTraits | The base reality: the kind of world, its mood and pace. Highest scores dominate. | translate → seed |
lowTraits | The base reality by exclusion: what to keep out, like thrill, crowds, or danger. | translate → seed |
archetype | The anchor: sets the genre of the world before any detail. | translate → seed |
summary | Augmentation: the specific objects and textures once the world's kind is set. | translate, post-spawn |
mbti | A light nudge on energy and social density. | translate |
platforms | Provenance, not content: proof the read is real. Use it for trust, not geometry. | UI / trust |
Traced end to end, the same profile produced this:
PROFILE → WHAT IT BECAME IN THE WORLD
archetype: Quiet Cartographer → a still, map-room-like observatory
topTraits: Curious, Aesthetic → layered vistas, beautiful light
lowTraits: Thrill-Seeking (18) → no crowds, no danger, slow tempo
summary: "depth over breadth" → a few rich rooms, not a sprawl
mbti: INFP → solitary, soft, unhurried
3. Plant the seed frame
Reactor's world model requires a seed frame, so the scene that Claude wrote is handed to an image model, which renders it into a single still: the seed frame the world is built from.
// Claude's scene prompt → one still image (a Blob), the frame the world grows from
const seed = await forgeSeed(scene.prompt)Under the hood, forgeSeed posts the scene to a “Seed Forge” endpoint and names the image model to use, in our case Gemini (gemini-3.1-flash-image). The provider call stays server-side, so no image-model key ever touches the client.
// what forgeSeed sends
POST /reactor/seed-forge
{
prompt: scene.prompt,
imageProvider: "gemini",
imageModel: "gemini-3.1-flash-image",
aspectRatio: "16:9",
imageSize: "2K"
}
// → streams back the rendered frame as an image BlobSwap imageProvider or imageModel to render the seed with a different image model, and the rest of the pipeline doesn't change.
This is the most important thing to understand about a Reactor integration. Reactor never sees the raw persona. By the time the world generates, everything has been distilled into two things: the seed frame and the scene prompt. The frame carries the most weight, so put your strongest signal, the archetype, top traits, and what to exclude, into the scene before it is rendered, so it is baked in.
4. Hand it to Reactor
All of this runs inside the Reactor provider, which mints a short-lived JWT and opens the WebRTC connection for you, so you never manage sockets. Then it is a short handshake: upload the seed, wait for the model to lock onto the frame, set the prompt. And your agent should make it start. You'll see the still turn into a live, walkable world.
// inside <ReactorWorldProvider> (auto-connects + handles auth)
const { uploadFile, setImage, setPrompt, setSeed, start } = useLingbot()
const ref = await uploadFile(seed) // upload the seed frame → a handle
const accepted = onceImageAccepted() // arm the image_accepted listener
await setImage({ image: ref })
await accepted // model has locked onto the frame
await setPrompt({ prompt: scene.prompt }) // how the world unfolds as you move
await setSeed({ seed: 42 }) // pin the RNG → reproducible world
await start() // live: WASD to move, look to exploreA few things worth knowing:
- Order matters. Arm the
image_acceptedlistener beforesetImage, or you can miss the event. Prompt and seed are set after the frame is accepted, thenstart(). - The seed is a frame, not text.
setImageis the heavy lift, it is what the world grows from.setPromptsteers how it unfolds and responds as you move. - Output is a live video track, rendered by the SDK's view component. Movement and look are ongoing commands (
setMovement,setLookHorizontal,setLookVertical), and the world reacts in real time.
5. Route back to reality
A daydream is only worth anything if it leads somewhere real. So every scene carries a realQuery, a short phrase for the real-world version of the place. We send that along with the frame users see at the moment to get back real world matches.
// reality.js
const frame = captureWorldFrame() // the live view, not the seed
const { places } = await findRealPlaces(scene.realQuery, {
profile, // who they are
image: frame, // what they're looking at right now
})
// → matched, real results (each enriched with a real photo)Results are cached per scene, so returning is instant, and a refresh re-reads the current frame for fresh matches.
Why Reactor + Onairos matters
Reactor builds the world, Onairos makes it personal, and together they turn personalized world models into something genuinely simple to build.
This is the first step toward world models that are personal by default.