# fradiation.json

> The optional file at the top of a build that sets the entry point, viewport and threads, and declares mutations and scoreboards. Every field, limit and icon name.

One JSON file tells the site what your game can unlock and score. The site is very literal about it.

The file is optional. Without it a build has no mutations and no scoreboards, the entry point is `index.html`, and the uploader asks you for a stage size.

## Where it goes

- Name it `fradiation.json`, lowercase, and put it at the top of the build, next to `index.html` (or whatever your `entry` is).
- In a zip where everything sits inside one top-level folder, that folder is dropped first. The file goes inside that folder, beside `index.html`.
- It is plain JSON. No comments, no trailing commas.
- The uploader reads it in your browser and does not store it as a build file. It isn't served from your game's origin.
- The [Unity kit](/docs/unity) copies `fradiation.json` from your project root into the build.

## What a web upload reads

Five fields: `entry`, `viewport`, `threads`, `mutations`, `boards`. Anything else in the file is ignored, including `$schema`.

Title, slug, description, engine, origin and AI disclosure come from the game's details form, on its Manage page. Setting `"title"` or `"engine"` in this file does nothing.

## Example

```json
{
  "$schema": "https://www.fradiation.games/fradiation.schema.json",
  "entry": "index.html",
  "viewport": { "width": 1280, "height": 720 },
  "threads": false,
  "mutations": [
    {
      "key": "first-blood",
      "name": "First Blood",
      "description": "Destroy your first drone.",
      "tier": "alpha",
      "icon": "crosshair"
    },
    {
      "key": "untouched",
      "name": "Untouched",
      "description": "Clear a level without taking damage.",
      "tier": "beta",
      "icon": "shield"
    },
    {
      "key": "waterfall-room",
      "name": "Damp Discovery",
      "description": "Find the room behind the waterfall.",
      "tier": "beta",
      "icon": "waves",
      "secret": true
    },
    {
      "key": "boss-hard",
      "name": "No Mercy",
      "description": "Beat the final boss on Hard.",
      "tier": "gamma",
      "icon": "crown"
    },
    {
      "key": "speedrun",
      "name": "Under Ten",
      "description": "Finish the game in under 10 minutes.",
      "tier": "gamma",
      "icon": "timer"
    },
    {
      "key": "everything",
      "name": "Completionist",
      "description": "Collect every item in the game.",
      "tier": "cherenkov",
      "icon": "trophy"
    }
  ],
  "boards": [
    {
      "key": "high-score",
      "name": "High score",
      "order": "desc",
      "format": "number",
      "max": 5000000
    },
    {
      "key": "fastest-clear",
      "name": "Fastest clear",
      "order": "asc",
      "format": "time",
      "max": 3600000
    }
  ]
}
```

The game refers to these by `key`: `fradiation.unlockMutation("first-blood")`, `fradiation.submitScore("fastest-clear", ms)`. See [The SDK](/docs/sdk).

## Fields

### Top level

| Field | Type | Default | Notes |
| --- | --- | --- | --- |
| `entry` | string | `"index.html"` | The HTML file to load. Must match a file in the build exactly: a relative path, no leading `./` or `/`. |
| `viewport` | object | none | Native size of the game. See below. Without it the uploader asks for a stage size (default 1280 x 720). |
| `threads` | boolean | `false` | Serve the build cross-origin isolated. See below. |
| `mutations` | array | `[]` | At most 30 per file, of which at most 3 `gamma` and 1 `cherenkov`. |
| `boards` | array | `[]` | At most 10 per file. |

### viewport

| Field | Type | Limits |
| --- | --- | --- |
| `width` | integer | 160 to 4096 |
| `height` | integer | 120 to 4096 |

The site letterboxes the game to this aspect ratio. Make the game fill its window (100% width and height) and handle resize. Don't hard-code a canvas size.

### threads

`true` serves the build cross-origin isolated (COOP `same-origin`, COEP `require-corp`), for engines that need `SharedArrayBuffer`: multithreaded Unity, threaded Godot. Anything loaded from another origin must then send CORP or CORS headers, so bundle everything. Leave it `false` unless your engine needs threads.

