# Publish a solver template

```http
POST /api/public/v1/contributor-network/items
Content-Type: application/json
```

**Scope**: `contributor-network.write`.

Publishes an immutable snapshot of one owned private Stage 2 solver template to the Contributor Network. The source template becomes read-only until its publication is withdrawn.

Publication is account-scoped. It requires completed Stage 2 enrollment, but not team ownership or an open formal-submission window. This operation does not create or replace the team's formal competition submission.

## Request fields

| Field | Type | Required | Description |
| :--- | :--- | :---: | :--- |
| `competitionId` | string | Yes | Use the Stage 2 competition ID shown in the example. |
| `kind` | string | Yes | Must be `solver-template`. |
| `source` | [Solver-template source](#solver-template-source) | Yes | Owned private template revision to publish. |
| `modelId` | string | Yes | Exact model ID from the live [competition catalog](./get-competition-detail.md#catalog-entries). |
| `remark` | string | No | Publication note of at most 5,000 characters. |
| `idempotencyKey` | string | Yes | Client-generated retry key, at most 200 UTF-8 bytes after trimming. |

`track`, `parentContributorNetworkItemId`, submission sources, inline sources, and other source shapes are not accepted. The server derives the publication track and lineage parent from the private template.

### Solver-template source

```ts
type SolverTemplateSource = {
  from: "solver-template";
  solverTemplateId: string;
  updatedAt: string;
};
```

`solverTemplateId` identifies an owned item returned by the [private solver-template library](./solver-templates.md). `updatedAt` must exactly match that template's current value; this prevents a concurrent edit from publishing source the caller did not review.

The server copies the template title and `solverCode` into the immutable publication and derives `track` from the template. It revalidates the source track, requested `modelId`, solver byte limit, and enrollment at the write boundary.

If the private template has a `sourceContributorNetworkItemId`, the server uses that immutable value as the lineage parent. The referenced item must still be an active Stage 2 `solver-template`; callers cannot replace or remove that provenance during publication. The parent may use a different track or model.

## Idempotency

Generate one `idempotencyKey` for the intended publication. Reuse it only when retrying the same normalized request after a timeout or uncertain response.

The first successful publication returns `201 Created`. An exact replay returns the original item with `200 OK` and does not generate another public item, public code, or lineage edge, consume quota, or change the private template again. Reusing the key with a different source revision, model, remark, competition, or kind returns `409 IDEMPOTENCY_CONFLICT`.

## Example request

```bash
SAIR_IDEMPOTENCY_KEY="publish-$(openssl rand -hex 16)"

curl -X POST "https://api.sair.foundation/api/public/v1/contributor-network/items" \
  -H "Authorization: Bearer $SAIR_API_KEY" \
  -H "Content-Type: application/json" \
  --data '{
    "competitionId": "mathematics-distillation-challenge-equational-theories-stage2",
    "kind": "solver-template",
    "source": {
      "from": "solver-template",
      "solverTemplateId": "sst_01JSTAGE200000000000000001",
      "updatedAt": "2026-05-04T16:58:20Z"
    },
    "modelId": "openai-gpt-oss-120b",
    "remark": "Baseline solver for the solo track.",
    "idempotencyKey": "'"$SAIR_IDEMPOTENCY_KEY"'"
  }'
```

## Response

Returns the complete item documented by [Get a Shared Solver Template](./get-contributor-solver.md). On the first successful request, the private template's `publishedContributorNetworkItemId` becomes the returned item ID.

```json
{
  "ok": true,
  "data": {
    "id": "cn_01JSTAGE200000000000000001",
    "publicCode": "EQT01-S00042",
    "competitionId": "mathematics-distillation-challenge-equational-theories-stage2",
    "kind": "solver-template",
    "title": "Equivalence prover",
    "remark": "Baseline solver for the solo track.",
    "author": {
      "sairId": "U-9f7e1c00",
      "displayName": "alice",
      "avatarUrl": null
    },
    "track": "solo",
    "modelId": "openai-gpt-oss-120b",
    "contentBytes": 135,
    "contentSha256": "f19dae70e4222d778477048200ad96d3f226a6e75069f8f41fd32cb1017dc565",
    "favoriteCount": 0,
    "viewerHasFavorited": false,
    "commentCount": 0,
    "directChildCount": 0,
    "publishedAt": "2026-05-04T17:02:11Z",
    "solverCode": "import json\ncontext = json.loads(input())\nprint(json.dumps({\"call\": \"judge\", \"verdict\": \"true\", \"code\": context.get(\"candidate\", \"\")}))",
    "lineage": {
      "depth": 0,
      "parent": null,
      "root": {
        "id": "cn_01JSTAGE200000000000000001",
        "publicCode": "EQT01-S00042",
        "available": true
      }
    }
  }
}
```

## Errors

| HTTP | Code | When |
| :---: | :--- | :--- |
| `400` | `MALFORMED_BODY` | The JSON body is malformed, has an invalid top-level or `source` shape, or contains an unknown field. |
| `400` | `IDEMPOTENCY_KEY_REQUIRED` | `idempotencyKey` is missing or empty. |
| `400` | `IDEMPOTENCY_KEY_TOO_LONG` | `idempotencyKey` exceeds 200 UTF-8 bytes. |
| `403` | `ENROLL_REQUIRED` | The caller has not completed Stage 2 enrollment. |
| `403` | `PUBLISH_CAP_EXCEEDED` | The account has no remaining active-publication capacity for this competition. |
| `404` | `NOT_FOUND` | The private solver template is missing or is not owned by the calling account. |
| `409` | `SOLVER_TEMPLATE_ALREADY_PUBLISHED` | The source already has an active Contributor Network publication. |
| `409` | `DUPLICATE_CONTENT` | The account already has an active Stage 2 item with identical solver source in this track. |
| `409` | `IDEMPOTENCY_CONFLICT` | The key was already used for a different publication request. |
| `409` | `SOURCE_REVISION_CONFLICT` | `source.updatedAt` no longer matches the private solver template. |
| `413` | `FILE_TOO_LARGE` | The source now exceeds the live Stage 2 solver byte limit. |
| `422` | `RESOURCE_FIELD_INVALID` | A competition, kind, source, model, remark, track snapshot, or lineage value is invalid. |
| `429` | `DAILY_RESOURCE_LIMIT_EXCEEDED` | The account exhausted the UTC-day Contributor Network creation quota for this competition. |

Read the current active cap from [Get my participation](./get-my-participation.md). Withdrawing an item releases active capacity but does not restore the daily creation quota. See [Errors](../../../errors.md) for shared authentication, scope, and rate-limit errors.
