# Authoring Match Day stages with a coding agent

You're reading this because a user pointed you (Claude, Codex, Copilot, Cursor — anything that fetches a URL on demand) at **`https://itsmatchday.app/AGENTS.md`** and asked you to help them author a USPSA stage. This page is the single source of truth for the JSON format the editor at **`https://itsmatchday.app/build/`** consumes, and how to hand a finished stage back to the user.

If you're a human, you can ignore this page. Open [the editor](/build/) directly.

---

## TL;DR

1. Emit a JSON object that matches the schema at **`https://itsmatchday.app/schema/v2/stage.json`** (JSON Schema 2020-12). Always set `"schemaVersion": 2`.
2. All distances are in **feet**. All angles in **degrees**. Origin `(0,0)` is the **bottom-left** corner of the bay, with **y=0 at the back of the bay (berm / target end, where AR markers sit)** and **y=bayDepthFeet at the shooter's end** (where Box A goes). `+y` goes from the markers TOWARD the shooter (uprange); `-y` is downrange (the direction bullets travel).
3. Hand the user a link to open the result in the editor. Two options:
   - **Hosted JSON (preferred):** post the file to a public URL (a GitHub gist's raw URL works perfectly) and give them `https://itsmatchday.app/build/?stage=<that-url>`.
   - **Inline:** base64url-encode the JSON and give them `https://itsmatchday.app/build/?stage=<base64>`. Fine for short stages; URLs over a few KB start to break in chat apps.

That's the whole loop. The rest of this document is the detail an agent needs to produce a stage that round-trips cleanly through the editor and the iOS app.

> **Going deeper?** If you're Claude Code (or anything that supports skills), there are two skills that wrap this raw schema in interview-driven workflows:
>
> - **[stage-builder](https://itsmatchday.app/skills/stage-builder/)** — design a single original USPSA stage from scratch, planned against master-shooter principles + safety rules, built incrementally in the editor with screenshots between chunks.
> - **[match-builder](https://itsmatchday.app/skills/match-builder/)** — compose a whole USPSA match (multiple stages, venue, MD attribution) and produce a `.usmatch` file. Picks a balanced selection from the bundled classifier set + any custom stages you name. Persists venue + MD profiles so you don't re-type them every month.
>
> Both install via `curl -sSL https://itsmatchday.app/skills/<name>.zip -o /tmp/sb.zip && unzip -o /tmp/sb.zip -d ~/.claude/skills/`.

> **Match-level deep-link:** the editor also accepts `?match=<https-url-or-base64>` to open a `.usmatch` file pre-loaded in the Match Builder dialog (mirrors `?stage=`).

---

## What is Match Day?

Match Day is an iOS app that overlays USPSA practical-shooting stages onto the real bay floor with AR. Stage Setter is the browser-based authoring tool that ships alongside it. A "stage" is a complete description of one course of fire: the bay dimensions, every prop (targets, walls, fault lines, steel, shooting boxes, etc.), the AR markers used for registration, and a structured briefing for shooters.

The user owns the stage. Stage Setter is local-first (autosaves to their browser); nothing you author needs to touch a server.

---

## Coordinate system & units

| | |
|---|---|
| Distances | **feet** (not meters, not inches). Use decimal feet — `2.6208333333333335` rather than `2 ft 7.45 in`. |
| Angles | **degrees**, where **0° = downrange (−y direction, toward the berm)** and **180° = uprange (+y direction, toward the shooter)**. Positive rotation is clockwise looking down from above. A target facing the shooter (so it can be shot) has `facingDegrees = 180`. |
| Origin | `(0, 0)` is the **bottom-left** corner of the bay. Looking down from above, the back of the bay (berm / target end) is at the top of your mental picture and the shooter's end is at the bottom. |
| `+x` | Runs to the **right** along the back of the bay, from `0` to `bayWidthFeet`. |
| `+y` | Runs **from the back of the bay (y=0, where markers and target stands sit) toward the shooter's end (y=bayDepthFeet, where Box A goes)**. So `+y` is the **uprange** direction; `-y` is **downrange** (the direction bullets travel). Paper targets sit at low y values (typically `y=1`); the shooting box sits at high y values (typically `bayDepthFeet - 2`). |

Every prop position must lie inside `[0, bayWidthFeet] × [0, bayDepthFeet]`. The editor's validator rejects anything outside the bay.

### Computing a facing angle from a target point

The forward direction of a prop with `facingDegrees = θ` is the unit vector `(sin θ, −cos θ)`. So if a prop's center is at `(Cx, Cy)` and you want it to aim at point `(Tx, Ty)`:

```js
facing = (Math.atan2(Tx - Cx, Cy - Ty) * 180 / Math.PI + 360) % 360
```

(The Python equivalent: `math.degrees(math.atan2(Tx - Cx, Cy - Ty)) % 360`.)

**Don't use `270 − atan2(Δy, Δx)`** — that formula looks plausible but produces facings mirrored across the shooter axis (your targets end up pointing AWAY from the intended target). This has bitten multiple prior CM 25-09 drafts.

### The schemaVersion trap

**Always set `"schemaVersion": 2`.** v1 stages used a different y-axis convention, and the editor silently re-flips y on save when it doesn't see `schemaVersion: 2`. An agent that omits the field — or leaves it as `1` — will produce stages whose AR placement is mirrored back-to-front from what the schematic shows. This has burned the project's curators repeatedly. Set it. Always.

---

## Producing a stage: the minimum viable object

```json
{
  "schemaVersion": 2,
  "id": "11111111-2222-3333-4444-555555555555",
  "name": "My Stage",
  "sport": "uspsa",
  "designer": "Jane Doe",
  "author": "Authored with Claude",
  "bayWidthFeet": 30,
  "bayDepthFeet": 50,
  "barrelColor": "white",
  "markers": [
    { "id": 0, "imageName": "marker_0", "physicalWidthMeters": 0.2032, "position": { "x": 0,  "y": 0 } },
    { "id": 1, "imageName": "marker_1", "physicalWidthMeters": 0.2032, "position": { "x": 30, "y": 0 } }
  ],
  "props": []
}
```

That validates. Now add props.

---

## Props

`props` is a flat array. Each prop is an object with a `kind` discriminator. The supported kinds and their required fields:

### `targetStand` — cardboard target on a stand

The two `baseEndpoint` coordinates are the chalk marks at the foot of the stand. `mounts[]` is the stack of cardboard on top (paper targets and/or no-shoots).

```json
{
  "kind": "targetStand",
  "id": "<uuid>",
  "label": "T1",
  "baseEndpointA": { "x": 5,  "y": 30 },
  "baseEndpointB": { "x": 6.5, "y": 30 },
  "facingDegrees": 180,
  "mounts": [
    {
      "type": "paperTarget",
      "bottomHeightFeet": 3.03,
      "facingOffsetDegrees": 0,
      "hardcover": null,
      "inverted": null
    }
  ]
}
```

- `mounts[].type`: `"paperTarget"` or `"noShoot"`.
- `bottomHeightFeet`: ground-to-bottom-of-A-zone in feet. A standard 5-ft shoulder is `≈ 3.03 ft`; a 4-ft shoulder is `≈ 2.62 ft`.
- `hardcover`: `null` | `"horizontalBottom"` | `"diagonalLowerLeft"` | `"diagonalLowerRight"` | `"tuxedo"`. Describes a hardcover pattern partially occluding the target.

### `wallSegment` — vista barrier wall

```json
{
  "kind": "wallSegment",
  "id": "<uuid>",
  "label": "W1",
  "endpointA": { "x": 0,  "y": 20 },
  "endpointB": { "x": 12, "y": 20 },
  "cutouts": []
}
```

`cutouts[]` are ports / windows. Each cutout has `distanceAlongWallFeet`, `widthFeet`, `bottomHeightFeet`, `topHeightFeet`.

### `faultLineSegment` — fault line on the ground

```json
{ "kind": "faultLineSegment", "id": "<uuid>", "endpointA": { "x": 4, "y": 45 }, "endpointB": { "x": 26, "y": 45 } }
```

### `steelTarget`

```json
{
  "kind": "steelTarget",
  "id": "<uuid>",
  "label": "S1",
  "position": { "x": 8, "y": 18 },
  "facingDegrees": 180,
  "subtype": "popper",
  "heightFeet": 4
}
```

`subtype`: `"popper"` | `"miniPopper"` | `"plate"` | `"roundPlate"` | `"roundPlate10"` | `"roundPlate12"` | `"rectangle18x24"`.

The last three are the official Steel Challenge (SCSA) faces — a 10″ round, a 12″ round, and an upright 18″W × 24″H rectangle — used by `sport: "scsa"` stages. `heightFeet` is the height from the ground to the **center** of the steel face (SCSA rounds sit at ~4.5 ft center for a 5′-to-top setup; the 18×24 at ~4.5 ft center for a 5′6″-to-top setup).

### `shootingBox`

```json
{
  "kind": "shootingBox",
  "id": "<uuid>",
  "label": "Box A",
  "position": { "x": 15, "y": 47 },
  "widthFeet": 3,
  "depthFeet": 3
}
```

`position` is the **center** of the box. Add `"facingDegrees": <n>` for a rotated box.

### `barrel`, `startPosition`, `activator`, `spanningNoShoot`, `table`

All follow the same pattern (see the [JSON schema](/schema/v2/stage.json) for required fields). `activator.subtype` is intentionally free-text — describe what to build, e.g. `"Texas star"`, `"single swinger"`, `"drop-turner"`.

---

## The `brief` block

Structured briefing the RO reads to the shooter. All fields optional; `startCondition`, `procedure`, `scoringType`, `startSignal`, and `endCondition` are the high-value ones.

```json
"brief": {
  "startCondition": "Standing in Box A, hands relaxed at sides, handgun loaded and holstered.",
  "procedure": "On the start signal, engage T1-T6 with two rounds each.",
  "scoringType": "comstock",
  "startSignal": "Audible",
  "endCondition": "Last shot",
  "penalties": "Per current edition USPSA Handgun Competition Rules.",
  "gotchas": ["Must engage all paper before any steel"]
}
```

`scoringType`: `"comstock"` | `"virginiaCount"` | `"fixedTime"` | `"limited"`.

---

## Reference measurements

Optional, but the build crew loves them. Each entry draws a labeled dimension line on the schematic. Use them to call out non-obvious spacings.

```json
"referenceMeasurements": [
  {
    "id": "<uuid>",
    "from": { "x": 3, "y": 1 },
    "to":   { "x": 5, "y": 1 },
    "displayText": "2 ft",
    "isDiagonal": false
  }
]
```

`displayText` is human-readable — `"2 ft"`, `"30 ft 7 in"`, etc.

---

## Handing the result to the user

When you're done, give the user a single link that opens their stage in the editor for review. Pick whichever fits the stage size:

### Option A — host the JSON, link the URL

Best for stages over ~2 KB and for anything the user might want to share. Post the JSON to a public URL that serves `Content-Type: application/json` with permissive CORS (`raw.githubusercontent.com` and `gist.githubusercontent.com` both do). Then:

```
https://itsmatchday.app/build/?stage=https://gist.githubusercontent.com/<user>/<id>/raw/stage.json
```

### Option B — inline base64

Encode the JSON with **URL-safe base64** (`+` → `-`, `/` → `_`, padding optional) and pass it in `?stage=`:

```
https://itsmatchday.app/build/?stage=eyJzY2hlbWFWZXJzaW9uIjoyLCJpZCI6Ij...
```

Reliable up to a couple of KB. Some chat clients mangle very long URLs.

### What happens when the user opens the link

The editor auto-loads the stage on page load and renders it on the canvas. If the URL fetch fails (CORS, 404, bad JSON) the user sees an `alert()` with the error and the URL — so use a stable host. From there they can edit, share to their phone, or download a clean copy.

---

## Validation before you ship

You can validate your output against the published schema with any JSON Schema validator:

```bash
npx ajv-cli validate -s https://itsmatchday.app/schema/v2/stage.json -d stage.json
```

Common gotchas the schema catches:

- Missing `schemaVersion: 2` (the silent y-flip trap)
- Prop coordinates outside `[0, bayWidth] × [0, bayDepth]`
- Mount missing `bottomHeightFeet` or `facingOffsetDegrees`
- `barrelColor` not one of `"white"` / `"blue"`
- Steel target missing `subtype` or `heightFeet`

The editor itself does additional checks at load time (UUID uniqueness, marker count ≥ 2). When in doubt, generate the link, open it, and look at the canvas — visual round-trip is the strongest validation.

---

## Reference stage — full worked example

A complete real-world stage: USPSA CM 99-08 *"Melody Line"* — 12 rounds, 6 paper targets with mixed hardcover patterns, one shooting box, a structured brief, and five reference measurements. Copy this, modify it, and you have a valid stage. (Schema for validation: [`https://itsmatchday.app/schema/v2/stage.json`](https://itsmatchday.app/schema/v2/stage.json).)

```json
{
  "schemaVersion": 2,
  "id": "D51DE622-D2FC-40A3-9D6E-2FEFB9BFC13B",
  "code": "CM 99-08",
  "name": "Melody Line",
  "sport": "uspsa",
  "designer": "Mike Davis (Modifications by US Design Team)",
  "author": "Match Day",
  "roundCount": 12,
  "bayWidthFeet": 15,
  "bayDepthFeet": 34,
  "barrelColor": "white",
  "briefing": null,
  "brief": {
    "startCondition": "Standing in Box A, back to targets, toes of both feet against rear fault line of Box A, both wrists above respective shoulders. Handgun is loaded and holstered as per ready condition in rule 8.1.1 and 8.1.2.",
    "procedure": "Melody Line is a 12-round, 60-point Virginia Count Classifier Course of Fire. There are six metric targets. The start signal is audible. The best two hits per target will score.\n\nThe start position is standing in Box A, back to targets, toes of both feet against rear fault line of Box A, both wrists above respective shoulders. Your gun will be loaded and holstered.\nPCC start position is standing in Box A, facing downrange, heels of both feet against the rear fault line of Box A, stock on belt, muzzle downrange, both hands on the loaded carbine, safety on.\n\nOn the start signal, from within the fault lines only, turn, then draw and engage T1-T6 with only one round per target, then make a mandatory reload, and engage T1-T6 with only one round per target.\nPCC: No turn",
    "stageProcedure": "Upon start signal, turn, then draw and from Box A engage T1-T6 with only one round per target, then make a mandatory reload and from Box A engage T1-T6 with only one round per target.",
    "buildNotes": "Set T1, T3, T4, and T6 to 5 feet high at shoulders. T2 and T5 are set so top of upper A zones are parallel with top of shoulder on adjacent target. Outer non-scoring edges of T1-T2 and T5-T6 butt together. Other targets are spaced two feet apart edge to edge. Shooting box is 3 feet by 3 feet. Hard cover on T3 & T4 is to bottom perforation of lower A zone. Hard cover on T2 & T5 is as shown.",
    "scoringType": "virginiaCount",
    "leaveShootingArea": false,
    "roundCountOverride": 12,
    "targetCountOverride": 6,
    "points": 60,
    "scoredHits": "Best 2 hits per paper",
    "startSignal": "Audible",
    "endCondition": "Last shot",
    "penalties": "Per current edition USPSA Handgun Competition Rules. Failure to perform mandatory reload will result in one procedural penalty per shot fired.",
    "gotchas": [
      "Failure to perform mandatory reload = one procedural penalty per shot fired",
      "Only ONE round per target per string (Virginia Count — extra hits/shots are penalized)",
      "Must turn before drawing (handgun divisions); PCC does not turn"
    ]
  },
  "markers": [
    { "id": 0, "imageName": "marker_0", "physicalWidthMeters": 0.2032, "position": { "x": 0,  "y": 0 } },
    { "id": 1, "imageName": "marker_1", "physicalWidthMeters": 0.2032, "position": { "x": 15, "y": 0 } }
  ],
  "props": [
    {
      "kind": "targetStand",
      "id": "3D0853A9-27D9-4D26-A36C-E9555678E0DD",
      "label": "T1",
      "baseEndpointA": { "x": 0, "y": 1 },
      "baseEndpointB": { "x": 1.5, "y": 1 },
      "facingDegrees": 180,
      "mounts": [
        { "type": "paperTarget", "bottomHeightFeet": 3.0316666666666663, "facingOffsetDegrees": 0, "label": null, "hardcover": null, "inverted": null }
      ]
    },
    {
      "kind": "targetStand",
      "id": "E3498D91-8736-4D82-84C7-D4C959F2B10C",
      "label": "T2",
      "baseEndpointA": { "x": 1.5, "y": 1 },
      "baseEndpointB": { "x": 3, "y": 1 },
      "facingDegrees": 180,
      "mounts": [
        { "type": "paperTarget", "bottomHeightFeet": 2.6208333333333335, "facingOffsetDegrees": 0, "label": null, "hardcover": "diagonalLowerRight", "inverted": null }
      ]
    },
    {
      "kind": "targetStand",
      "id": "8181F74E-5339-47AC-B0BA-2F86E73E0A6A",
      "label": "T3",
      "baseEndpointA": { "x": 5, "y": 1 },
      "baseEndpointB": { "x": 6.5, "y": 1 },
      "facingDegrees": 180,
      "mounts": [
        { "type": "paperTarget", "bottomHeightFeet": 3.0316666666666663, "facingOffsetDegrees": 0, "label": null, "hardcover": "horizontalBottom", "inverted": null }
      ]
    },
    {
      "kind": "targetStand",
      "id": "B201A3E5-C681-4256-9881-3FBD2DA228F4",
      "label": "T4",
      "baseEndpointA": { "x": 8.5, "y": 1 },
      "baseEndpointB": { "x": 10, "y": 1 },
      "facingDegrees": 180,
      "mounts": [
        { "type": "paperTarget", "bottomHeightFeet": 3.0316666666666663, "facingOffsetDegrees": 0, "label": null, "hardcover": "horizontalBottom", "inverted": null }
      ]
    },
    {
      "kind": "targetStand",
      "id": "37BDAF67-6DBC-423B-996D-4A37B26E4C07",
      "label": "T5",
      "baseEndpointA": { "x": 12, "y": 1 },
      "baseEndpointB": { "x": 13.5, "y": 1 },
      "facingDegrees": 180,
      "mounts": [
        { "type": "paperTarget", "bottomHeightFeet": 2.6208333333333335, "facingOffsetDegrees": 0, "label": null, "hardcover": "diagonalLowerLeft", "inverted": null }
      ]
    },
    {
      "kind": "targetStand",
      "id": "D57DD3B4-7FE4-4BCC-B54B-6B1446CB8E84",
      "label": "T6",
      "baseEndpointA": { "x": 13.5, "y": 1 },
      "baseEndpointB": { "x": 15, "y": 1 },
      "facingDegrees": 180,
      "mounts": [
        { "type": "paperTarget", "bottomHeightFeet": 3.0316666666666663, "facingOffsetDegrees": 0, "label": null, "hardcover": null, "inverted": null }
      ]
    },
    {
      "kind": "shootingBox",
      "id": "074AB982-AF19-45C5-AC5D-5FC04B30B7B1",
      "label": "Box A",
      "position": { "x": 7.5, "y": 32.5 },
      "widthFeet": 3,
      "depthFeet": 3
    }
  ],
  "referenceMeasurements": [
    { "id": "16B8F64F-27C5-4846-B549-AFAFE137DDE1", "from": { "x": 3,    "y": 1 }, "to": { "x": 5,    "y": 1 },  "displayText": "2 ft",       "label": null, "isDiagonal": false },
    { "id": "293CFB56-CB31-4BE2-AE4B-A761D461D197", "from": { "x": 6.5,  "y": 1 }, "to": { "x": 8.5,  "y": 1 },  "displayText": "2 ft",       "label": null, "isDiagonal": false },
    { "id": "5D0B8514-5DF1-4301-AEB3-B0C7162F3467", "from": { "x": 10,   "y": 1 }, "to": { "x": 12,   "y": 1 },  "displayText": "2 ft",       "label": null, "isDiagonal": false },
    { "id": "736A25FD-F0CF-44F1-B4D1-DF62466BB72A", "from": { "x": 8.5,  "y": 1 }, "to": { "x": 8.5,  "y": 31 }, "displayText": "30 ft",      "label": null, "isDiagonal": false },
    { "id": "659E5B9B-DC3C-4D82-A639-8F2D2F0817AA", "from": { "x": 9,    "y": 31 }, "to": { "x": 15,   "y": 1 }, "displayText": "30 ft 7 in", "label": null, "isDiagonal": true  }
  ],
  "forkedFrom": null
}
```

---

## Versioning & change policy

This is **schema v2**. If a v3 ships, it will live at `/schema/v3/stage.json`, and v2 will continue to validate at `/schema/v2/stage.json` indefinitely so existing agent tooling keeps working. Watch this page for updates.

Questions or bug reports → `hey@itsmatchday.app`.