> [!WARNING]
> Threaded builds don't run on the site yet: the game pages that frame them aren't cross-origin isolated, so `SharedArrayBuffer` is unavailable inside the cabinet. Export without threads for now. See [Threads](/docs/builds#threads).

## Mutations

Mutations are a game's achievements. Each one is declared here and unlocked from game code with `unlockMutation(key)`.

| Field | Type | Default | Limits |
| --- | --- | --- | --- |
| `key` | string | required | Matches `^[a-z0-9-]{2,40}$`: lowercase letters, digits and dashes, 2 to 40 characters. This is the id your game passes to the SDK. |
| `name` | string | required | 1 to 40 characters |
| `description` | string | required | 1 to 120 characters |
| `tier` | string | required | `alpha`, `beta`, `gamma` or `cherenkov` |
| `icon` | string | required | One of the names in [Icons](#icons) |
| `secret` | boolean | `false` | See [Secret mutations](#secret-mutations) |

### Tiers

Tiers run alpha, beta, gamma, cherenkov, from common to rare. Pick the tier by how hard the mutation is to earn. The site does not measure that for you.

| Tier | Rads to the player | Cap per file |
| --- | --- | --- |
| `alpha` | 25 | none beyond the 30 total |
| `beta` | 50 | none beyond the 30 total |
| `gamma` | 100 | 3 |
| `cherenkov` | 250 | 1 |

Rads are paid the first time a signed-in player unlocks the mutation. Developers can unlock their own mutations but get no rads from their own games.

The caps apply to the file in each upload, and the file as a whole is limited to 30 mutations. Secret mutations count toward the same caps. An upload over a cap is refused with a message such as `fradiation.json: at most 3 gamma mutations.`

### Secret mutations

`"secret": true` hides a mutation until the player unlocks it. Before that it shows as a locked "?" with the name, icon and description withheld. Use it for things you don't want to give away.

### Icons

`icon` must be one of these 100 names, exactly as written. They are [Lucide](https://lucide.dev/icons) icon names.

```text
skull flame zap bomb crosshair shield shield-alert trophy crown star rocket
ghost heart swords sword target timer gem key eye radiation biohazard atom
bug sparkles medal award flag map compass footprints gauge infinity moon sun
snowflake droplet anchor bot radar satellite plane car hand-fist dumbbell
brain clover dice-5 puzzle gamepad-2 joystick coins package hammer wrench
lock unlock hourglass siren tent tree-pine mountain waves wind
cloud-lightning egg fish bird cat dog rabbit turtle axe shovel pickaxe
wand-sparkles scroll cake pizza beer coffee music headphones camera film tv
cpu battery-charging plug magnet flask-conical syringe pill bone ear hand
thumbs-up party-popper fire-extinguisher shell
```

Any other name is refused. The full list is also in the [JSON Schema](#json-schema).

## Boards

A board is a scoreboard. The game submits a number with `submitScore(key, value)` and the site keeps each player's best.

| Field | Type | Default | Notes |
| --- | --- | --- | --- |
| `key` | string | required | Matches `^[a-z0-9-]{2,40}$`. |
| `name` | string | required | 1 to 40 characters. |
| `order` | string | `"desc"` | `"desc"`: higher is better. `"asc"`: lower is better, for times. |
| `format` | string | `"number"` | `"number"`: shown as a whole number with separators. `"time"`: the value is in milliseconds, shown as `m:ss.mmm`. |
| `max` | number | none | Greater than 0. Scores above it are rejected as implausible. |

- Negative scores are always rejected. There is no minimum.
- `max` is a ceiling for both orders. On an `"asc"` time board it rejects slow times, not fast ones. Set it above the slowest run you would accept.
- A `"number"` board displays whole numbers. To keep two decimals, submit the value times 100.
- The first score a player posts on a board pays 5 rads, once per board. Developers get none.

## How changes apply

- Mutations and boards are upserted by `key` when a build is sealed, which is when its upload finishes. It does not wait for the build to go live. Changing a name, description, tier or icon changes it for the whole game at once.
- Renaming a `key` makes a new mutation or board. The old one stays, along with anything players unlocked or scored on it.
- Removing an entry from the file does not remove it from the game.
- Caps are checked against the file in the upload, not against everything the game has ever registered.
- A draft runs its newest build, so a draft picks up changes on the next upload.

## JSON Schema

The schema for this file is at `https://www.fradiation.games/fradiation.schema.json`. Put it first in the file and editors that support `$schema`, VS Code included, give you autocomplete and inline errors:

```json
{
  "$schema": "https://www.fradiation.games/fradiation.schema.json"
}
```

Give the same URL to a coding agent when it writes the file. See [Working with agents](/docs/agents) for the other machine-readable files.

## Common mistakes

- **Comments or trailing commas.** The uploader stops with `fradiation.json isn't valid JSON.`
- **Wrong place.** The file must sit at the top of the build, not in a subfolder of it.
- **Expecting the file to set the title.** `title`, `slug`, `description`, `engine`, `origin` and `ai` are ignored. Use the game's Manage page.
- **An icon that isn't in the list.** The upload is refused with a message that names the field, for example `fradiation.json: mutations.2.icon: ...`. Lucide has many more icons than this list. Only these 100 are allowed.
- **Keys with capitals or underscores.** `First_Blood` fails. `first-blood` works.
- **Too many gamma or cherenkov mutations.** The limits are 3 and 1. The message reads `fradiation.json: at most 3 gamma mutations.` or `at most 1 cherenkov mutation.`
- **A key in game code that isn't in the file.** The SDK does not throw. The call resolves with the same defaults as outside the site (`signedIn: false`), and nothing is kept. See [Testing](/docs/sdk#testing).
- **Times in seconds.** A `"time"` board reads milliseconds. Submitting `92` shows `0:00.092`.
- **`"order": "desc"` on a time board.** The slowest player would rank first. Use `"asc"`.
- **`max` set too low.** Real scores are rejected and the game gets the default result back.
- **`entry` written as `./index.html` or `/index.html`.** It must equal a file path in the build: `index.html`.
- **Renaming a key after launch.** Players who unlocked the old one keep it, and the new one starts at zero unlocks.
