A smart wearable is a wearable item plus an SDK7 scene that runs as a portable experience for the player who has it equipped, wherever they go. The project is a normal SDK7 project with a wearable.json; the code, the wearable GLB(s), a thumbnail and any assets are packed into one zip and uploaded to the Builder.
Related skills — link, don't repeat:
| Need | Skill |
|---|---|
| Drive Blender at all (headless CLI vs Blender MCP, paths, never-fall-back rule) | add-3d-models → {baseDir}/../add-3d-models/references/blender-authoring.md |
| Model the wearable GLB itself (base meshes, limits, skinning, export, thumbnail) | wearables |
| Author the avatar part of an emote (rig, posing, export, masks) | avatar-animations |
| Add props / bone particles to that emote | avatar-animations → {baseDir}/../avatar-animations/references/emote-props.md |
triggerSceneEmote, stopEmote, AvatarAttach, AvatarLocomotionSettings API |
player-avatar |
Physics.applyImpulseToPlayer and forces |
player-physics |
Held-key polling (inputSystem.isPressed) |
advanced-input |
| HUD (fuel bars, cooldowns) | build-ui |
AudioSource |
audio-video |
AssetLoad preloading |
optimize-scene → "Asset Preloading" |
WebSocket / fetch to a third-party server (the only way to share state, see Multiplayer below) |
scene-runtime |
Worked example (jetpack: F to thrust, fuel/overheat, masked emote with flame props, wearer-only sound, preloading): {baseDir}/references/jetpack-example.md.
Project layout
Scaffold with npx @dcl/sdk-commands init --project smart-wearable (the Creator Hub cannot create one, but can open and preview an existing one). The toolchain treats a project as a smart wearable because wearable.json exists.
| File | Notes |
|---|---|
wearable.json |
Builder metadata. Strict schema, see below. |
scene.json |
"isPortableExperience": true, "main": "bin/index.js". scene.parcels is used only by the local preview. |
thumbnail.png |
256 × 256 PNG, transparent background (Curation Committee rejects opaque backgrounds). |
models/male/*.glb, models/female/*.glb |
One GLB per body shape when the shapes differ (they do for any body-hugging wearable). One GLB for both is fine for rigid accessories. |
src/index.ts |
main() runs when the player equips the wearable. |
package.json |
Add "pack": "sdk-commands pack-smart-wearable" to scripts if missing. |
.dclignore |
Same as a scene's. Everything not ignored goes into the zip. |
RULE: wearable.json must match the Builder schema exactly
The Builder validates against WearableConfigSchema (builder-client) with additionalProperties: false on the top-level object (nested objects such as data are not strict). Any violation gives the generic "the wearable config file is invalid" with no detail.
- Top-level keys allowed:
id,name,description,rarity,data,mapping. Nothing else. Remove legacymenuBarIcon,model,bodyShape,collectionAddressif present (older docs and samples listed them). - Required:
name,data. description: max 64 characters. The commonest cause of the error. A longer description can be written in the Builder's item editor after upload.rarity:unique(1) ·mythic(10) ·exotic(50) ·legendary(100) ·epic(1000) ·rare(5000) ·uncommon(10000) ·common(100000).id: a UUID. Generate a fresh one when forking a project (crypto.randomUUID()/uuidgen).datarequires all five:replaces,hides,tags,representations,category.replaces,hidesandtagsmay be[];representationsneeds at least one entry. Optional:removesDefaultHiding,blockVrmExport.representations[]:bodyShapes(urn:decentraland:off-chain:base-avatars:BaseMale/…:BaseFemale),mainFile,contents(must includemainFile; paths relative to the project root),overrideHides,overrideReplaces. Both body shapes must be covered.category:eyebrows,eyes,facial_hair,hair,mouth,upper_body,lower_body,feet,earring,eyewear,hat,helmet,mask,tiara,top_head,skin,hands_wear.
Validating locally with Ajv: the schema uses $data references in representations, so construct Ajv with { $data: true } or it reports false negatives.
{
"id": "00000000-0000-4000-8000-000000000000",
"name": "Example Jetpack",
"description": "Press F to fly. Hold to climb until the fuel runs dry.",
"rarity": "mythic",
"data": {
"replaces": [], "hides": [], "tags": ["jetpack", "smart"],
"category": "upper_body",
"representations": [
{ "bodyShapes": ["urn:decentraland:off-chain:base-avatars:BaseMale"],
"mainFile": "models/male/suit.glb", "contents": ["models/male/suit.glb"],
"overrideHides": [], "overrideReplaces": [] },
{ "bodyShapes": ["urn:decentraland:off-chain:base-avatars:BaseFemale"],
"mainFile": "models/female/suit.glb", "contents": ["models/female/suit.glb"],
"overrideHides": [], "overrideReplaces": [] }
]
}
}
RULE: Stay under 3 MB uncompressed — measure the files, not the zip
- The Builder rejects a smart wearable whose unzipped contents exceed 3 MB (
MAX_SMART_WEARABLE_FILE_SIZE) with a "file too big" error. npm run packonly prints a message above 2 MB and still writessmart-wearable.zip. A successful pack proves nothing.- Measure:
unzip -l smart-wearable.zipand read the total line. Do this after every pack. - Creator Hub trap: editing the project in the Creator Hub writes
assets/Images/autogenerated-thumbnail.png(a 1920×1080 screenshot, ~4 MB) and pointsscene.json→display.navmapThumbnailat it. It saves it when you go Back from the editor and before Publish / Deploy (saveThumbnail), re-adding it whenever the field is empty or the file is missing. A 1.5 MB wearable became 4.9 MB this way. Addassets/Images(or the file) to.dclignoreand setnavmapThumbnailto"". The Creator Hub also addsfeatureToggles,spawnPoints,skyboxConfig,landscapeTerraintoscene.json; those are harmless. - Also ignore source folders:
blender/,*.blend, renders, the zip itself. - Budget killers: PNG textures in GLBs (export JPEG instead, see wearables), uncompressed audio (use short
.mp3loops), unused npm libraries.
RULE: The preview is not the published wearable
npm run start / Creator Hub Play runs the code as a regular scene on the parcels in scene.json, not as a portable experience (Explorer LoadSmartWearablePreviewSceneSystem). Consequences:
- Everything that requires "the scene the player is in" — physics impulses, input — stops working when the player walks off those parcels. Published, it works everywhere. Add a few parcels to
scene.jsonif you need room to test. - The wearable model may not be shown on your avatar in preview [UNVERIFIED].
- To test it equipped, for real: upload to the Builder, add to a collection, click See in Decentraland (opens on Sepolia; equip it from the backpack). No minting or publishing needed.
- The preview is still the fast loop for gameplay tuning (impulse feel, emote pose, UI). Confirm once via See in Decentraland.
What runs where — who sees what
The code runs only on the wearer's client. Design every visible effect around that:
| Effect | Wearer | Other players |
|---|---|---|
Entities, models, UI, ParticleSystem, AudioSource created by the code |
✅ | ❌ |
| The wearable model itself | ✅ | ✅ |
Avatar emote played with triggerSceneEmote() / triggerEmote() |
✅ | ✅ |
| Props and bone-driven particles inside that emote file | ✅ | ✅ |
Sound with a triggerSceneEmote() clip |
❌ | ❌ |
Player motion from Physics impulses / AvatarLocomotionSettings |
✅ | ✅ (it's the avatar's movement) |
- To show an effect to everyone, put it in an emote as a prop. Particles must be meshes skinned to animated prop bones — see
{baseDir}/../avatar-animations/references/emote-props.md.ParticleSystemand scene entities are invisible to others. - Scene emotes play no audio; only Builder-uploaded emotes do. Why:
{baseDir}/../avatar-animations/references/emote-props.md→ "Playing it". There is currently no way to make a smart-wearable sound audible to others; report it via report-sdk-issue if the user needs it. - Wearer-only sound: an entity with
AvatarAttach { anchorPointId: AvatarAnchorPointType.AAPT_POSITION }(the field is required) plus a loopingAudioSourcewithplaying: false; toggleplaying. Only the wearer hears it. - Abilities used while moving → mask the emote.
triggerSceneEmote({ src, loop: true, mask: AvatarMask.AM_UPPER_BODY })keeps playing through jumps and falls (a full-body emote is cut by locomotion), props still play, and the mask is replicated to remote players. Stop withstopEmote({}). Masked emotes emit noAvatarEmoteCommandevents on the local player — track state yourself.
Multiplayer
Smart wearables currently have no built-in multiplayer:
MessageBusdoesn't work in a smart wearable, and neither doessyncEntity/ CRDT sync of scene entities. Each wearer runs their own isolated copy of the code, even when two players wearing the same wearable stand side by side.- The Multiplayer Server (authoritative-server) doesn't work for smart wearables either.
- What other players do see is the wearer's avatar: its position and movement (including what
Physicsimpulses do to it) and the emotes it plays, with their props. Those are synced by the avatar system, not by the wearable's code. - What they don't see: any entity the code creates, sounds, UI, and all game data (scores, health, fuel, cooldowns, inventory).
The only way to share state between players is a third-party server the wearable talks to over WebSocket or fetch (see scene-runtime). Use it for a shared leaderboard, a game between wearers, or showing other wearers' state. Each client then renders other players' state locally; it's still invisible to players who aren't wearing the wearable.
Don't plan a feature on MessageBus, syncEntity or the Multiplayer Server for a smart wearable. If the user asks for one, explain the limitation and offer the third-party server or an emote-based design (everyone sees emotes). Report the gap via report-sdk-issue if it blocks them.
Runtime behaviour of a published smart wearable
- A portable experience is always treated as the current scene (Explorer calls
SetIsCurrent(true)for PX), soPhysics.applyImpulseToPlayer, continuous forces andinputSystemwork anywhere the wearer goes. AvatarAttachnormally requires the player to be inside the scene; for a PX that condition always holds [inferred from the above, not tested on a published wearable].- Host scenes win. A scene can set
featureToggles.portableExperiencesto"disabled"(wearable code stops while the player is there) or"hideUi". ForAvatarLocomotionSettings, a scene's value beats the wearable's. Mention this to the user when designing movement abilities — parkour scenes are expected to block them. - Load order varies: if the player logs in already wearing it, the wearable's code may start before other players/scenes load. Don't assume nearby players exist at
main().
Physics tuning numbers (for movement abilities)
Measured from unity-explorer main: character mass is 1, so an impulse magnitude is the velocity change in m/s. External velocity decays with drag 1.5 /s and is capped at 50 m/s. An upward impulse zeroes the current downward (gravity) velocity first, so a mid-fall thrust doesn't have to fight the fall. A launch kick of ~6 plus ~0.8 every 0.1 s while held gives a steady ~4–5 m/s climb. API details: player-physics.
Preloading
Create one AssetLoad entity in main() listing the emote .glb and the sound files (API: optimize-scene). Audio goes into the same cache AudioSource uses, so the first play is instant. An emote .glb is downloaded by the model loader, not the emote loader: preloading removes the download wait, but the emote is still set up on first triggerSceneEmote. Register assetLoadLoadingStateSystem to log FINISHED_WITH_ERROR / NOT_FOUND — that's how you catch a wrong path.
Pack and upload
npm run pack→smart-wearable.zip.unzip -l smart-wearable.zip— total under 3 MB, no source/Blender files, no autogenerated thumbnail. Make sure the user uploads this zip (check its path and size with them if the Builder disagrees).- Builder → Collections → a collection → New item → drop the zip. It's detected as a smart wearable from
wearable.json. - The Builder asks for a video showcase (
.mp4, ≤ 4 MB, ≤ 15 s) shown in the marketplace. Tell the user to record one before uploading. - In the item editor: check hides/replaces overrides, description, tags.
- See in Decentraland on the collection to test on Sepolia. Then publish the collection (Curation Committee review).
Pre-upload checklist
-
wearable.json: only allowed top-level keys;description≤ 64 chars; all five requireddatakeys present; both body shapes covered; fresh UUID. - Each GLB within category limits (wearables); both body shapes exported and referenced.
-
thumbnail.png256 px square, transparent. -
.dclignorecoversblender/,assets/Images,*.zip;navmapThumbnailempty. -
unzip -ltotal < 3 MB. - Effects meant for others live in the emote as props, not in
ParticleSystem/entities; sound is wearer-only and the user knows it. - Tested beyond the preview parcels via See in Decentraland.
Sources
Verified 2026-10 against main of: builder-client (WearableConfigSchema, MAX_SMART_WEARABLE_FILE_SIZE), builder, js-sdk-toolchain (pack-smart-wearable, MAX_WEARABLE_SIZE), unity-explorer (LoadSmartWearablePreviewSceneSystem, LoadSceneEmotesSystem, SDKExternalPhysicsSystems, PX SetIsCurrent), creator-hub (autogenerated thumbnail), and a jetpack smart wearable built and uploaded end to end. Docs: Smart wearables · Uploading smart wearables · Creating wearables.