# Compositing & stages

> How equipped items layer onto the buddy, and how evolution preserves them atomically.

Source: https://docs.hatched.live/docs/concepts/compositing-and-stages

When a user equips a hat on a stage-1 egg and then evolves to stage 2,
the hat does not disappear. This page is how that invariant is
maintained end-to-end.

## The 8 canonical categories

Every marketplace item belongs to one of eight slots:

| Category     | `layer_order` | Multi-equip? |
| ------------ | ------------- | ------------ |
| `background` | 10            | no           |
| `body`       | 20            | no           |
| `feet`       | 30            | no           |
| `hand`       | 40            | no           |
| `neck`       | 50            | no           |
| `face`       | 60            | no           |
| `head`       | 70            | no           |
| `accessory`  | 80            | yes          |

`layer_order` is the compositing z-order (back → front). `background`
paints first, `accessory` paints last. `accessory` is the only slot that
accepts multiple equipped items — everything else rejects the second
item in the same category with `category_conflict`.

### Equip bounds

- **Max 4 equipped items.** The fifth rejects with `too_many_items`.
- **Non-accessory categories are exclusive.** Equipping a second `head`
  while one is already equipped rejects with `category_conflict`.
- Items sort deterministically by `(layer_order, item_id)` before
  reaching the image pipeline, so two equipped items always composite
  in the same order.

These checks happen at the API boundary, surfaced in the SDK as
`TooManyItemsError` and `CategoryConflictError`.

## Stage-aware item assets

Items can ship a stage-specific override via `stage_image_urls`:

```json
{
  "image_url": "https://cdn.hatched.live/items/wizard_hat/base.png",
  "stage_image_urls": {
    "3": "https://cdn.hatched.live/items/wizard_hat/stage3.png",
    "5": "https://cdn.hatched.live/items/wizard_hat/stage5.png"
  }
}
```

The compositing pipeline reads `stage_image_urls[currentStage]` and
falls back to `image_url` when there's no override. Designers only have
to ship overrides for the stages where the base asset would look wrong.

## The appearance state machine

Anything that changes the buddy's image — hatch, equip / unequip, evolve — runs
through the image pipeline asynchronously. The buddy carries an `appearance`
block so you always know whether what you're showing is the final render. This
is the single source of truth for "is the visual ready"; the buddy's economy
state (coins, skills, stage) is already committed regardless.

| `appearance.status` | Meaning | What to show / do |
| --- | --- | --- |
| `ready` | `image_url` is the final composite; `rendered_equipped_items` matches `equipped_items`. | Show `image_url`. Nothing to do. |
| `pending` | A job is generating or compositing. `appearance.operation_id` points at it. | Show `image_url` (the last good render) and an optional "updating…" hint. `operations.wait(operationId)` to know when it's done. |
| `awaiting_credits` | The job is blocked on insufficient image credits. | Show the last good `image_url`. Surface a top-up prompt; the job resumes once credits land. See [Credits](/docs/billing/credits). |
| `failed` | The job failed. Check `appearance.error.code`. | Show the last good `image_url`. If `error.code === 'needs_rerender'` (typically a migrated buddy with no usable bare-stage image), call `buddies.rerenderAppearance(buddyId)` — or the widget `POST /widget/appearance/rerender` with an `items:equip` session — wait for `ready`, then re-equip. For other error codes, retry the originating action. |

Two fields make recovery deterministic: `base_image_url` is the trustworthy
bare-stage image, and `equipped_items` is the desired item set. `image_url` and
`rendered_equipped_items` are "what's currently on screen". A rerender
regenerates the bare stage from scratch, after which you re-apply
`equipped_items`.

## Atomic evolve × equip

The invariant that unlocks the whole feature: **the stage transition is
committed atomically, while the item composite is tracked as appearance
state.**

What happens when a user with an equipped hat evolves:

1. Client calls `hatched.buddies.evolve(buddyId)` and receives an
   `operation_id`.
2. The evolve worker re-checks readiness, then generates the next-stage
   bare image and stores it as `base_image_url`.
3. If `equipped_items` is non-empty, the same job attempts to composite
   the desired items over that bare image.
4. If compositing succeeds, `buddy.image_url` becomes the rendered image,
   `rendered_equipped_items` matches `equipped_items`, and
   `appearance.status` is `ready`.
5. If compositing is delayed or fails, the stage still advances. The buddy
   keeps the new bare stage image, `appearance.status` becomes
   `awaiting_credits` or `failed`, and `appearance.operation_id` points at
   the job that owns recovery.
6. Operation transitions to `completed`, and `buddy.evolved` fires on
   webhooks. Read `buddy.appearance` to decide whether the visual composite
   is also done.

```ts
const op = await hatched.buddies.evolve(buddyId);
const result = await hatched.operations.wait(op.operationId);
// result.buddy.evolutionStage has advanced.
// result.buddy.appearance?.status tells you whether item compositing is ready.
```

The split matters for recovery. `base_image_url` is the trustworthy bare
stage. `image_url` is the currently displayable render. `equipped_items`
is the desired item set, while `rendered_equipped_items` is what actually
made it into the current image. If a migrated buddy reports
`appearance.status === 'failed'` with `error.code === 'needs_rerender'`,
call `buddies.rerenderAppearance(buddyId)` or the widget
`POST /widget/appearance/rerender` endpoint, wait until `ready`, then
re-equip the desired items.

## Demo path parity

The demo widget (publishable-key `widget_sessions.demo`) runs through
the same atomic pipeline via a mock image provider. Stage + equipped
items still composite; evolution history rows are still written with
`source: 'demo'`. That's why the marketing demo and production builds
show identical behavior for this flow.

## Related

- [Marketplace](/docs/concepts/marketplace) — where items live.
- [Evolution](/docs/concepts/evolution) — stage triggers.
- [Customize buddy](/docs/guides/customize-buddy) — walking through an
  equip + evolve flow end to end.
