# Create a Playground run

```http
POST /api/public/v1/competitions/mathematics-distillation-challenge-equational-theories-stage2/playground/runs
Content-Type: application/json
```

**Scope**: `playground.write`.

Creates a billable, asynchronous Stage 2 practice Run for one track, one solver snapshot, and an ordered set of equational problems. The caller must be enrolled in Stage 2. A practice Run does not create or replace a formal competition submission and does not affect the leaderboard.

The request copies source rather than referencing a saved template. Use `solverCode` from a template whose immutable `track` matches this request; later template edits, deletion, or publication do not change an existing Run.

## Track and solver protocol

`track` prevents a solver written for one execution protocol from being run under the other:

| Track | Execution protocol |
| :--- | :--- |
| `solo` | The service starts one solver process for each selected problem. The process reads one JSON startup object from standard input, writes newline-delimited `llm` or `judge` calls to standard output, and reads one JSON response after each call. |
| `marathon` | The service starts one solver process for the complete ordered problem snapshot. The process reads the JSONL file named by `JUDGE_MARATHON_MANIFEST`, appends `{ "id", "verdict", "code" }` JSONL records to `JUDGE_MARATHON_OUTPUT`, and may use the bundled `marathon_llm.call_llm` helper within the applied wall-time and token budgets. |

For Marathon Runs, the service derives and snapshots `budgetSeconds` and `budgetTokens` from the problem count, the requested compression ratio, and the live platform references. Clients cannot override the derived budgets directly.

## Idempotency

`idempotencyKey` is required because an uncertain retry must not create a second billable Run, reserve credits twice, or enqueue the solver twice.

- Generate one key for one logical Run creation and reuse it only when retrying the exact same request.
- The trimmed key must be non-empty and at most 200 UTF-8 bytes.
- The first successful creation returns `201 Created`.
- Replaying the same key with the same normalized request returns the recorded creation response with `200 OK` and performs no additional admission work.
- Reusing the key with a different request returns `409 IDEMPOTENCY_CONFLICT`.

Keys are isolated by calling account and competition.

## Request fields

