World Models, As They Should Be

Onairos and Reactor show how user-owned context can turn generated environments into personalized, explorable interfaces.

Nicholas BerryNicholas Berry
June 29, 2026

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.

A user's daydream shifting from one aesthetic scene to the next

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):

A personalized daydream, running on Reactor

Build it into your app

Three pieces make the interface work.

  1. Onairos brings the user-owned context.
  2. Gemini renders that context into a seed frame.
  3. 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.

Connect.jsx
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:

profile.js
// 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.

scene.js
// 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 first

Not every field matters equally. Here is what each one drives as you translate.

FieldWhat it shapesActs on
topTraitsThe base reality: the kind of world, its mood and pace. Highest scores dominate.translate → seed
lowTraitsThe base reality by exclusion: what to keep out, like thrill, crowds, or danger.translate → seed
archetypeThe anchor: sets the genre of the world before any detail.translate → seed
summaryAugmentation: the specific objects and textures once the world's kind is set.translate, post-spawn
mbtiA light nudge on energy and social density.translate
platformsProvenance, not content: proof the read is real. Use it for trust, not geometry.UI / trust
Rule of thumb: traits set the base reality, the kind of world and what to keep out. The summary fills in the details once that world exists. The archetype is the anchor that ties it together.

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.

seed.js
// 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.

seed-forge.http
// 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 Blob

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

spawn.js
// 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 explore

A few things worth knowing:

  • Order matters. Arm the image_accepted listener before setImage, or you can miss the event. Prompt and seed are set after the frame is accepted, then start().
  • The seed is a frame, not text. setImage is the heavy lift, it is what the world grows from. setPrompt steers 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
// 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.

Try it for yourself
reactor.onairos.io