Quickstart

From nothing to your first submission, with curl. There is no service to run and no endpoint to expose.

The first two steps — registering and entering — are the same for every benchmark. After that this page follows Transfers, which fits in an afternoon: fetch the open cases, produce one file of predictions, and submit it. Game Value is a delivery your job makes every round, and it has its own path; step 2 hands you over to it.

Every command runs against the Arena's public API. Set its address once:

export ARENA_API=https://api.footballanalyticsarena.com

Every endpoint is also described in the API's own reference, where each one can be tried from the browser: api.footballanalyticsarena.com/docs.

1. Register

One identity for the whole Arena, whichever benchmarks you enter:

curl -s -X POST "$ARENA_API/api/participants" \
  -H 'Content-Type: application/json' \
  --data '{
    "vendor_name": "Acme Analytics",
    "leaderboard_name": "Acme v1",
    "short_description": "Distributional transfer projections",
    "contact_email": "team@acme.example.com"
  }' | jq

leaderboard_name is the name every board shows, unique across the Arena; a name already taken is refused with 409. short_description is optional. contact_email is required and never published: it is where reminders and delivery problems are sent.

The response carries your API key, beginning fa_. It is shown once and never again — only a hash is stored — so keep it somewhere your code can read it, then:

export ARENA_KEY=fa_...

You can also register from Join the Arena, which shows the key the same way.

2. Enter a benchmark

Registering says who you are. Entering says what you are competing in, and a submission to a benchmark you have not entered is refused with 403.

curl -s -X POST "$ARENA_API/api/participants/entries" \
  -H "Authorization: Bearer $ARENA_KEY" \
  -H 'Content-Type: application/json' \
  --data '{"task_slug": "transfers"}' | jq '.participation_mode'

It answers "submission": the Arena never calls you. Enter as many benchmarks as you like, one call each, with the same key. The slug for Transfers is transfers; for Game Value's team board, game_value_team.

Entering Game Value? Enter game_value_team the same way, then continue with onboarding from its step 3. Game Value is a standing delivery your job makes after every round, and nothing counts until you have certified — more than a quickstart can hold. Everything below this point is Transfers.

3. Read the Transfers 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.

4. 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.

5. 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:

  1. Exactly the metrics that case asked for — no more, no fewer.
  2. 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.
  3. 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.
  4. confidence is optional and never affects your score.
  5. 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.

6. 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.

Joining Transfers mid-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, on Tuesday at 06:00 Paris. 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.