| Field | Type | Required | Description |
| :--- | :--- | :---: | :--- |
| `idempotencyKey` | string | Yes | Retry key for this logical Run creation. |
| `track` | string | Yes | Exact, case-sensitive track ID from [Get competition detail](./get-competition-detail.md). |
| `solverCode` | string | Yes | Python source for the selected track's protocol. It must contain a non-whitespace character and fit within the live `submissionSpec.limits.maxBytes`. The exact source is snapshotted. |
| `solverName` | string | No | Display name after trimming, from 1 through 200 characters. Defaults to `solver.py`. |
| `problemIds` | string[] | Yes | Non-empty, ordered, unique individual problem IDs. Use `items[].id` from [Get a Playground problem set](./get-playground-problem-set.md) or an owned [custom problem](./custom-problems.md). |
| `allowedModels` | string[] | No | Non-empty, unique IDs from [List Playground models](./list-playground-models.md). Omit to snapshot every currently available model. |
| `marathon` | [MarathonConfiguration](#marathon-configuration) | Conditional | Required when `track` is `marathon` and forbidden for `solo`. |

Unknown top-level and nested fields are rejected. An explicit empty `allowedModels` array is invalid; omission is the only way to select the complete live catalog.

The service enforces the live problem-count, credit, cost, frequency, and platform-capacity constraints returned by [Get Playground usage](./get-playground-usage.md).

### Marathon configuration

| Field | Type | Required | Description |
| :--- | :--- | :---: | :--- |
| `compressionRatio` | number | Yes | Positive ratio no greater than `1`. It is the fraction of the full per-problem reference budget: for example, `0.5` requests half of that reference. |

## Input resolution and credit hold

A problem-set ID identifies a catalog container and is not valid in `problemIds`. Traverse the corresponding detail pages and send the selected problem IDs. The service verifies every public problem and custom-problem ownership, preserves request order, and snapshots the complete problem inputs and proof policy.

Run creation also revalidates the model catalog. If `allowedModels` is omitted, all models available at admission time are stored in the Run; later model or provider changes do not change its execution configuration. If no executable model is available, creation fails.

The temporary credit reservation is:

```text
creditsHeld = problemIds.length × creditsPerProblem
```

`creditsPerProblem` comes from the live [model catalog](./list-playground-models.md). Selecting more models does not multiply the reservation. A rejected creation request leaves no credit reservation or partial Run.

## Example request

Generate a new idempotency key for this logical Run and retain it unchanged for any uncertain retry. The example uses the Solo protocol and illustrative live catalog IDs:

```json
{
  "idempotencyKey": "stage2-run-3f91d0cb8f1f4b96b22f03cf944fdd76",
  "track": "solo",
  "solverCode": "import json\n\ncontext = json.loads(input())\nproblem = context[\"problem\"]\nprompt = \"Prove Equation 2 from Equation 1 in Lean: {} -> {}\".format(problem[\"equation1\"], problem[\"equation2\"])\nprint(json.dumps({\"call\": \"llm\", \"context\": {\"messages\": [{\"role\": \"user\", \"content\": prompt}]}}), flush=True)\ncompletion = json.loads(input())\nprint(json.dumps({\"call\": \"judge\", \"verdict\": \"true\", \"code\": completion.get(\"response\", \"\")}), flush=True)\njudge_result = json.loads(input())",
  "solverName": "equivalence-prover",
  "problemIds": [
    "prob_01JSTAGE200000000000000001",
    "prob_01JSTAGE200000000000000002"
  ],
  "allowedModels": [
    "openai-gpt-oss-120b"
  ]
}
```

## Response

The first successful creation returns `201 Created`. An exact idempotent replay returns the same recorded data with `200 OK`.

| Field | Type | Description |
| :--- | :--- | :--- |
| `runId` | string | Stable Run ID used by all later Run endpoints. |
| `status` | string | Initial status. A successful creation returns `pending`. |
| `track` | string | Snapshotted `solo` or `marathon` execution protocol. |
| `problemCount` | integer | Number of snapshotted problems. |
| `allowedModels` | string[] | Snapshotted public model IDs available to the solver. |
| `creditsHeld` | number | Temporary maximum credit reservation. |
| `marathon` | object \| null | Applied Marathon compression ratio and derived budgets, or `null` for a Solo Run. |

```json
{
  "ok": true,
  "data": {
    "runId": "run_01JSTAGE200000000000000001",
    "status": "pending",
    "track": "solo",
    "problemCount": 2,
    "allowedModels": [
      "openai-gpt-oss-120b"
    ],
    "creditsHeld": 0.2,
    "marathon": null
  }
}
```

For a Marathon Run, `marathon` contains the accepted `compressionRatio` and the derived integer `budgetSeconds` and `budgetTokens`. These values are part of the immutable execution snapshot.

`creditsHeld` is the temporary maximum reservation. After the Run reaches a terminal state, unused credits are released and the settled charge is available from [Get a Playground Run](./get-playground-run.md).

Use [Get a Playground Run](./get-playground-run.md) to poll status, or [List Playground Runs](./list-playground-runs.md) to discover the Run later.

## Errors

See [Errors](../../../errors.md). Endpoint-specific errors are:

| HTTP | Code | When |
| :---: | :--- | :--- |
| `400` | `MALFORMED_BODY` | The JSON, content type, idempotency key, or top-level request shape is invalid. |
| `403` | `ENROLL_REQUIRED` | The caller has not completed Stage 2 enrollment. |
| `403` | `RUN_BUDGET_EXCEEDED` | The required credit hold exceeds the available balance. |
| `404` | `NOT_FOUND` | The Stage 2 competition is unavailable. |
| `409` | `IDEMPOTENCY_CONFLICT` | The idempotency key was already used with a different request. |
| `422` | `RESOURCE_FIELD_INVALID` | `solverCode` or `solverName` violates its content or size constraint. |
| `422` | `RUN_PARAMS_INVALID` | A track, Marathon configuration, problem, model, duplicate, or live per-Run constraint is invalid. |
| `429` | `PLAYGROUND_RUN_LIMIT_EXCEEDED` | A live per-account or platform Run frequency, cost, credit, cooldown, or capacity limit was reached. |
