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:
| 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
headwhile one is already equipped rejects withcategory_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:
- Client calls
hatched.buddies.evolve(buddyId)and receives anoperation_id. - The evolve worker re-checks readiness, then generates the next-stage bare image.
- Same worker, same job — if
equipped_itemsis non-empty, it composites the items over the new bare image before the operation completes. buddy.image_urlupdates atomically from the old composite directly to the new composite. No intermediate bare frame is ever observable.- Operation transitions to
completed, andevolution.completedfires 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.
Related
- Marketplace — where items live.
- Evolution — stage triggers.
- Customize buddy — walking through an equip + evolve flow end to end.