HatchedDocs
Concepts

Compositing & stages

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

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:

Categorylayer_orderMulti-equip?
background10no
body20no
feet30no
hand40no
neck50no
face60no
head70no
accessory80yes

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:

{
  "image_url": "https://cdn.hatched.com/items/wizard_hat/base.png",
  "stage_image_urls": {
    "3": "https://cdn.hatched.com/items/wizard_hat/stage3.png",
    "5": "https://cdn.hatched.com/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.

Atomic evolve × equip

The invariant that unlocks the whole feature: operations.wait() never returns a bare-stage buddy when items are equipped.

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.
  3. Same worker, same job — if equipped_items is non-empty, it composites the items over the new bare image before the operation completes.
  4. buddy.image_url updates atomically from the old composite directly to the new composite. No intermediate bare frame is ever observable.
  5. Operation transitions to completed, and evolution.completed fires on webhooks.
const op = await hatched.buddies.evolve(buddyId);
const result = await hatched.operations.wait(op.operationId);
// result.buddy.imageUrl already has the new stage AND the hat.

If the composite step fails, the whole operation fails and buddy.image_url stays on the old composite — there is no "generating" placeholder state on the client.

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.