Submitting a version
A submission declares the two files you are entering, by hash, and says which of your
models it is a version of. The API records a new version and answers with two
one-shot upload URLs. You PUT the files to them, then admission and an unrated
trial decide whether the version becomes that model’s active one.
The platform stores no bytes of its own and downloads nothing from you, so you upload the files yourself, and no process on the platform needs a fetch allowlist.
Create the model first. A submission never creates one: the platform refuses an unknown model with
unknown_model, so a typo cannot start a second lineage with its own version numbers and its own
rating.
Prepare the two files
| File | Content |
|---|---|
model.onnx | Self-contained ONNX model |
manifest.json | What your graph takes and returns, and one adapter per input |
There is no release to cut and no repository to own. The audit trail is the artifact. The
platform holds your bytes under models/<version_id>/model.onnx, a key it generates from the version
id. No one can retag that key, you cannot delete it, and every reader of that version resolves the
same one.
The short way: the site’s form
Sign in and go to /submit. Pick the model, pick the two files, press the button.
The form submits to the season the site is showing. With no model yet, it sends you to the New model
form on your profile first. The page then:
- reads each file and computes its SHA-256 with the browser’s own
crypto.subtle; - POSTs those two digests as the submission;
PUTs both files from your browser to the object store. Nothing passes through the site, which holds no bytes of its own.
The digest and the bytes come from one read, so they cannot disagree: the page hashes a buffer and uploads that same buffer. The page shows both digests as you pick the files, since they are the contract and a rejection would name them.
If a transfer fails (a blocked request, a proxy, a firewall), the platform still records the
version, and the page hands you the two curl commands for the URLs it was using.
The long way: the API
To script a submission, make the same calls yourself.
Compute the SHA-256 of each final file with sha256sum model.onnx manifest.json on Linux or
shasum -a 256 model.onnx manifest.json on macOS, and prefix each digest with sha256: in the
request. A whitespace change in JSON changes the hash too.
The API authenticates with the HttpOnly soma_session browser cookie and has no standalone API
tokens, so a script runs same-origin in the browser, and not from a shell. Replace the model id and
both illustrative hashes:
const response = await fetch('/v1/submissions', {
method: 'POST',
credentials: 'same-origin',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
game: 'ants',
season: 'summer-2026', // the season's slug; left out, the featured season
model: '<the model_id the create call returned>',
weights_hash: 'sha256:<64 hexadecimal digits>',
manifest_hash: 'sha256:<64 hexadecimal digits>',
note: 'wider second layer' // optional
})
});
const result = await response.json();
if (!response.ok) throw new Error(JSON.stringify(result));
console.log(result);
model is the model_id the create call returned, and GET /v1/models lists yours. season is
the slug of the season you enter; without it the version enters the game’s featured season, which
is not always the one you mean while several seasons run. A note is yours to read back on the
version’s page, at most a short paragraph.
The hashes must be real 64-digit values; the placeholders are invalid on purpose. A successful
response has status 201 and the fields version_id, model_id, model, version, status,
season, weights_hash, manifest_hash and upload. Store the version ID to follow this
version.
Upload the two files
The site’s form does this step for you. The 201 carries:
"upload": {
"model_onnx": "https://…/models/<version_id>/model.onnx?X-Amz-…",
"manifest_json": "https://…/models/<version_id>/manifest.json?X-Amz-…",
"expires_in": "30m"
}
PUT each file to its URL with the file as the whole body, and no headers or credentials:
curl -T model.onnx "$MODEL_URL"
curl -T manifest.json "$MANIFEST_URL"
Your version has thirty minutes from its first POST for both files to land. POST the
submission again with the same two hashes inside that window and the same version answers 200
with fresh URLs that expire when the window does, so a failed PUT costs you no version number.
After the window, or with a different hash while one is in flight, the platform answers
version_in_flight.
Nothing happens until both files land. Admission waits out the window for them. A version still
missing a file when the window closes is rejected with ARTIFACT_MISSING or MANIFEST_MISSING. A
first-time entrant is most likely to make this mistake.
The platform re-hashes what arrives. Admission refuses a file whose SHA-256 differs from what you declared. The re-hash makes a signed upload URL safe to hand out, and the platform checks your declaration twice: the node against the graph’s digest, and the database against the manifest’s.
Version numbers restart per model, and the platform assigns them. Your second model’s first version is v1. A lineage whose history began at 4 because you had an earlier model would carry a number the Version screen could not explain.
Season and candidate restrictions
A version enters the season its submission names, or the featured one; a season that is not open
refuses it with season_not_open. A restricted season admits only its participants, and a season’s
own administrators may not enter it (Seasons).
Each season declares its other restrictions itself (Seasons has the whole list), and
the platform reports each limit on models, versions and cooldown before the request as well as after
it, in the same words.
One testing or verified version per model per season may exist at a time. A competitor with
three models may have three versions in admission at once, and one model may have a version in
admission in each of two seasons; a season can also cap how many of yours are in flight together. A
model’s active version does not block a replacement submission to it.
GET /v1/games/{game}/submission reports your standing against every one of those rules before you
make a request, in the same words the refusal would use. It reads the featured season, or the one
?season=<slug> names.
Re-entering from an earlier season
An entry that stood in one season can enter another with the same files and no upload:
POST /v1/submissions/reenter with {game, model, season, from}. The new version is testing,
and admission and a trial judge it again under the new season’s rules, classes, memory and engine.
Moving to a new season has the details.
What happens next
Watch the version’s page, or read GET /v1/me/versions/{version_id}, which is yours alone and
never cached. The status starts at testing, with phase queued or verifying. Passing admission
moves it to verified and awaiting_trial; passing the trial promotes it to active, and from
then on the public GET /v1/versions/{version_id} shows it too (in a private season, only
GET /v1/private/versions/{version_id} does).
A request error and a later rejection are separate things. Missing hashes return 400;
season or duplicate/candidate conflicts return 409; an invalid session returns
401. Fix the request before retrying. If the request succeeded and a later check
fails, read reject_reason and the rejection reference.
Verification is asynchronous, so wait for it; submitting again will not hurry it.