Token Toss level manifest structure
This reference explains how the Token Toss Unity layer serialises ScriptableObject data into the manifest JSON that OGAL ingests. It is intended for engineers who need to audit or reproduce a level export without relying on the Unity editor. The format mirrors the ScriptableLevelDTO class that Token Toss shares across its build tools; OGAL only depends on this DTO when we mint levels for the game. Other namespaces that integrate with OGAL can upload arbitrary JSON as long as they meet the protocol requirements, so treat the layout below as guidance for Token Toss projects rather than a protocol mandate.
Metaplex NFT ownership
Each Token Toss level is linked to a Metaplex NFT mint. That NFT certifies ownership of the level, and whoever controls the wallet holding the NFT has the authority to update or remint the level metadata. When you want to verify ownership, open the mint on a block explorer or Metaplex view page, for example: https://explorer.solana.com/address/<MINT_ADDRESS>?cluster=mainnet-beta.
Protocol flexibility
Token Toss standardises on the ScriptableLevelDTO layout because the game consumes the manifest at runtime, not because OGAL restricts submissions to this shape. Teams that adopt OGAL for other namespaces are free to publish entirely different JSON structures (for example, cosmetic-item metadata) as long as they satisfy the base manifest validation rules outlined in the hashing checklist.
Required top-level fields
The manifest must serialise the following properties because they are the only fields hashed by the mint pipeline:
levelName– human-readable name displayed in clients.entities– array of entity descriptors, each encoding placement/configuration data.numberOfTossesAllowed– maximum attempts before a run fails.goalsToWin– number of goals that must be cleared to succeed.isTokenResetAfterToss– whether the token respawns to its initial state between tosses.
The OGAL CLI rejects uploads that omit any of these fields, so verify the serialized JSON includes them before generating the manifest hash.
Entity records
Every element in entities must include the spatial data Unity replays at runtime together with an entity-specific configuration block:
type– numericGameEntityTypevalue emitted by the Unity exporter.position–{ x, y }coordinates relative to the level origin.rotation– z-axis rotation in degrees.scale–{ x, y, z }vector describing the transform scale applied in Unity.config– object whose shape is determined by the entity type. The Token Toss helper instantiates the concreteGameEntityConfigsubclass from thetypeenum, so omitting required fields leaves the prefab unusable at runtime.
Type to config mapping
| Type | Entity | Required config fields |
|---|---|---|
-1 | GoalZone | entityName, useRandomPositioning, xRange, yRange, useRandomScaling, scaleRange |
0 | Portal | entityName, preserveVelocity, exitForce, exitLocalPosition, exitLocalRotation, secondsBeforeTurningBackOn, secondsBeforeTurningOff |
1 | Magnet | entityName, pullStrength, radius, secondsBeforeTurningBackOn, secondsBeforeTurningOff |
2 | BoostPad | entityName, boostForce, shouldRetainDirection, secondsBeforeTurningOff, secondsBeforeTurningBackOn |
3 | Wall (Bounce) | entityName, bounceForce, maxVelocityAfterBounce |
4 | RotatingPlatform | entityName, rotationSpeed, clockwise, friction |
5 | Fan | entityName, forceStrength, radius, direction, angle, angleFalloff, secondsBeforeTurningOff, secondsBeforeTurningBackOn |
6 | BreakableWall | entityName, hitThresholdVelocity, debrisPrefab (use null for the stock debris), respawnDelay |
7 | Switch | entityName, gateID, resetDelay, requiredHits |
8 | TimedGate | entityName, gateID, openDuration, startOpen |
9 | TrampolineFixed | entityName, bounceMultiplier, launchAngleOffset, maxRebounds, retainVelocity, extraBounceForce |
10 | RadialBouncePad | Same schema as TrampolineFixed |
12 | Hazard | entityName, isMoving, moveOffset, moveSpeed, autoLandOnContact |
All numeric and boolean fields should match the values stored in Unity; ranges and vector objects are serialized as nested { x, y } pairs to match the DTO schema.
Example manifest
The example below mirrors a Unity export and can be adapted for custom levels. It demonstrates the expected nesting, animation-curve representation, and field naming conventions.
{
"levelName": "Vault Gauntlet Showcase",
"entities": [
{
"type": -1,
"position": { "x": 4.5, "y": 1.0 },
"rotation": 0.0,
"scale": { "x": 1.5, "y": 1.0, "z": 1.0 },
"config": {
"entityName": "Goal Entry",
"useRandomPositioning": false,
"xRange": { "x": 4.0, "y": 5.0 },
"yRange": { "x": 0.8, "y": 1.2 },
"useRandomScaling": false,
"scaleRange": { "x": 1.0, "y": 1.5 }
}
},
{
"type": 0,
"position": { "x": -6.0, "y": 2.5 },
"rotation": 0.0,
"scale": { "x": 1.0, "y": 1.0, "z": 1.0 },
"config": {
"entityName": "Entry Portal",
"preserveVelocity": true,
"exitForce": 1.25,
"exitLocalPosition": { "x": 0.0, "y": 8.0 },
"exitLocalRotation": 45.0,
"secondsBeforeTurningBackOn": 2.5,
"secondsBeforeTurningOff": 3.5
}
},
{
"type": 1,
"position": { "x": -2.5, "y": 1.5 },
"rotation": 0.0,
"scale": { "x": 1.2, "y": 1.2, "z": 1.0 },
"config": {
"entityName": "Central Magnet",
"pullStrength": 24.0,
"radius": 18.0,
"secondsBeforeTurningBackOn": 4.0,
"secondsBeforeTurningOff": 2.0
}
},
{
"type": 2,
"position": { "x": -1.25, "y": -0.5 },
"rotation": -30.0,
"scale": { "x": 0.9, "y": 0.9, "z": 1.0 },
"config": {
"entityName": "Launch Pad",
"boostForce": 360.0,
"shouldRetainDirection": false,
"secondsBeforeTurningOff": 2.0,
"secondsBeforeTurningBackOn": 3.0
}
},
{
"type": 3,
"position": { "x": -4.0, "y": -2.0 },
"rotation": 90.0,
"scale": { "x": 2.5, "y": 0.5, "z": 1.0 },
"config": {
"entityName": "Left Wall",
"bounceForce": 6.0,
"maxVelocityAfterBounce": 11.0
}
},
{
"type": 4,
"position": { "x": 1.0, "y": -1.25 },
"rotation": 0.0,
"scale": { "x": 1.0, "y": 1.0, "z": 1.0 },
"config": {
"entityName": "Spinner Bridge",
"rotationSpeed": 40.0,
"clockwise": true,
"friction": 0.75
}
},
{
"type": 5,
"position": { "x": 0.5, "y": 2.0 },
"rotation": 0.0,
"scale": { "x": 1.0, "y": 1.0, "z": 1.0 },
"config": {
"entityName": "Gust Fan",
"forceStrength": 7.5,
"radius": 3.0,
"direction": { "x": 0.0, "y": 1.0 },
"angle": 75.0,
"angleFalloff": {
"serializedVersion": 2,
"m_Curve": [
{
"serializedVersion": 3,
"time": 0.0,
"value": 1.0,
"inSlope": 0.0,
"outSlope": 0.0,
"tangentMode": 0,
"weightedMode": 0,
"inWeight": 0.33333334,
"outWeight": 0.33333334
},
{
"serializedVersion": 3,
"time": 1.0,
"value": 0.6,
"inSlope": 0.0,
"outSlope": 0.0,
"tangentMode": 0,
"weightedMode": 0,
"inWeight": 0.33333334,
"outWeight": 0.33333334
}
],
"m_PreInfinity": 2,
"m_PostInfinity": 2,
"m_RotationOrder": 4
},
"secondsBeforeTurningOff": 4.0,
"secondsBeforeTurningBackOn": 2.0
}
},
{
"type": 6,
"position": { "x": 3.5, "y": -1.0 },
"rotation": 0.0,
"scale": { "x": 1.5, "y": 0.8, "z": 1.0 },
"config": {
"entityName": "Shatter Wall",
"hitThresholdVelocity": 6.5,
"debrisPrefab": null,
"respawnDelay": 4.0
}
},
{
"type": 7,
"position": { "x": 2.0, "y": 0.75 },
"rotation": 0.0,
"scale": { "x": 0.8, "y": 0.8, "z": 1.0 },
"config": {
"entityName": "Switch A",
"gateID": "gate-west",
"resetDelay": 1.5,
"requiredHits": 2
}
},
{
"type": 8,
"position": { "x": 2.75, "y": 1.25 },
"rotation": 0.0,
"scale": { "x": 1.2, "y": 0.6, "z": 1.0 },
"config": {
"entityName": "Timed Gate A",
"gateID": "gate-west",
"openDuration": 3.5,
"startOpen": false
}
},
{
"type": 9,
"position": { "x": 0.0, "y": -2.0 },
"rotation": 0.0,
"scale": { "x": 1.1, "y": 1.1, "z": 1.0 },
"config": {
"entityName": "Fixed Trampoline",
"bounceMultiplier": 6.0,
"launchAngleOffset": 10.0,
"maxRebounds": 2,
"retainVelocity": false,
"extraBounceForce": { "x": 0.0, "y": 2.0 }
}
},
{
"type": 10,
"position": { "x": -3.25, "y": 0.25 },
"rotation": 0.0,
"scale": { "x": 1.0, "y": 1.0, "z": 1.0 },
"config": {
"entityName": "Radial Trampoline",
"bounceMultiplier": 4.5,
"launchAngleOffset": 0.0,
"maxRebounds": 3,
"retainVelocity": true,
"extraBounceForce": { "x": 1.0, "y": 1.0 }
}
},
{
"type": 12,
"position": { "x": 5.0, "y": 2.25 },
"rotation": 45.0,
"scale": { "x": 0.9, "y": 0.9, "z": 1.0 },
"config": {
"entityName": "Hover Hazard",
"isMoving": true,
"moveOffset": { "x": 1.5, "y": 0.0 },
"moveSpeed": 1.2,
"autoLandOnContact": false
}
}
],
"numberOfTossesAllowed": 3,
"goalsToWin": 1,
"isTokenResetAfterToss": true
}
Hashing checklist
After publishing the manifest JSON to storage (Arweave, IPFS, HTTPS, etc.), compute its SHA-256 digest and supply the resulting 32-byte hex string to --manifest-hash. OGAL rejects:
- empty or missing manifest URIs,
- hashes that are not exactly 64 hex characters (32 bytes), and
- manifest URIs longer than 128 characters.
Treat these conditions as hard requirements when preparing Token Toss manifests.