About Invokation
Invokation provides matchmaking and skill-ratings for video games.
SBMM: Skill-Based Matchmaking
Definition
Skill-Based Matchmaking (SBMM) is the process of matching together players of similar skill.
Why do games use SBMM?
In short, SBMM increases engagement and retention. This effect has been observed consistently in experiments by game developers1 2 3 and in academic studies4.
The absence of SBMM leads to repetitive outcomes for players. High-skill players will dominate most of their matches, but low-skill players will consistently struggle. Matchmaking without SBMM leads to a greater variety of skills within each match, but less variety of individual outcomes.
For the vast majority (>80%) of players, the increased outcome variance with SBMM lets them experience more frequent highs, but with the trade-off of relatively more frequent lows for highly-skilled players.
Problems with SBMM
Reduced sense of mastery
The main problem with SBMM is that it reduces extrinsic feedback. As players increase in skill, they're matched with opponents of equal skill, which makes it harder to gauge absolute skill and personal improvement. Solving this requires deliberate transparency around MMR.
Lack of variety → Burnout
Tight SBMM can lead to always facing the same types of opponents, and to punishing players for trying off-META5 strategies.
If the MMR system does not adapt appropriately (or if there is no SBMM at all), players will be discouraged from experimentation or from simply enjoying the game. It's necessary to tune the tightness of matchmaking against the responsiveness of the MMR system to mitigate this.
Invokation can help you quantify the trade-offs and balance the two.
-
GameRant: Respawn Entertainment Dev Reveals Why SBMM Will Be the New Norm for Multiplayer Games ↩
-
Level Up: Leveraging Skill and Engagement to Maximize Player Game-Play in Online Video Games ↩
-
Most Effective Tactic Available ↩
Matchmaking-Rating (MMR)
MMR is the skill metric used by a matchmaking system.
Coming soon:
- Why naive systems like "average KD" are pretty bad.
- Technical details of common MMR systems:
- Elo
- Glicko
- TrueSkill/OpenSkill
- TrueSkill 2
- How and why IVK Skill works so well.
IVK Skill API 1.0
Getting Started
You should have received from us:
user_id
api_key
config_id
For most requests, you will also need to provide a match_id
. This can be an arbitrary string for testing.
REST API Overview
All requests use Basic Auth:
username: {{user_id}}
password: {{api_key}}
Base URL:
https://dev.ivk.dev/v1/mmr/{{config_id}}
e.g. to test credentials with cURL (and retrieve the configuration JSON):
curl https://dev.ivk.dev/v1/mmr/{{config_id}} -u "{{user_id}}:{{api_key}}"
Sending Match Results
Send the match results as a list of JSON objects in the body of the POST request. (see the examples)
POST https://dev.ivk.dev/v1/mmr/{{config_id}}/full/player/?match_id={{match_id}}
Request Parameters
Most parameters are optional and will fall back to default values when null
or omitted.
required
player_id
(str)- The unique identifier of the player.
header data
-
team_id
(str|int|null)- The local identifier of the player's team, e.g.
"Team 1"
. - Default: unique team_id per player.
- Default behavior is to treat each player as their own team, so
team_id
can be omitted for 1v1 or free-for-all modes.
- The local identifier of the player's team, e.g.
-
party_id
(str|int|null)- The local identifier of the player's party.
- Default: unique party_id per player.
- Default behavior is to treat each player as their own party, so
party_id
can be omitted for games that don't support parties.
pre-match data
mmr
(float|null)- The MMR of the player at the start of the match (the prior MMR).
- Default: default_mmr specified in the MMR configuration
played_frac
(float|null)- The fraction of the match the player was present.
- Default: 1.0
games_played
(int|null)- The previous number of games the player has played (ever, or just this season).
- Default: 0
- Not necessary if placement matches are disabled in the MMR configuration, but most MMR systems should use some form of placement.
post-match data
player_score
(float|null)- The player's score in this match.
- Default: 0
- It's up to the game to determine this value. The only requirement is that a higher score is better than a lower score.
- This value only matters if player performance is configured to have non-zero weight.
team_score
(float|null)- The player's team's score.
- Default: the first non-null team_score for the team, otherwise the sum of the player scores on the team (e.g. total kills)
- This value is normalized according to the MMR configuration, e.g:
- binary outcome: { loss => 0, win => 1 }
- margin-of-victory (considers number of rounds won, or difference in score)
optional overrides
outcome
(float|null)[0-1]- For games with custom score normalization or custom player/team models
- Default: calculated from player_score and team_score using IVK normalization and player/team performance model (recommended).
placement_frac
(float|null)[0-1]- Manually calculated placement fraction
- Default: calculated from games_played based on MMR configuration (recommended).
Response Attributes
The POST response includes summary data (see the examples) and a list of JSON objects per player in the player_list
field:
player_id
(str)- pass-through identifier of the player
mmr
(float)- prior MMR of the player
mmr_team
(float)- total MMR of the player's team as determined by the party and team models (not necessarily the sum of MMRs)
- can be shown directly in an after-action report
played_frac
(float)- pass-through value
placement_frac
(float)- the prior fraction of placement matches the player completed (0-1)
party_size
(int)- total number of players in the player's party
expected
(float)- the combined expected outcome for the player (0-1)
- predicted based on combined party, team, and player performance models
- can be used indirectly in an after-action report
outcome
(float)- the actual outcome for the player (0-1)
- based on the combined the normalized player and team scores
- can be used indirectly in an after-action report
player_expected
(float)- the expected individual outcome regardless of team (0-1)
- potentially clamped to a maximum value (typically 0.8) to guarantee minimum payoffs
player_expected_raw
(float)- the unclamped player_expected value (0-1)
- can be used indirectly in an after-action report
player_outcome
(float)- the personal outcome for the player (0-1)
- regardless of party or team factors
player_weight
(float)- how much personal performance factors into the final MMR update
- configured as a function of placement and/or mmr range
- typically higher during placement than regular matches
player_contrib
(float)- MMR change due to player performance
- can be used in an after-action report (sometimes. carefully.)
- can be negative even if team wins and/or
mmr_delta
is net positive - behavior depends heavily on the specific MMR configuration
team_expected
(float)- the expected team outcome based on party and team model (0-1)
- potentially clamped to a maximum value (typically 0.8) to guarantee minimum payoffs
team_expected_raw
(float)- the unclamped team_expected value (0-1)
- can be used indirectly in an after-action report
team_outcome
(float)- the team outcome (0-1)
- regardless of individual performance
team_weight
(float)- how much team performance factors into the final MMR update
- configured as a function of placement and/or mmr range
- typically lower during placement than regular matches
- often 1.0 after placement is complete in Ranked modes
team_contrib
(float)- MMR change due to team outcome
- can be used in an after-action report (sometimes. carefully.)
- can have different sign than player_contrib
- behavior depends heavily on the specific MMR configuration
mmr_delta
(float)- net MMR change after this match
- should be shown on after-action report (in Ranked modes)
mmr_final
(float)- final MMR after this match
- equivalent to
mmr + mmr_delta
- should be shown on after-action report (in Ranked modes)
Advanced
IVK Skill also has routes for calculating pre-match data, or for calculating in-match results (e.g. when a team or player is eliminated in a battle royale). BR has multiple possibilities for MMR though, including tracking separate encounter MMR (based on head-to-head kills) and survival MMR (based on time).
POST https://dev.ivk.dev/v1/mmr/{{config_id}}/pre/player/?match_id={{match_id}}
POST https://dev.ivk.dev/v1/mmr/{{config_id}}/partial/player/?match_id={{match_id}}
IVK Skill API 1.0 - Examples
We recommend using Bruno as an API client. (not Postman)
The most common route for sending match results:
POST https://dev.ivk.dev/v1/mmr/{{config_id}}/full/player/?match_id={{match_id}}
1v1 - Balanced Match
body - 1v1
[
// Team 1
{
"player_id": "T1P1",
"team_score": 30,
"player_score": 30,
"mmr": 400,
"games_played": 100
},
// Team 2
{
"player_id": "T2P1",
"team_score": 40,
"player_score": 40,
"mmr": 400,
"games_played": 100
}
]
response - 1v1
{
"success": true,
"player_count": 2,
"team_count": 2,
"result": [
{
"player_id": "T1P1",
"mmr": 400,
"mmr_team": 0.4,
"played_frac": 1,
"placement_frac": 1,
"party_size": 1,
"expected": 0.5,
"outcome": 0.009485174635513356,
"player_expected_raw": 0.5,
"player_expected": 0.5,
"player_outcome": 0,
"player_weight": 0.8,
"player_contrib": -19.583213144091705,
"team_expected_raw": 0.5,
"team_expected": 0.5,
"team_outcome": 0.04742587317756678,
"team_weight": 0.2,
"team_contrib": -4.43142779453245,
"mmr_delta": -24.014640938624154,
"mmr_final": 375.98535906137585
},
{
"player_id": "T2P1",
"mmr": 400,
"mmr_team": 0.4,
"played_frac": 1,
"placement_frac": 1,
"party_size": 1,
"expected": 0.5,
"outcome": 0.9905148253644867,
"player_expected_raw": 0.5,
"player_expected": 0.5,
"player_outcome": 1,
"player_weight": 0.8,
"player_contrib": 20.416786855908327,
"team_expected_raw": 0.5,
"team_expected": 0.5,
"team_outcome": 0.9525741268224333,
"team_weight": 0.2,
"team_contrib": 4.6200547419162215,
"mmr_delta": 25.036841597824548,
"mmr_final": 425.03684159782455
}
],
"debug": null
}
3v3 - Balanced Match
body - 3v3
[
//
// Team 1
//
{
"team_id": "T1",
"player_id": "T1P1",
"team_score": 30,
"player_score": 15,
"mmr": 500,
"games_played": 100
},
{
"team_id": "T1",
"player_id": "T1P2",
"team_score": 30,
"player_score": 10,
"mmr": 400,
"games_played": 100
},
{
"team_id": "T1",
"player_id": "T1P3",
"team_score": 30,
"player_score": 5,
"mmr": 400,
"games_played": 100
},
//
// Team 2
//
{
"team_id": "T2",
"player_id": "T2P1",
"team_score": 40,
"player_score": 20,
"mmr": 500,
"games_played": 100
},
{
"team_id": "T2",
"player_id": "T2P2",
"team_score": 40,
"player_score": 15,
"mmr": 450,
"games_played": 100
},
{
"team_id": "T2",
"player_id": "T2P3",
"team_score": 40,
"player_score": 5,
"mmr": 350,
"games_played": 100
}
]
response - 3v3
{
"success": true,
"player_count": 6,
"team_count": 2,
"result": [
{
"player_id": "T1P1",
"mmr": 500,
"mmr_team": 1.3,
"played_frac": 1,
"placement_frac": 1,
"party_size": 1,
"expected": 0.5717913636893147,
"outcome": 0.5694851746355133,
"player_expected_raw": 0.5897392046116434,
"player_expected": 0.5897392046116434,
"player_outcome": 0.7,
"player_weight": 0.8,
"player_contrib": 1.1530945269004178,
"team_expected_raw": 0.5,
"team_expected": 0.5,
"team_outcome": 0.04742587317756678,
"team_weight": 0.2,
"team_contrib": -1.1530945269004178,
"mmr_delta": -0.11530945269004178,
"mmr_final": 499.88469054730996
},
{
"player_id": "T1P2",
"mmr": 400,
"mmr_team": 1.3,
"played_frac": 1,
"placement_frac": 1,
"party_size": 1,
"expected": 0.46381049397388585,
"outcome": 0.3294851746355134,
"player_expected_raw": 0.4547631174673573,
"player_expected": 0.4547631174673573,
"player_outcome": 0.4,
"player_weight": 0.8,
"player_contrib": -2.1448756035963696,
"team_expected_raw": 0.5,
"team_expected": 0.5,
"team_outcome": 0.04742587317756678,
"team_weight": 0.2,
"team_contrib": -4.43142779453243,
"mmr_delta": -6.5763033981288,
"mmr_final": 393.4236966018712
},
{
"player_id": "T1P3",
"mmr": 400,
"mmr_team": 1.3,
"played_frac": 1,
"placement_frac": 1,
"party_size": 1,
"expected": 0.46381049397388585,
"outcome": 0.08948517463551336,
"player_expected_raw": 0.4547631174673573,
"player_expected": 0.4547631174673573,
"player_outcome": 0.1,
"player_weight": 0.8,
"player_contrib": -13.89480349005136,
"team_expected_raw": 0.5,
"team_expected": 0.5,
"team_outcome": 0.04742587317756678,
"team_weight": 0.2,
"team_contrib": -4.431427794532437,
"mmr_delta": -18.3262312845838,
"mmr_final": 381.6737687154162
},
{
"player_id": "T2P1",
"mmr": 500,
"mmr_team": 1.3,
"played_frac": 1,
"placement_frac": 1,
"party_size": 1,
"expected": 0.5717913636893147,
"outcome": 0.9905148253644867,
"player_expected_raw": 0.5897392046116434,
"player_expected": 0.5897392046116434,
"player_outcome": 1,
"player_weight": 0.8,
"player_contrib": 16.41043181553427,
"team_expected_raw": 0.5,
"team_expected": 0.5,
"team_outcome": 0.9525741268224333,
"team_weight": 0.2,
"team_contrib": 4.525741268224334,
"mmr_delta": 20.936173083758604,
"mmr_final": 520.9361730837586
},
{
"player_id": "T2P2",
"mmr": 450,
"mmr_team": 1.3,
"played_frac": 1,
"placement_frac": 1,
"party_size": 1,
"expected": 0.5181319340019225,
"outcome": 0.7505148253644867,
"player_expected_raw": 0.5226649175024031,
"player_expected": 0.5226649175024031,
"player_outcome": 0.7,
"player_weight": 0.8,
"player_contrib": 7.165081511622735,
"team_expected_raw": 0.5,
"team_expected": 0.5,
"team_outcome": 0.9525741268224333,
"team_weight": 0.2,
"team_contrib": 4.571473482662635,
"mmr_delta": 11.73655499428537,
"mmr_final": 461.73655499428537
},
{
"player_id": "T2P3",
"mmr": 350,
"mmr_team": 1.3,
"played_frac": 1,
"placement_frac": 1,
"party_size": 1,
"expected": 0.4108023896865164,
"outcome": 0.2705148253644867,
"player_expected_raw": 0.3885029871081455,
"player_expected": 0.3885029871081455,
"player_outcome": 0.1,
"player_weight": 0.8,
"player_contrib": -11.159640135958671,
"team_expected_raw": 0.5,
"team_expected": 0.5,
"team_outcome": 0.9525741268224333,
"team_weight": 0.2,
"team_contrib": 4.376526947614297,
"mmr_delta": -6.783113188344373,
"mmr_final": 343.2168868116556
}
],
"debug": null
}
3v3 - Leaver
body - leaver
[
//
// Team 1
//
{
"team_id": "T1",
"player_id": "T1P1",
"team_score": 30,
"player_score": 15,
"mmr": 500,
"games_played": 100
},
{
"team_id": "T1",
"player_id": "T1P2",
"team_score": 30,
"player_score": 10,
"played_frac": 0.5, // <-- was only in half of the match
"mmr": 400,
"games_played": 100
},
{
"team_id": "T1",
"player_id": "T1P3",
"team_score": 30,
"player_score": 5,
"mmr": 400,
"games_played": 100
},
//
// Team 2
//
{
"team_id": "T2",
"player_id": "T2P1",
"team_score": 40,
"player_score": 20,
"mmr": 500,
"games_played": 100
},
{
"team_id": "T2",
"player_id": "T2P2",
"team_score": 40,
"player_score": 15,
"mmr": 450,
"games_played": 100
},
{
"team_id": "T2",
"player_id": "T2P3",
"team_score": 40,
"player_score": 5,
"mmr": 350,
"games_played": 100
}
]
response - leaver
{
"success": true,
"player_count": 6,
"team_count": 2,
"result": [
{
"player_id": "T1P1",
"mmr": 500,
"mmr_team": 1.1,
"played_frac": 1,
"placement_frac": 1,
"party_size": 1,
"expected": 0.5293052378801512,
"outcome": 0.5694851746355133,
"player_expected_raw": 0.5897392046116434,
"player_expected": 0.5897392046116434,
"player_outcome": 0.7,
"player_weight": 0.8,
"player_contrib": 4.410431815534266,
"team_expected_raw": 0.2875693709541823,
"team_expected": 0.2875693709541823,
"team_outcome": 0.04742587317756678,
"team_weight": 0.2,
"team_contrib": -2.401434977766158,
"mmr_delta": 2.008996837768109,
"mmr_final": 502.0089968377681
},
{
"player_id": "T1P2",
"mmr": 400,
"mmr_team": 1.1,
"played_frac": 0.5,
"placement_frac": 1,
"party_size": 1,
"expected": 0.42132436816472235,
"outcome": 0.3294851746355134,
"player_expected_raw": 0.4547631174673573,
"player_expected": 0.4547631174673573,
"player_outcome": 0.4,
"player_weight": 0.8,
"player_contrib": -2.1448756035963883,
"team_expected_raw": 0.2875693709541823,
"team_expected": 0.2875693709541823,
"team_outcome": 0.04742587317756678,
"team_weight": 0.2,
"team_contrib": -2.3513906510635976,
"mmr_delta": -4.496266254659986,
"mmr_final": 395.50373374534
},
{
"player_id": "T1P3",
"mmr": 400,
"mmr_team": 1.1,
"played_frac": 1,
"placement_frac": 1,
"party_size": 1,
"expected": 0.42132436816472235,
"outcome": 0.08948517463551336,
"player_expected_raw": 0.4547631174673573,
"player_expected": 0.4547631174673573,
"player_outcome": 0.1,
"player_weight": 0.8,
"player_contrib": -13.894803490051348,
"team_expected_raw": 0.2875693709541823,
"team_expected": 0.2875693709541823,
"team_outcome": 0.04742587317756678,
"team_weight": 0.2,
"team_contrib": -2.351390651063578,
"mmr_delta": -16.246194141114927,
"mmr_final": 383.7538058588851
},
{
"player_id": "T2P1",
"mmr": 500,
"mmr_team": 1.3,
"played_frac": 1,
"placement_frac": 1,
"party_size": 1,
"expected": 0.6142774894984783,
"outcome": 0.9905148253644867,
"player_expected_raw": 0.5897392046116434,
"player_expected": 0.5897392046116434,
"player_outcome": 1,
"player_weight": 0.8,
"player_contrib": 16.410431815534242,
"team_expected_raw": 0.7124306290458177,
"team_expected": 0.7124306290458177,
"team_outcome": 0.9525741268224333,
"team_weight": 0.2,
"team_contrib": 2.401434977766153,
"mmr_delta": 18.811866793300396,
"mmr_final": 518.8118667933004
},
{
"player_id": "T2P2",
"mmr": 450,
"mmr_team": 1.3,
"played_frac": 1,
"placement_frac": 1,
"party_size": 1,
"expected": 0.560618059811086,
"outcome": 0.7505148253644867,
"player_expected_raw": 0.5226649175024031,
"player_expected": 0.5226649175024031,
"player_outcome": 0.7,
"player_weight": 0.8,
"player_contrib": 7.165081511622751,
"team_expected_raw": 0.7124306290458177,
"team_expected": 0.7124306290458177,
"team_outcome": 0.9525741268224333,
"team_weight": 0.2,
"team_contrib": 2.4257012653981826,
"mmr_delta": 9.590782777020934,
"mmr_final": 459.59078277702093
},
{
"player_id": "T2P3",
"mmr": 350,
"mmr_team": 1.3,
"played_frac": 1,
"placement_frac": 1,
"party_size": 1,
"expected": 0.45328851549567994,
"outcome": 0.2705148253644867,
"player_expected_raw": 0.3885029871081455,
"player_expected": 0.3885029871081455,
"player_outcome": 0.1,
"player_weight": 0.8,
"player_contrib": -11.159640135958677,
"team_expected_raw": 0.7124306290458177,
"team_expected": 0.7124306290458177,
"team_outcome": 0.9525741268224333,
"team_weight": 0.2,
"team_contrib": 2.322259331731681,
"mmr_delta": -8.837380804226996,
"mmr_final": 341.162619195773
}
],
"debug": null
}