Delivery contract — Game Value, player board

The reference for every call. To get from registering to a first score, follow onboarding first; for what the board measures and why, the overview; for how a published number is computed, the scoring page.

GET  /api/tasks/game-value-player/players       who a player_id is             public
GET  /api/tasks/game-value-player/appearances   who played, and by when        Bearer <key>
POST /api/tasks/game-value-player/ratings       deliver ratings                Bearer <key>
GET  /api/tasks/game-value-player/status        what you still owe             Bearer <key>

Contract version in force: 2026-09-game-value-player-v0. It is checked on every delivery.

All paths are relative to https://api.footballanalyticsarena.com. The interactive reference for every endpoint is at api.footballanalyticsarena.com/docs.

The unit here is an appearance, not a match. A round is about 1,400 of them and a season about 54,000. Almost everything below follows from that number: why the list is paged, why what you owe is counted rather than listed, and why there is a cap on deliveries per round.

Why you deliver, and nothing calls you

Participation is submission mode: your scheduled job posts ratings to the Arena. There is no endpoint for you to expose, nothing to keep running at our convenience, and no timeout of ours that can turn your infrastructure into part of your score.

Run it daily, not weekly. A midweek round is due at 23:59 the day after it is played, so a job that ran once a week would miss one deadline in every midweek week.

The player directory

GET /api/tasks/game-value-player/players — public, no key. Every player you may be asked to rate this season: player_id, name, first_name, last_name, birth_year, nationality, position, and the team_id and competition of the club he last appeared for, with his appearances so far. ?team_id= and ?competition= narrow it; a competition is named exactly as published, and anything else is refused with the valid names.

Map it onto your own database once, at enrolment, and re-read it weekly for new players. Every player_id the appearance list can send you is on it. Club ids are the team board's: GET /api/tasks/game-value-team/teams resolves them. No provider identifier is published.

The appearance list

GET /api/tasks/game-value-player/appearances

One row per player per appearance, for matches played and not yet past their deadline.

FieldMeaning
match_idthe Arena's match identifier
player_idan Arena player identifier, never a name
team_idthe side he played for
minutes_playedwhat the Arena recorded, on a 90-minute scale
is_starterwhether he started
competition, round, kickoffwhere and when it was played
deadlineby when a rating for it is due
updated_atwhen this row last changed

An unused substitute is not on it. He made no appearance, so there is nothing to rate and nothing to weight a rating by, and a rating for him is refused. A three-minute substitute is on it: he played. Declining to rate him is a product boundary the coverage column reports, not a row that does not exist.

The minutes are the Arena's own. You may disagree with them; deliver your rating anyway. They are published for two reasons: every participant is weighted by the same numbers, and holding this list, your own ratings and the published segment tables you can recompute your score yourself. A board whose arithmetic cannot be reproduced has to be trusted rather than checked.

Paging, and why the watermark is a cursor

The response carries count, more and next_since. Send next_since back to get what follows:

curl -s "$ARENA_API/api/tasks/game-value-player/appearances?limit=1000" \
  -H "Authorization: Bearer $ARENA_KEY"
# { "count": 1000, "more": true, "next_since": "2026-09-27T01:12:44+00:00|fam_…_0042|FAP0000731", … }

Mid-list next_since is an opaque cursor naming the exact row you stopped on. At the end of the list it is a plain updated_at, which is also what a job should keep between runs to read only what has changed since.

Both forms are accepted. A timestamp of your own means everything changed strictly after that instant.

Why a cursor and not just a timestamp. A match's appearances are all written at once, under one clock reading. A page that ends in the middle of a match therefore ends on an instant that several unserved rows also carry — and resuming on the instant alone would skip them, silently and for good. This is the single easiest thing to get wrong when integrating, and the reason the field is not simply a time.

Reads are free and count toward no delivery limit.

Delivering ratings

POST /api/tasks/game-value-player/ratings — one number per player per appearance.

{
  "task": "game_value_player",
  "contract_version": "2026-09-game-value-player-v0",
  "model_version": "my-ratings-2.1",
  "ratings": [
    { "match_id": "fam_2627_premierleague_0042", "player_id": "FAP0000731", "rating": 7.4 },
    { "match_id": "fam_2627_premierleague_0042", "player_id": "FAP0004120", "rating": 5.9 }
  ]
}

match_id and player_id come from the appearance list — never a name. Not one rating per segment: the Arena decides who was on the pitch for each segment, from the event stream.

Nothing is normalised. Out of 10, out of 100, z-scores: the score depends only on the order your ratings put players in, so rescaling changes nothing — and the Arena cannot be accused of having rescaled you wrongly.

