Quickstart
There is no service to run and no endpoint to expose. You fetch the open cases, produce one file of predictions, and submit it. An afternoon, and most of that is your model.
Throughout, $ARENA_API is the Arena's API base URL, and $ARENA_KEY is the key you receive when
you register.
1. Read the specification
Everything you implement against is one public JSON document:
curl -s "$ARENA_API/api/tasks/transfers/spec" | jq '.contract_version, .metric_masks'
Fetch it rather than transcribing it. Band grids move when a reference pool is re-cut, and a grid copied into your code is a grid that will be wrong one day without telling you. Every table in these pages is rendered from this same payload.
2. Fetch the open cases
curl -s "$ARENA_API/api/tasks/transfers/cases" > cases.json
Each case carries the player, the transfer, the destination competition, and — per metric — the exact percentile bands you are predicting over, with labels saying what each band means in real values.
{
"case_id": "sample-transfer-001",
"contract_version": "2026-09-transfers-distributional-v0",
"player": {
"age_at_transfer": 27,
"broad_role": "CM",
"citizenships": [
"Brazil"
],
"date_of_birth": "1999-03-14",
"foot": "right",
"height_m": 1.81,
"macro_role_family": "Midfield",
"name": "Sample Player"
},
"target": {
"competition": "Premier League",
"metrics": {
"key_passes": {
"bands": [
[
0,
46.7
],
[
46.7,
76.6
],
[
76.6,
91.7
],
[
91.7,
97.1
],
[
97.1,
100
]
],
"labels": [
"0",
"1",
"2",
"3",
"4+"
]
},
"pass_success": {
"bands": [
[
0,
9.8
],
[
9.8,
20.3
],
[
20.3,
30.1
],
[
30.1,
40.2
],
[
40.2,
50.4
],
[
50.4,
60.3
],
[
60.3,
70.2
],
[
70.2,
80.1
],
[
80.1,
90.2
],
[
90.2,
100
]
],
"labels": [
"0-69.7",
"70-76.7",
"76.9-80.6",
"80.7-83.3",
"83.6-85.7",
"85.9-88.1",
"88.2-90.2",
"90.3-92.8",
"92.9-97.1",
"97.2+"
]
},
"tackles_won": {
"bands": [
[
0,
31.4
],
[
31.4,
66.2
],
[
66.2,
87.4
],
[
87.4,
100
]
],
"labels": [
"0",
"1",
"2-3",
"4+"
]
},
"total_passes": {
"bands": [
[
0,
9.9
],
[
9.9,
20.1
],
[
20.1,
30.8
],
[
30.8,
40.4
],
[
40.4,
51.3
],
[
51.3,
61.5
],
[
61.5,
71.3
],
[
71.3,
81
],
[
81,
91.1
],
[
91.1,
100
]
],
"labels": [
"0-7",
"8-14",
"15-21",
"22-27",
"28-33",
"34-39",
"40-46",
"47-55",
"56-71",
"72+"
]
}
},
"peer_group": "CM",
"season_label": "26/27"
},
"task": "transfers",
"transfer": {
"date": "2026-08-08",
"fee_eur": 42000000,
"from_club": {
"competition": "Ligue 1",
"country": "France",
"name": "Sample Source FC"
},
"market_value_at_transfer_eur": 38000000,
"to_club": {
"competition": "Premier League",
"country": "England",
"name": "Sample Target FC"
},
"transfer_type": "permanent"
}
}
Resolve the players offline, once. Name, date of birth, destination club and transfer date are a strong key against any player database, and doing that matching here — at your own pace, able to flag anything ambiguous — is the whole reason this benchmark is submission-based rather than a live endpoint. A mis-resolution made under a request timeout is noise in your score, not a signal about your model.
Each case also carries registry_player_fa_id. It is stable: case_id changes with every
transfer, but that player id is the same next season. Map it to your own identifier once and keep
the mapping.
3. Build a submission
One file, one model version, one entry per case you are answering.
{
"task": "transfers",
"contract_version": "2026-09-transfers-distributional-v0",
"model_version": "acme-recruitment-4.2.1",
"predictions": [
{
"case_id": "tr_2627_premier-league_000184",
"predictions": {
"total_passes": [0.02, 0.04, 0.07, 0.10, 0.13, 0.16, 0.17, 0.15, 0.10, 0.06],
"pass_success": [0.05, 0.14, 0.26, 0.33, 0.22],
"key_passes": [0.38, 0.31, 0.18, 0.09, 0.04],
"tackles_won": [0.12, 0.21, 0.28, 0.25, 0.14]
},
"confidence": 0.72
},
{ "case_id": "tr_2627_laliga_000091", "refused": true, "reason": "unsupported_role" }
]
}
The rules each entry must satisfy:
- Exactly the metrics that case asked for — no more, no fewer.
- One non-negative float per band in that metric's own grid, in the same order. Band counts differ between metrics within a single case, from 3 to 10. Assuming they are all the same is the most common first mistake.
- Each array sums to 1 within
1e-6. Arrays are never renormalised for you: a distribution that does not sum to one is a bug in your model, and silently rescaling it would hide that. confidenceis optional and never affects your score.- To decline a case, mark it
refused. It is not scored as a wrong answer — it costs coverage.
Start with the base rate
Predict each metric's band widths. That is not a strategy, it is the floor: a band's percentile width is its share of peer appearances, so predicting the widths is predicting the league average, and it scores exactly zero.
Ship that first. It proves your resolution and your file format end to end before any modelling is involved.
4. Validate, then submit
Validation is a dry run. Nothing is stored, so run it as often as you like.
curl -s -X POST "$ARENA_API/api/tasks/transfers/submissions?dry_run=true" \
-H "Authorization: Bearer $ARENA_KEY" \
-H 'Content-Type: application/json' \
--data @submission.json | jq
You get a verdict per case — which will be scored, which are rejected and exactly why, which you
have already answered. Fix, re-validate, then drop dry_run to submit for real.
Only valid entries are stored. Rejected ones stay open, so a partial submission is normal: send forty cases today and thirty next week.
A case you have already answered is never overwritten. One answer per case, recorded with the model version that produced it, so a board row is always traceable to the model behind it.
You can join at any point in the season
Registration is open all season, and a case does not close because the player has started playing.
If you join in November, you forecast from that point on — using everything already observable, including the matches already played. That is a legitimate forecast, and it is how a benchmark stays open to a provider whose sign-off took three months.
What it costs is stated rather than hidden:
- Your prediction is scored only on appearances after you locked it. You may use the first ten matches as input; you are graded on match eleven onward. Nobody is scored on football they had already watched.
- Fewer scored appearances means a wider interval and longer as
provisional. - Coverage reflects the cases you did not answer.
- Your information level is published — how much of the season had already happened when you predicted — so a reader can tell a blind forecast from an informed one.
What happens next
Scoring runs weekly. Each week every case's realised distribution is rebuilt from the appearances in your own scoring window, your locked prediction is scored against it, and a snapshot is published and never rewritten.
See how to read a leaderboard for what the published columns mean.