feat(rust): Economics — pay coins, balances, staff adjust, the richest (PLAN_REDESIGNS §11.2)
All checks were successful
PR Checks / server-tests (pull_request) Successful in 31s
PR Checks / client-build (pull_request) Successful in 29s
PR Checks / frozen-manifest (pull_request) Successful in -1m29s

The site half of §11.2.2, on protocol 14:

- `rust.coins.pay` ("Pay coins"): irreversible, reversible none (D335). The
  recipient modes of rust.kit.entitle including `linked`, one payment per
  website user (D103's rule), an optional message with {amount}, a new
  `rust.coins` budget. Refused with "Economics is not installed on this
  server" in the dry run too (D199). Core's key goes to the plugin, so a
  retried step answers `repeat` and pays nobody twice.
- `rust_player_wipe_stats.balance`: the tally's balance, written as a value.
  A nameless balance frame (an offline player) moves nobody's last-seen.
  `economics.wiped` clears the wipe's balances, except at a new save.
- The richest: a Coins leaderboard column on one wipe of a server that
  reports Economics, `sort=balance`, and a `richest` title category
  (Magnate); both rank only above the starting balance (D336).
- GET /player/rust/economics (live, falling back to the last known), and
  GET/POST /admin/users/:id/rust/economics for staff — scoped to the user's
  own links, keyed per submission, an activity row with before and after.
- Client: the account page's Coins, the admin user page's Coins panel with
  its form, the leaderboard column, and Economics in the server's mod line
  (saying when its config could not be read).

Server 535/535 (14 new in test/economics.test.js), client 74/74,
routes.manifest regenerated against the pinned core (87 routes), swagger
fragment regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
2026-10-12 07:07:05 -05:00
parent 1777cb6ce1
commit 845caef817
32 changed files with 1907 additions and 35 deletions

View File

@@ -2205,6 +2205,140 @@
}
}
},
"/api/v1/admin/users/{id}/rust/economics": {
"get": {
"tags": [
"Admin · Users"
],
"summary": "A user’s coin balances (admin only)",
"description": "This user’s Economics balance on every server, per linked Steam account — live where the server answers, else the last balance it reported. A server without Economics is listed with the reason. The same shape as GET /player/rust/economics.",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "integer"
},
"description": "User id."
}
],
"responses": {
"200": {
"description": "The balances",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RustEconomicsBalances"
}
}
}
},
"500": {
"description": "Internal Server Error"
}
},
"security": [
{
"cookieAuth": []
},
{
"bearerAuth": []
}
]
},
"post": {
"tags": [
"Admin · Users"
],
"summary": "Deposit, withdraw or set a user’s coins on one server (admin only)",
"description": "Through the server’s Economics, for one Steam account this user has linked. A withdrawal above the balance is refused with the balance, because Economics takes all or nothing. `key` makes a resent form harmless: the game answers a key it has seen from the first reply and moves nothing. Recorded in the activity log with the balance before and after.",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "integer"
},
"description": "User id."
}
],
"responses": {
"200": {
"description": "Adjusted",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RustEconomicsAdjusted"
}
}
}
},
"400": {
"description": "A bad op or amount",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "No such server, or that Steam account is not linked to this user",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"409": {
"description": "Economics is not installed there, or the balance is too low to withdraw that much",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"500": {
"description": "Internal Server Error"
},
"503": {
"description": "The server could not be reached",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
},
"security": [
{
"cookieAuth": []
},
{
"bearerAuth": []
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RustEconomicsAdjust"
}
}
}
}
}
},
"/api/v1/admin/users/{id}/rust/links": {
"get": {
"tags": [
@@ -2477,6 +2611,38 @@
]
}
},
"/api/v1/player/rust/economics": {
"get": {
"tags": [
"Player · Rust"
],
"summary": "Your coin balances",
"description": "The caller’s Economics balance on every server, per linked Steam account (PLAN_REDESIGNS §11.2.2). Read live from each server; where a server does not answer, the last balance it reported (`source: last-known`), and null where neither knows. A server without Economics is listed with `economics: false` and the reason. `startingKnown: false` means the server’s Economics config could not be read and the starting balance shown is Economics’ default. Empty `accounts` with no linked account. Requires a session.",
"responses": {
"200": {
"description": "The balances",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RustEconomicsBalances"
}
}
}
},
"500": {
"description": "Internal Server Error"
}
},
"security": [
{
"cookieAuth": []
},
{
"bearerAuth": []
}
]
}
},
"/api/v1/player/rust/link": {
"post": {
"tags": [
@@ -2919,7 +3085,7 @@
"Public · Rust"
],
"summary": "The leaderboard for one Rust server",
"description": "Per-wipe when `wipe` is given, all-time otherwise. All-time is the per-wipe rows summed rather than a second set of counters, so a wipe splits a player’s history without ending it. `lastSeen` is withheld below the operator’s presence audience: a gather tally refreshes it every minute a player is on, so it would name who is online. Each row carries `titles`: the chat titles that player holds now, as `[{ text, color }]` in the order the game shows them, and an empty list for a player with none. Titles rank the CURRENT wipe whichever wipe the page asks for, and are set by the operator’s rules.",
"description": "Per-wipe when `wipe` is given, all-time otherwise. All-time is the per-wipe rows summed rather than a second set of counters, so a wipe splits a player’s history without ending it. `lastSeen` is withheld below the operator’s presence audience: a gather tally refreshes it every minute a player is on, so it would name who is online. Each row carries `titles`: the chat titles that player holds now, as `[{ text, color }]` in the order the game shows them, and an empty list for a player with none. Titles rank the CURRENT wipe whichever wipe the page asks for, and are set by the operator’s rules. On a server that reports Economics, and only for one wipe, each row also carries `balance` (its last known coin balance, null when none was reported) and the answer carries `economics: { startingBalance }` (PLAN_REDESIGNS §11.2.2); `economics` is null otherwise. `sort=balance` ranks the richest, and only balances above the starting balance are on it (D336).",
"parameters": [
{
"name": "id",
@@ -2943,7 +3109,7 @@
"name": "sort",
"in": "query",
"required": false,
"description": "kills, deaths, npcKills or playtime",
"description": "kills, deaths, npcKills, playtime, or balance (one wipe on a server with Economics)",
"schema": {
"type": "string"
}
@@ -4521,6 +4687,7 @@
"rockets",
"explosives",
"missions",
"richest",
"playtime",
"profilekills"
],
@@ -12842,6 +13009,343 @@
}
}
},
"RustEconomicsBalances": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "object"
},
"description": {
"type": "string",
"example": "Coin balances per server and linked Steam account (PLAN_REDESIGNS §11.2.2). `source` is `live`, `last-known` or null."
},
"properties": {
"type": "object",
"properties": {
"servers": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "array"
},
"items": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "object"
},
"properties": {
"type": "object",
"properties": {
"serverId": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "string"
},
"example": {
"type": "string",
"example": "main"
}
}
},
"server": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "string"
},
"example": {
"type": "string",
"example": "Main"
}
}
},
"economics": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "boolean"
},
"example": {
"type": "boolean",
"example": true
}
}
},
"reason": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "string"
},
"example": {
"type": "string",
"example": "Economics is not installed on this server"
}
}
},
"startingBalance": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "number"
},
"example": {
"type": "number",
"example": 1000
}
}
},
"startingKnown": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "boolean"
},
"example": {
"type": "boolean",
"example": true
}
}
},
"live": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "boolean"
},
"example": {
"type": "boolean",
"example": true
}
}
},
"accounts": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "array"
},
"items": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "object"
}
}
},
"example": {
"type": "array",
"items": {
"type": "object",
"properties": {
"steamId": {
"type": "string",
"example": "76561198000000001"
},
"name": {
"type": "string",
"example": "Wanderer"
},
"balance": {
"type": "number",
"example": 1500
},
"source": {
"type": "string",
"example": "live"
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
},
"RustEconomicsAdjust": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "object"
},
"required": {
"type": "array",
"example": [
"serverId",
"steamId",
"op",
"amount"
],
"items": {
"type": "string"
}
},
"properties": {
"type": "object",
"properties": {
"serverId": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "string"
},
"example": {
"type": "string",
"example": "main"
}
}
},
"steamId": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "string"
},
"example": {
"type": "string",
"example": "76561198000000001"
}
}
},
"op": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "string"
},
"enum": {
"type": "array",
"example": [
"deposit",
"withdraw",
"set"
],
"items": {
"type": "string"
}
},
"example": {
"type": "string",
"example": "deposit"
}
}
},
"amount": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "number"
},
"example": {
"type": "number",
"example": 500
},
"description": {
"type": "string",
"example": "0.01 to 1,000,000; 0 is allowed for set."
}
}
},
"key": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "string"
},
"example": {
"type": "string",
"example": "3f2a9c1e-7b4d-4e8a-9f10-2c6d5e8b7a41"
},
"description": {
"type": "string",
"example": "One per submission, so a resend moves nothing."
}
}
}
}
}
}
},
"RustEconomicsAdjusted": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "object"
},
"properties": {
"type": "object",
"properties": {
"before": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "number"
},
"example": {
"type": "number",
"example": 1000
}
}
},
"after": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "number"
},
"example": {
"type": "number",
"example": 1500
}
}
},
"repeat": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "boolean"
},
"example": {
"type": "boolean",
"example": false
}
}
}
}
}
}
},
"RustVisibilityUpdate": {
"type": "object",
"properties": {