A missing rating is missing, never zero. Declining to rate an appearance is a coverage fact and is published as one. A zero for a player you did not judge is a wrong number, and on a mean over eleven players a very wrong one.

?dry_run=true runs every check and stores nothing. The receipt is identical to a real delivery's, so a clean dry run guarantees acceptance. Dry runs count toward no delivery limit.

The receipt

The response is the receipt: an outcome per entry, the Arena's receipt time, and a checksum of the payload as received. Time is the Arena's; your timestamps are not read.

OutcomeMeaning
acceptedstored, and it counts
replaceda new version of a rating you had already sent, before the deadline
unchangedidentical to what we hold; kept, and not a new version
latea first rating after its deadline. Stored, and scored only toward matches kicking off after it arrived
rejectednot stored, with a reason

Rounds, deadlines and the freeze

Ratings for a round are due at 23:59 Paris on the day after its last match — the same rule, to the minute, as the team board. A vendor on both should not have to check.

  • Replaceable until the deadline. The full day exists for exactly this: an estimate on Sunday, a correction on Monday.
  • Frozen at it. After the deadline a first rating still lands, as late; a different rating for something you already rated is refused as frozen. The version in force at the deadline is the one that scores.
  • A rating counts only toward matches kicking off after it was due. You may legitimately deliver after a later match has been played, and a rating that can be sent once a result is known must not be allowed to inform it.

Ten deliveries per round

Stored deliveries, per participant, per round. Dry runs are exempt. Batch your ratings and check status before resending: a job that posts the same round nightly will spend the cap inside a fortnight and be refused on the eleventh.

What you still owe

GET /api/tasks/game-value-player/status answers for your own participant: every open round with something missing, its deadline, how many ratings are expected, how many arrived, how many are absent, which matches they are absent from, and how many of that round's deliveries remain.

What is missing is counted, not listed. A round is about 1,400 appearances, so a list of every outstanding one would be longer than the delivery that answers it. matches_incomplete says where the gaps are and how many each match has.

missed_this_season counts ratings that were not there when a deadline passed. Those can still be delivered, as late.

Reminders

At deadline − 12 h and − 3 h, to your contact address, and only when something is missing. A job that is working never hears from us.

Certification

Before anything counts: a dry run of one completed round with every starter in it, nothing rejected. Passing it is the moment you are enrolled, and ratings count for matches played after it.

Send the whole round — every appearance you rate, cameos included. The gate asks only that no starter is missing.

Why the XI and not every appearance. The XI is what a signal is built from: a segment is priced on the mean prior rating of the players on the pitch, and the starters are that set at kick-off. A participant that rates every starter can be scored. One that cannot is not exercising a product boundary, it is failing to cover the football.

The rule used to be every appearance, and it made a stated boundary unreachable: a source that declines to rate short substitute appearances was refused enrolment by the gate meant to prove its integration worked. Declining a cameo still costs coverage, which is published, and still costs the rated-XI floor at scoring time. Neither needs a closed door here.

Entries come back late on a completed round. That is expected and is not a failure.

Send one round per competition, in a single dry run. The competitions a certification proves are the ones you are scored on, so certifying on one league enters you for that league alone. Certifying again later adds a competition the first run could not prove: the enrolment moment never moves, and competitions are added to rather than replaced.

Coverage is all or nothing within a competition

You choose which competitions you enter. Inside one, you do not choose which appearances to rate: picking appearances is picking a sample, and no coverage column undoes it, because the players a rating system finds hardest to judge are exactly the ones it would have most reason to omit.

A partial XI is accepted — rating nine of a side's eleven is a product boundary, not an error — and it is published. Goalkeepers are not optional: a keeper's rating is as much of a product claim as an outfielder's, and an unrated keeper counts against the rated share like anyone else.

Refusals

The whole delivery is refused for: no valid key (401), a participant that has not entered or not certified (403), the wrong task (400), a contract_version not in force (409), or a body that is not shaped like the contract (422).

A single entry is rejected, leaving the rest of the delivery intact, for: an unknown match_id or player_id; a player who did not appear in that match; a rating that is not a finite number; a duplicate (match_id, player_id) within one delivery; a different rating for something already frozen; or a round whose ten deliveries are spent.

The split is deliberate: one bad entry never costs the others.

Versioning

2026-09-game-value-player-v0. A change to what is asked of you, or to what a field means, is a new version with a published date; a change to how the Arena computes a score is a scoring version and appears on the snapshot instead. Deliveries built against a version no longer in force are refused with 409 rather than reinterpreted.

  • Scoring — how a delivered rating becomes a published number, and how to recompute one.
  • Overview — what the board measures, and what it does not claim.
  • Reading a leaderboard — the two rankings, and why level is the expected first-season result.