# FIFA 17 FUT — Complete Endpoint Map (clean-room)

Status: 2026-08-02. Derived **entirely** from binaries we own (`CardsDLL_Win64_retail.dll`,
image base `0x180000000`) plus our own running client. **No leaked EA source used.**

This is the spec for rebuilding FIFA 17 Ultimate Team fully offline (and the Rosetta for
porting to FIFA 23). It maps the **complete FUT API surface** — ~100 `FutXServerResponse`
types the client can parse — with field-level detail for every endpoint, prioritizing the
core playable loop. Target: port into Rust `openfut-core` behind a FIFA-17 bridge.

## How this was produced (methodology)

- **Master atom dictionary**: dumped the name table at `0x1802d2760` straight from the DLL
  → **907 atoms** (`atom int → JSON key name`). Every SAX deserializer dispatches on these.
  Table extractor: `tools/atomdump.py` (regenerable); output `atoms.tsv`.
- **Deserializer locator (RECIPE)**: struct-name string → `VA = 0x1801e5000 + (fileoff − 0x1e4400)`
  → `.text` `lea r8` xref → the response object's vtable (**deserializer at vtable slot +0x08**)
  → walk the atom `cmp`/`sub`/`dec`+`je` ladder → translate atoms via `atoms.tsv` → classify
  each field by the leaf getter it calls.
- **Parser internals** (shared by ALL responses): key → FNV-1a (`0x180180d00`, seed
  `0x811c9dc5`) → atom → jump-table dispatch; unknown atom → value-SKIP `0x180135ff0`
  (**extra/unknown keys are always safe**). Leaf getters: int/num `0x1801c79d0`, bool
  `0x1801c7620`, string `0x1801c7aa0`. **Type fidelity is mandatory**: feeding a scalar
  getter an object/array desyncs the SAX reader → infinite tokenizer spin (freeze at
  `0x1801c7f1a`). Freeze-risk (must-be-object/array) fields are flagged per struct below.
- **Ladder caveat**: atom dispatch is a jump-ladder of *running-sum* `sub`/`dec` chains — the
  real atom is the accumulated sum, not the raw immediate. Field schemas are HIGH confidence;
  **HTTP verbs are often inferred** (the verb table isn't statically recoverable) — LOW
  confidence unless corroborated by `/tmp/utas_server.log`.

## Reversal coverage (7 feature groups, ~100 structs)

| Group | Structs | Fully reversed | Section |
|---|---|---|---|
| Transfer Market / Auction House | 12 | 9 | [Transfer Market](#transfer-market--auction-house) |
| Squad Building Challenges (SBC) | 8 | 5 | [SBC](#squad-building-challenges-sbc) |
| FUT Draft | 7 | 5 | [Draft](#fut-draft) |
| Match / Seasons / Tournaments | 19 | 9 | [Seasons](#match--seasons--tournaments) |
| Club / Cards / Consumables | 16 | 15 | [Club](#club--cards--consumables) |
| Store / Packs / Purchases | 6 | 5 | [Store](#store--packs--purchases) |
| User / Hub / Settings / Objectives / LB | 32 | 7 deep + 11 partial | [User](#user--hub--settings--objectives--leaderboards) |

"Fully reversed" = top-level field schema + types decoded HIGH-confidence. Nested card/squad
elements reuse the shared item (`0x18013fe00`) / squad (`0x18013d1f0`) parsers documented in
`CARD_SYSTEM.md`. Every struct in the sections has its deserializer VA, key list, and a
minimal known-good JSON — including ack-only (`{}`) responses.

## Shared record parsers (reused across groups)

| VA | Record | Used by |
|---|---|---|
| `0x18013fe00` | ITEM / card element (`itemData`) | club, squad, packs, purchased, market, draft, SBC |
| `0x18013d1f0` | full SQUAD object | squad, draft, SBC squad-challenge |
| `0x18013e410` | auction/trade record (`auctionInfo[]` element) | market search/watch/tradepile |
| `0x18013e7f0` | IS-list body `{auctionInfo,credits,total,duplicateItemIdList}` | market |
| `0x180135ff0` | value-SKIP (unknown keys) | all |

## ★ Highest-value findings (actionable now)

1. **Store "not available" — the `ut/v2/store` eligibility gate.** `FutStorePackQuantitiesServerResponse`
   (deser `0x1801758c0`) reads exactly one key `result` (atom `0x288`) → must be
   **`{"result":"SUCCESS"}`** (other enum values gate the store closed:
   `TOO_MANY_TOURNAMENTS`/`LOCKED_PERMANENT`/`LOCKED_RETRY`/`LOCKED_TROPHIES`). Pair with the
   Blaze purchase flags + the `>1024×768` `GetSystemMetrics` resolution check. See Store §.
2. **Match rewards live in `FutDestroyMatchServerResponse`** (`0x180121b60`), NOT `FutPlayGame`
   (ack-only). Coin fields at struct offsets `0x28–0x48`: `allCoins`/`matchCoins`/`seasonCoins`/
   `tournamentCoins`/`coins`/`boostConis`(sic)/`participationAward`. See Seasons §.
3. **`GetClubInfo` returns `user` (club-user stat array), not `itemData`.** Rendered cards come
   from **`FutViewCardsServerResponse` (`0x1801293d0`) on `ut/%s/item`** — explains why the
   current `/club` `itemData` is SKIP'd yet cards still render. See Club §.
4. **SBC requirements = generic `{eligibilityKey,eligibilityOperation,eligibilityValue}` triples**
   in an `elgReq` array — the key to modding challenges. See SBC §.
5. **Objectives = ManagerQuests**, client-driven with **no ServerResponse struct / no route**;
   gated by `enableObjectives` in settings. See User §.

## Catalog corrections to the current backend (`utas_server.py` / `fut_store.py`)

- Store `extPrice` inner keys are **`amount`/`currency`**, not `mtx`; pack identity is
  **`assetId`** (0x23), not `id` (harmless SKIPs otherwise).
- `ut/v2/store` must return `{"result":"SUCCESS"}` (currently unhandled → contributes to store error).
- ~~`GetUserMassInfo` top-level wrapper key is **`user`**~~ — **WRONG, corrected 2026-08-03.**
  The body is FLAT: `userInfo`(0x370)/`squad`(0x2cd)/`settings`(0x2bf)/`userData`(0x36d) are
  top-level keys. `user`(0x36c) only appears nested inside `clubUser`. Now served populated.
- No-op deserializers (bare `ret`) where `{}` always suffices: ChangeClubName, ActivateCard,
  SignLoanPlayer, and most ack responses.

---

# Deep sections
## Transfer Market / Auction House

Clean-room RE of the FIFA 17 CardsDLL (`cardsdll.dll`, image base `0x180000000`)
Internet-Shopping (IS = auction house / transfer market) response deserializers,
via the RECIPE.md method (name-string → `.text` `lea r8` xref → atom dispatch →
`atoms.tsv`). All VAs are static CardsDLL VAs.

### Shared parsers (the spine of every IS response)

| VA | Role |
|---|---|
| `0x18013e410` | **Core auction/trade RECORD deserializer** (one item in `auctionInfo[]`). Reversed deeply below. |
| `0x18013e7f0` | **Shared IS-list response body** — `{auctionInfo:[record…], credits, total, duplicateItemIdList}`. Search / WatchList / TradePile all tail-delegate to it. |
| `0x18013fe00` | Shared ITEM/card element deserializer (the `itemData` object; same one used by club/squad/pack). |
| `0x180135ff0` | value-SKIP (unknown atoms — safe to send extra keys). |
| Leaf getters | int/number `0x1801c79d0` · bool `0x1801c7620` · string `0x1801c7aa0`. |

Scalar-convert helpers seen: `0x1800d7b30` (num→int32), `0x1800d7b50` (num→bool/byte),
`0x180166380` (str→bidState enum), `0x180166bd0` (str→tradeState enum),
`0x180008120`/`0x180008020` (bounded string copy).

---

### ★ Core record: `auctionInfo[]` element — deserializer `0x18013e410`  (confidence: HIGH)

Key dispatch (atom in `edi`, FNV-1a via `0x180180d00`; ordered by atom value):

| atom | key | JSON type | getter | notes |
|---|---|---|---|---|
| `0x57` | `bidState` | string (enum) | `0x1801c7aa0`→`0x180166380` | e.g. `none`/`highest`/`outbid`/`buyNow` |
| `0x65` | `buyNowPrice` | int | `0x1801c79d0`→`0x1800d7b30` | |
| `0xc1` | `currentBid` | int | `0x1801c79d0`→`0x1800d7b30` | current highest bid |
| `0x116` | `expires` | int | `0x1801c79d0` (QWORD) | **seconds remaining** (not epoch) |
| `0x16b` | `itemData` | **nested OBJECT** | `0x18013fe00` | the card. **FREEZE-RISK: must be an object**, feeding a scalar desyncs the SAX reader (spin at `0x1801c7f1a`) |
| `0x2b6` | `sellerEstablished` | int | `0x1801c79d0`→`0x1800d7b30` | |
| `0x2b7` | `sellerName` | string | `0x1801c7aa0`→`0x180008120` | bounded copy, max 0x1e=30 chars |
| `0x2e6` | `startingBid` | int | `0x1801c79d0`→`0x1800d7b30` | |
| `0x2f4` | `coinsProcessed` | int/bool | `0x1801c79d0`→`0x1800d7b50` | truncated to byte |
| `0x331` | `tradeId` | int (64-bit) | `0x1801c79d0` (QWORD) | |
| `0x335` | `tradeState` | string (enum) | `0x1801c7aa0`→`0x180166bd0` | `active`/`closed`/`expired` |
| `0x380` | `watched` | bool | `0x1801c7620` | true if on watch list |

Not present at record level: `bid` (0x55), `seller` id, `offers`. Extra keys are
skipped safely.

Minimal known-good record:
```json
{
  "tradeId": 100000001,
  "itemData": { "id": 100000001, "resourceId": 1610612736, "assetId": 20801,
                "itemType": "player", "rating": 94, "preferredPosition": "ST",
                "untradeable": false, "itemState": "free" },
  "tradeState": "active",
  "buyNowPrice": 3000,
  "startingBid": 1500,
  "currentBid": 0,
  "bidState": "none",
  "expires": 3600,
  "sellerName": "OpenFUT",
  "sellerEstablished": 1,
  "watched": false,
  "coinsProcessed": 0
}
```

### Shared IS-list body — `0x18013e7f0`  (confidence: HIGH)

| atom | key | type | getter |
|---|---|---|---|
| `0x35` | `auctionInfo` | **array of records** (`0x18013e410` in a loop) | FREEZE-RISK: must be array |
| `0xc0` | `credits` | int | `0x1801c79d0`→`0x1800d7af0`→object setter |
| `0xec` | `duplicateItemIdList` | nested (array) `0x180138e10` | FREEZE-RISK: must be array/obj |
| `0x325` | `total` | int | `0x1801c79d0`→`0x1800d7b30` (→ `obj+0x60`) |

---

### The 12 response structs

All 12 are **GAPs** — `utas_server.py` currently has **no** `auctionhouse` / `trade`
/ `tradePile` / `watchList` / `marketdata` routes. The nested `itemData` card is the
one piece already served (by the existing `/club` + `item_def` machinery in
`utas_server.py`), so it can be reused verbatim inside these responses.

Path template `%s = "game/fifa17"`. Methods inferred from struct verb + endpoint.

| # | Struct | Deser VA | Method + Path (inferred) | Schema | Conf |
|---|---|---|---|---|---|
| 1 | **FutISSearchServerResponse** | `0x180163420` → `0x18013e7f0` | `GET ut/%s/auctionhouse?...` (market search) | `{auctionInfo:[record], credits, total, duplicateItemIdList}` | HIGH |
| 2 | **FutISStartServerResponse** | `0x180165d70` (dispatch `…e98`) | `POST ut/%s/auctionhouse` (list item for sale) | `{id:int}` (new tradeId) — only atom `0x15c`=`id` | HIGH |
| 3 | **FutISViewTradeServerResponse** | `0x1801644d0` (rec call `…461f`) | `GET ut/%s/trade/{id}` (view one auction) | `{auctionInfo:[record], credits}` — atoms `0x35`,`0xc0` | HIGH |
| 4 | **FutISWatchListServerResponse** | `0x180166130` → `0x18013e7f0` | `GET ut/%s/watchList` | `{auctionInfo:[record], credits, total}` | HIGH |
| 5 | **FutISWatchTradeServerResponse** | `0x180164cd0` | `PUT ut/%s/watchList` (add to watch list) | ack; parses per-item status enum ladder (`0x28/0x14/0x11/0x24`, `-1`), no atom-keyed body → `{}` known-good | MED (partial) |
| 6 | **FutISOfferTradeServerResponse** | `0x180165410` (rec call `…56cf`) | `POST ut/%s/trade/{id}/bid` (place bid) | `{auctionInfo:[record], credits}` (echoes updated auction) | HIGH |
| 7 | **FutISRemoveTradeServerResponse** | `0x1801648d0` | `DELETE ut/delete/%s/trade/{id}` (clear from trade pile) | ack, no atom-keyed body → `{}` | MED |
| 8 | **FutISRemoveWatchServerResponse** | `0x1801659f0` | `DELETE ut/delete/%s/watchList/{id}` | ack, no atom-keyed body → `{}` | MED |
| 9 | **FutGetTradePileServerResponse** | `0x180170810` → `0x18013e7f0` | `GET ut/%s/tradePile` | `{auctionInfo:[record], credits, total}` | HIGH |
| 10 | **FutRelistAllServerResponse** | `0x180164210` (req-ser `0x180164370`) | `PUT ut/%s/auctionhouse/relist` (relist all expired) | ack; response body minimal → `{}` (request-side serializer builds a tradeId list) | MED (partial) |
| 11 | **FutGetAuctionCountServerResponse** | `0x180163670` (dispatch `…83e`) | `GET ut/%s/auctionhouse` count (or `marketdata`) | `{count, maxAuctionsAllowed, offered, selling, sold}` — atoms `0xbc,0x1bf,0x1e5,0x2b8,0x2c9`, all int | HIGH |
| 12 | **FutGetSuggestedPricingServerResponse** | `0x180163bb0` (dispatch `…ffa`) | `GET ut/%s/marketdata?defId=…` (price bands) | `{defId, minPrice, maxPrice}` — atoms `0xcf,0x1c2,0x1ca`, all int | HIGH |

### Minimal known-good JSON per struct

```jsonc
// 1 FutISSearch  (GET auctionhouse)
{ "auctionInfo": [ <record> ], "credits": 100000, "total": 1, "duplicateItemIdList": [] }
// 2 FutISStart   (POST auctionhouse)
{ "id": 100000001 }
// 3 FutISViewTrade (GET trade/{id})
{ "auctionInfo": [ <record> ], "credits": 100000 }
// 4 FutISWatchList (GET watchList)
{ "auctionInfo": [ <record> ], "credits": 100000, "total": 1 }
// 5 FutISWatchTrade (PUT watchList)   -> {}
// 6 FutISOfferTrade (POST trade/{id}/bid)
{ "auctionInfo": [ <record> ], "credits": 99000 }
// 7 FutISRemoveTrade (DELETE trade)   -> {}
// 8 FutISRemoveWatch (DELETE watchList) -> {}
// 9 FutGetTradePile (GET tradePile)
{ "auctionInfo": [ <record> ], "credits": 100000, "total": 1 }
// 10 FutRelistAll (PUT auctionhouse/relist) -> {}
// 11 FutGetAuctionCount
{ "count": 0, "maxAuctionsAllowed": 100, "offered": 0, "selling": 0, "sold": 0 }
// 12 FutGetSuggestedPricing
{ "defId": 1610612736, "minPrice": 900, "maxPrice": 10000 }
```

### Freeze-risk summary (type fidelity is mandatory)
- `auctionInfo` → **array** (never object/scalar).
- `itemData` inside each record → **object** (the card; reuse `item_def`).
- `duplicateItemIdList` → **array**.
- `bidState`, `tradeState`, `sellerName` → **strings**.
- `credits`, `total`, `count`, `*Price`, `*Bid`, `expires`, `tradeId` → **numbers**.
- `watched` → **bool**.
Any scalar fed where an object/array is expected desyncs the tokenizer → hard
busy-loop freeze at `0x1801c7f1a` (same failure mode documented for
`userMassInfo`/squad in CARD_SYSTEM.md).

### Implementation notes for utas_server.py
- Add routes (all currently missing): `GET /auctionhouse` (search + count),
  `POST /auctionhouse` (start), `PUT /auctionhouse/relist`, `GET/POST /trade`,
  `GET /tradePile`, `GET/PUT /watchList`, `GET /marketdata`,
  `DELETE (ut/delete)/trade`, `DELETE (ut/delete)/watchList`.
- Every list response shares one builder: `{auctionInfo, credits, total}`.
- `<record>.itemData` = exactly the object returned by `item_def(rid)`.
- Ack endpoints (5,7,8,10) can safely return `{}` (bodies are non-keyed/ack).
## Squad Building Challenges (SBC)

Reversed from FIFA17 `cardsdll.dll` (base `0x180000000`) using the deserializer-reversal
recipe: struct name → `.rdata` VA → `.text` xref (`lea r8`) → deserializer function →
atom `cmp`/`sub`/`dec` ladder + jump-table → `atoms.tsv` key names → leaf getter type.
Getter fingerprints: `0x1801c7aa0`=string, `0x1801c79d0`=int/number, `0x1801c7620`=bool,
`0x180008120`=fixed-buffer string-copy (string), `0x180135ff0`=value-SKIP (safe unknown key),
peek+loop via `0x1801c7f10`/`0x1801369f0`/`0x18015a750`=nested object/array.

**Endpoint family:** `ut/%s/sbs` (`%s` = `game/fifa17`), base string @ fileoff `0x21d908`.
The baseline `utas_server.py` has **no `sbs*` routes at all** → every endpoint below is a **GAP**.

### Path ↔ response bindings (proven via path-template `lea r8` adjacent to the response-struct `lea r8` in each request-builder)

| Response struct | Method* | Path (under `ut/game/fifa17/`) | Builder site |
|---|---|---|---|
| FutSBCTagSetsServerResponse | POST/PUT | `sbs/sets/tag` | `0x180153fd1`→`0x18015405d` |
| FutLoadSetTypesServerResponse | GET | `sbs/challenge/%d/squad` | `0x1801545b9`→`0x18015469d` |
| FutSBCStartChallengeResponse | POST | `sbs/challenge/%d` | `0x1801552a9`→`0x18015531d` |
| FutSBCSubmitChallengeServerResponse | POST/PUT | `sbs/challenge/%d` (submit) | `0x1801618ff`→`0x18016196d` |
| FutSBCSetDataServerResponse | GET | `sbs/sets` (set-list / by category) | builder `0x18016fa7d` (no distinct path lea) |
| FutSBCLoadCategoryDetailsServerResponse | GET | `sbs/sets` | `0x18017a9a5`→`0x18017aa3d` |
| FutLoadSetChallengesResponse | GET | `sbs/setId/%d/challenges` | `0x18017b979`→`0x18017b9ed` |
| FutSBCSaveSquadChallengeServerResponse | PUT | `sbs/challenge/%d/squad` | `0x18017ce6f`→`0x18017cedd` |

\*Method column is inferred from FUT16/17 REST conventions (builder method-enum not decoded); path bindings themselves are byte-proven.

---

### 1. FutLoadSetChallengesResponse — CONFIDENCE: HIGH  ✅ fully reversed
Deserializer dispatch @ **`0x18017bbbb`** (key-iter `call 0x180141ee0`); wrapper/ctor `0x18017b9ed`.
Parses an **array of flat SET+CHALLENGE records** (each record carries both set-level and
challenge-level fields). Low atoms via `cmp r8d`/`sub` ladder; high atoms (`0x280–0x354`)
via byte+dword jump table at `0x18017c334`/`0x18017c310` (decoded from binary).

Method+path: **GET `ut/game/fifa17/sbs/setId/{setId}/challenges`**

| key | atom | type | notes / struct offset |
|---|---|---|---|
| challengeId | 0x074 | int | `[rdi+0x34]` |
| categoryId | 0x073 | int | `[rdi+0x38]` |
| index | 0x163 | int | `[rdi+0x3c]` |
| setId | 0x2bc | int | `[rdi+0x3c]`-cluster |
| type | 0x2c4 | **string→enum** | string-compared: `"OPEN_CHALLENGE"`→0, `"BRICK_CHALLENGE"`→2 → `[rdi+0x40]` |
| name | 0x1d0 | string | `[rdi+0x44]`, max 0x7f |
| description | 0x0d1 | string | `[rdi+0xc3]`, max 0xff |
| challengeImageId | 0x075 | string | `[rdi+0x1c8]`, max 0x64 |
| formation | 0x12b | **string** | mapped via `0x180166590` → `[rdi+0x22c]`. **FREEZE-RISK: must be a JSON string, not int** |
| endTime | 0x106 | int (epoch s) | `[rdi+0x240]`, day-scaled (cmp 0x16d=365) |
| repeatable | 0x280 | bool | `[rdi+0x239]` |
| trophyId | 0x2ee | int | `[rdi+0x1c4]` |
| status | 0x30e | string | hash-mapped state |
| timesCompleted | 0x322 | int | |
| squadId | 0x2dc | int | |
| tutorial | 0x33e | (nested) | |
| **awards** | 0x047 | **nested ARRAY** | reward objects (see shared records). **FREEZE-RISK: must be array** |
| **elgReq** | 0x0f7 | **nested ARRAY** | the SBC requirement/constraint list (see shared records). **FREEZE-RISK: must be array** |

All other high atoms (205 of them) route to the value-SKIP default → extra keys are safe.

Minimal known-good:
```json
[{"challengeId":1,"setId":1,"categoryId":0,"index":0,"type":"OPEN_CHALLENGE",
  "name":"League Basics","description":"Submit 11 players.","challengeImageId":"sbc_challenge_image_1",
  "formation":"f442","endTime":0,"repeatable":false,"trophyId":0,"status":"OPEN",
  "timesCompleted":0,"awards":[],"elgReq":[]}]
```

### 2. FutSBCSubmitChallengeServerResponse — CONFIDENCE: HIGH  ✅ fully reversed
Dispatch @ **`0x180161bda`**; ctor `0x18016196d`. Method+path: **POST `sbs/challenge/{challengeId}`** (submit).

| key | atom | type |
|---|---|---|
| challengeId | 0x074 | int |
| setId | 0x2bc | int |
| credits | 0x0c0 | int |
| preOrderPacks | 0x24b | int |
| recoveredPacks | 0x27b | int |
| grantedChallengeAwards | 0x14a | **nested array** (freeze-risk) |
| grantedSetAwards | 0x14b | **nested array** (freeze-risk) |

```json
{"challengeId":1,"setId":1,"credits":500,"preOrderPacks":0,"recoveredPacks":0,
 "grantedChallengeAwards":[],"grantedSetAwards":[]}
```

### 3. FutSBCStartChallengeResponse / squadChallenge record — CONFIDENCE: HIGH  ✅ fully reversed
Dispatch @ **`0x180155949`**; ctor `0x18015531d`. Method+path: **POST `sbs/challenge/{challengeId}`**.
Returns the **squadChallenge** record (the working squad for a challenge). This same record
parser is shared by FutLoadSetTypesServerResponse.

| key | atom | type |
|---|---|---|
| challengeId | 0x074 | int/nested |
| index | 0x163 | int |
| playerType | 0x23d | string |
| playerRequirements | 0x237 | **nested array** (per-slot constraint list; freeze-risk) |
| squad | 0x2cd | **nested array** of slot objects (freeze-risk) |

Slot object = `{index:int, playerType:string, playerRequirements:[...]}`.

```json
{"challengeId":1,"squad":[{"index":0,"playerType":"","playerRequirements":[]}]}
```

### 4. FutSBCLoadCategoryDetailsServerResponse — CONFIDENCE: HIGH  ✅ fully reversed
Dispatch @ **`0x18017ac08`**; ctor `0x18017aa3d`. Method+path: **GET `sbs/sets`**.

| key | atom | type |
|---|---|---|
| categoryId | 0x073 | int |
| name | 0x1d0 | string |
| priority | 0x250 | int |
| sets | 0x2be | **nested array** of set records (freeze-risk) |

```json
{"categoryId":0,"name":"Challenges","priority":0,"sets":[]}
```

### 5. FutSBCSaveSquadChallengeServerResponse — CONFIDENCE: MEDIUM-HIGH  ✅ fully reversed (minimal)
Dispatch @ **`0x18017d08a`**; ctor `0x18017cedd`. Method+path: **PUT `sbs/challenge/{challengeId}/squad`**.
Only one scalar field parsed:

| key | atom | type |
|---|---|---|
| id | 0x15c | int (saved squad id) |

```json
{"id":1}
```

### 6. FutLoadSetTypesServerResponse — CONFIDENCE: MEDIUM  ⚠️ partial
Dispatch @ **`0x180154d69`**; ctor `0x18015469d`. Method+path: **GET `sbs/challenge/{challengeId}/squad`**.
Parses an **array of squadChallenge templates** ("set types" = squad-building slot templates),
same key set as the squadChallenge record: `{challengeId(0x074), index(0x163), playerType(0x23d str),
playerRequirements(0x237 nested), squad(0x2cd nested)}`. Outer top-level wrapper (array framing at
`0x180154a2d`) is callback-driven; exact top-level envelope key not resolved.

```json
[{"challengeId":1,"index":0,"playerType":"","playerRequirements":[],"squad":[]}]
```

### 7. FutSBCSetDataServerResponse — CONFIDENCE: LOW-MEDIUM  ⚠️ partial
Dispatch @ **`0x18016ff2a`**; ctor `0x18016fa7d`. Method+path: **GET `sbs/sets`** (set-data / by category).
Top-level is a **callback array-parser** (`lea r8,[rsi+0x50]` vector-append at `0x18016fb5e`)
that appends **set records** parsed by the shared flat set-record deserializer; only one
top-level scalar was resolved:

| key | atom | type |
|---|---|---|
| reset | 0x283 | bool |
| (set records) | — | **nested array**; each element has the SET-level fields (setId, name, description, awards, repeatable, endTime, challenges, starRating, setImageId, timesCompleted, sortPriority) — reuse struct #1's field vocabulary |

```json
{"reset":false,"sets":[{"setId":1,"name":"League Basics","description":"",
  "repeatable":false,"endTime":0,"awards":[],"challenges":[]}]}
```

### 8. FutSBCTagSetsServerResponse — CONFIDENCE: LOW-MEDIUM  ⚠️ partial
Ctor `0x18015405d`; deserializer is a **callback-based array parser** (installs per-element
handler `0x180154280`, vector-append) rather than an atom `cmp` ladder — no scalar keys resolved
from the dispatch. Semantically it acknowledges a set-tag operation and returns the updated
tagged sets. Method+path: **POST/PUT `sbs/sets/tag`**.

```json
{"sets":[]}
```

---

### Shared record shapes

**elgReq — the SBC requirement/constraint list** (the tricky part; parsed in struct #1 @ `0x18017bd9d`,
vector at `[rdi+0x358]`). Array of constraint objects, inner dispatch @ `0x18017bde0`:

| key | atom | type |
|---|---|---|
| eligibilityKey | 0x0f2 | int (constraint selector) |
| eligibilitySlot | 0x0f4 | int/bool |
| eligibilityValue | 0x0f5 | int (target value) |
| eligibilityOperation | 0x0f3 | int (comparator; skip-handled here) |
| eligibilities | 0x0f1 | (container atom) |

Chemistry/rating/nation/league-count constraints (`teamChemistry 0x307`, `starRating 0x2e2`,
`sameNationCount 0x297`, `sameLeagueCount 0x296`, `sameClubCount 0x295`, `nationCount 0x1d4`,
`leagueCount 0x18c`, `clubCount 0x8b`, `playerCount 0x22f`, `chemistry 0x81`) are encoded
generically as `{eligibilityKey, eligibilityOperation, eligibilityValue}` triples, **not** as
named scalar fields on the record. **FREEZE-RISK: elgReq must be a JSON array of objects.**

**awards / grantedAwards** — nested array of reward objects (atoms: `rewardType 0x28e`,
`rewardValue 0x28f`, `rewardQuantity 0x28d`, `rewardMultiplier 0x28c`, `awardCount 0x40`,
`awardSet 0x45`, `awardSetId 0x46`, `prizeSet 0x253`). **FREEZE-RISK: must be array.**

**SET-level field vocabulary** (available atoms for a full set record):
`setId 0x2bc`, `name 0x1d0`, `description 0x0d1`, `challenges 0x76`, `challengesCount 0x78`,
`challengesCompletedCount 0x77`, `repeatable 0x280`, `endTime 0x106`, `awardSet 0x45`,
`setImageId 0x2bd`, `starRating 0x2e2`, `sortPriority 0x2cb`, `timesCompleted 0x322`,
`categoryId 0x73`, `priority 0x250`.

### Freeze-risk summary (type fidelity mandatory)
- `formation` = **string** (e.g. `"f442"`), never int.
- `awards`, `elgReq`, `squad`, `playerRequirements`, `grantedChallengeAwards`,
  `grantedSetAwards`, `sets`, `challenges` = **arrays/objects**, never scalar.
  Feeding a scalar getter an object/array desyncs the SAX reader → tokenizer spin
  (freeze at `0x1801c7f1a`).
- Unknown/extra keys are safe (value-SKIP handler `0x180135ff0`).
## FUT Draft

Clean-room reverse of the FIFA 17 CardsDLL (base `0x180000000`) FUT **Draft** response
deserializers. Method: located each `RS4:Fut*DraftServerResponse` name string, computed its
`.rdata` VA, found the `.text` factory (`lea r8,[name]`), then read the adjacent JSON
deserializer's atom dispatch (FNV key → atom → getter). Atoms translated via `atoms.tsv`.
**All 7 endpoints are a total GAP** — `tools/utas_server.py` has zero `draft` routes today.

Endpoint family: `ut/%s/draft/mode` (+ sub-paths below), `%s = "game/fifa17"`.

Shared sub-deserializers used by Draft:
- `0x18013fe00` — ITEM/card element deser (a full player card, same schema as club/squad `itemData`; see CARD_SYSTEM.md). **Must be a JSON object.**
- `0x18013d1f0` — SQUAD deser (the whole squad model: `formation`, `players[].itemData`, `manager`, `custom`, `kicktakers`…; identical to `GET /squad/0` LoadActiveSquad). **Must be a JSON object.**

Leaf getters: int/num `0x1801c79d0`, bool `0x1801c7620`, string `0x1801c7aa0`; unknown-key SKIP `0x180135ff0` (extra keys are safe). Feeding a scalar getter an object/array desyncs the SAX reader → tokenizer freeze — so every field flagged **[freeze-risk]** below MUST be emitted as the right container type (or omitted entirely).

---

### 1. FutGetDraftChoicesServerResponse  — CONFIDENCE: HIGH (fully reversed, incl. deep choice record)
- Struct name `.rdata` `0x180225ea0`; factory `0x18014f250`; **deserializer `0x18014f2d0`**.
- Method/Path: **GET** `ut/game/fifa17/{champId}/draft` (path fragment `"/%d/draft"` @0x2252c8). Returns the choices offered for the current pick.
- Top-level keys (ordered):
  | key | atom | type | notes |
  |---|---|---|---|
  | `choices` | 0x83 | **array** [freeze-risk] | array of choice records (below) → vec@+0x30 |
  | `positionid` | 0x244 | int | @+0x28 |
  | `tier` | 0x315 | int | @+0x2c |
- **Choice record** (each element of `choices`, element size 0x20; sub-dispatch @0x18014f44c):
  | key | atom | type | notes |
  |---|---|---|---|
  | `formation` | 0x12b | **string** | formation-choice rounds (e.g. `"f442"`); parsed str→id |
  | `index` | 0x163 | int (byte) | choice slot index 0..n → byte@+0x28 |
  | `itemData` | 0x16b | **object** [freeze-risk] | the offered player card, parsed by ITEM deser `0x18013fe00` |
  So a pick round is either a **formation** round (choices carry `formation`) or a **player** round (choices carry `itemData` + `index`), with the round's slot given by top-level `positionid` and `tier`.
- MINIMAL known-good (player round, 5 choices for one position):
```json
{"positionid":0,"tier":1,
 "choices":[
   {"index":0,"itemData":{"id":1000001,"resourceId":20801,"assetId":20801,"itemType":"player","rating":94,"preferredPosition":"ST","nation":38,"teamid":243,"leagueId":53,"rareflag":1,"untradeable":true,"attributeList":[{"index":0,"value":90}],"itemState":"free","owners":1,"contract":7,"fitness":99}}
 ]}
```
  Formation round: `{"positionid":0,"tier":1,"choices":[{"index":0,"formation":"f442"},{"index":1,"formation":"f433"}]}`.

---

### 2. FutGetDraftCurrentStateServerResponse — CONFIDENCE: HIGH (fully reversed, incl. roundsInfo element + squad delegation)
- Struct name `0x180224200`; factory `0x180146cc0`; **deserializer `0x180147070`** (begin-obj @0x1801470f2, dispatch @0x18014715c).
- Method/Path: **GET** `ut/game/fifa17/squad/mode/draft/state?mode=ONLINE` | `?mode=SINGLE_PLAYER`.
  **CORRECTED 2026-08-04 from a LIVE CAPTURE.** The path above previously omitted the
  `squad/mode` segment. The real URL is `ut/%s/squad/mode` (template @`0x18021e7f8`)
  with `/draft/state` appended, which is why it is invisible to the request-template
  table and why the generic `/squad` route swallowed it. The suffix is appended to a
  caller-supplied buffer by `FUN_180146ac0`, which has no resolvable callers, so the
  full path is established by the capture in `REBUILD_RESEARCH.md` §15, NOT statically.
- Top-level keys:
  | key | atom | type | notes |
  |---|---|---|---|
  | `squad` | 0x2cd | **object** [freeze-risk] | drafted squad, SQUAD deser `0x18013d1f0` |
  | `entranceCriteria` | 0x108 | **object** [freeze-risk] | nested object (safe to omit) |
  | `gamesWonCurrentMatch` | 0x13b | int | @+0x60 |
  | `roundsInfo` | 0x293 | **array** [freeze-risk] | array of round records (below), elem deser `0x180146eb0`, elem size 0x20 → vec@+0x80 |
  | `squadState` | 0x2d5 | string | e.g. `"DRAFTSQUAD_ON"` |
  | `stateParam1` | 0x2ee | string→enum | @+0x5c (default 5) |
  | `stateParam2` | 0x2ef | string→int | @+0x64 |
- **Round record** (`roundsInfo[]`, all scalar; deser `0x180146eb0`):
  `round`(0x290,int@+0xc), `score`(0x29a,int@**+0x10**), `opponentScore`(0x200,int@+0x14), `penaltyScore`(0x217,int@**+0x18**), `opponentPenaltyScore`(0x1fd,int@+0x1c), `opponentId`(0x1fc,int/long@+0x0), `difficulty`(0xd4,string enum@+0x8).
  (`score` and `penaltyScore` offsets were SWAPPED here until 2026-08-04.)
- **THE ROOT CONTAINER IS A JSON ARRAY.** `0x180147070` initialises, discards two
  tokens, then tests `if (t3 != 0xd)` around a `do { ... } while (t != 0xd)` element
  loop. Handed a top-level OBJECT it never reaches its exit condition and spins in the
  inner `while (t != 10)` loop while the tokenizer returns EOF forever: process alive,
  no crash dump, no dialog. That is the hang observed live on 2026-08-03.
- **The body previously printed here was a HANG RECIPE** and is replaced below. It was
  object-root, it used the spelling `DRAFTSQUAD_ON` which is NOT an accepted value of
  the `squadState` enum, and it embedded a full `squad` object. Anyone who served it
  would have reproduced the exact freeze this entry was supposed to help avoid.
- MINIMAL known-good (verified: two agents independently walked this body through the
  deserializer token by token to a clean exit in 16 reads):
```json
[{"squadState":"INVALID","stateParam1":"INVALID","stateParam2":"0","gamesWonCurrentMatch":0,"roundsInfo":[]}]
```
  `squad`(0x2cd) and `entranceCriteria`(0x108) are OMITTED and omission is provably
  inert: `FUN_180135ff0` is called from both defaults, so unknown keys are skipped.
  `entranceCriteria`'s shape is now known anyway (an object of three int keys
  COINS / DRAFT_TOKEN / POINTS) but knowing a shape is not a reason to send it.
  `stateParam2` may be a JSON number or a string; the string getter stringifies token
  types 2/3/4, so `"0"` is correct but not mandatory.
  Served today behind `FUT_DRAFT_STATE` (default on) in `utas_server.py`.

---

### 3. FutGetDraftStatsServerResponse — CONFIDENCE: HIGH (all fields scalar; no freeze-risk)
- Struct name `0x180226540`; factory `0x18015076d`; **deserializer `0x1801508c0`** (begin-obj @0x18015093b, dispatch @0x1801509bc).
- Method/Path: **GET** `ut/game/fifa17/draft/mode` (GetDraftStats RPC; historical/aggregate draft stats).
- Keys (all int, except `draftChampion` bool):
  `gamesWon`(0x13a), `gamesLost`(0x138), `scoredGoals`(0x29b), `concededGoals`(0xa1), `bestBuilderScore`(0x52), `draftChampion`(0xe1, bool), `draftsCompleted`(0xe2), `passAccuracyTotal`(0x212), `possessionPercentage`(0x247), `possessionTotal`(0x249).
- MINIMAL known-good:
```json
{"gamesWon":0,"gamesLost":0,"scoredGoals":0,"concededGoals":0,"bestBuilderScore":0,"draftChampion":false,"draftsCompleted":0,"passAccuracyTotal":0,"possessionPercentage":0,"possessionTotal":0}
```

---

### 4. FutGetDraftAwardServerResponse — CONFIDENCE: HIGH (fully reversed)
- Struct name `0x1802266f8`; factory `0x18015106c`; **deserializer `0x1801510c0`** (begin-obj @0x180151129, dispatch @0x1801511ac).
- Method/Path: **GET/POST** `ut/game/fifa17/draft/mode` (GetDraftAward — claim/return the draft prize).
- Keys:
  | key | atom | type | notes |
  |---|---|---|---|
  | `item` | 0x16a | **array** [freeze-risk] | awarded item cards, ITEM deser `0x18013fe00` (loops) |
  | `halId` | 0x150 | int | @ (prize/hal id) |
  | `type` | 0x354 | int | prize type code |
  | `value` | 0x377 | int | prize value (e.g. coins) |
- **ROOT CONTAINER: ARRAY, not object. CORRECTED 2026-08-04.** `0x1801510c0` has the
  same array-root prologue as `FutGetDraftCurrentState`. The object-root body that used
  to be printed here would hang the client identically, and it was sitting in this file
  labelled "known-good".
- MINIMAL (root shape corrected; the key set itself is unchanged and was not re-verified
  this pass, so `TODO/CONFIRM` the members before serving):
```json
[{"type":1,"value":15000,"halId":0,"item":[]}]
```
  With an item prize: `"item":[{ …full card object as in itemData… }]`.
- **Why both of these were wrong at once:** a census claimed only three array-root
  readers existed in the DLL. It missed this one. A second census, run to check the
  first, was wrong in the opposite direction. Roughly 23 of 86 top-level readers remain
  unclassified. **Do not serve any endpoint in this document until its root container
  has been classified by reading the actual prologue, not by regex.**

---

### 5. FutPickDraftChoiceServerResponse — CONFIDENCE: MEDIUM (empty-ack, inferred)
- Struct name `0x180226078`; factory `0x18014fba0`. The factory installs the **generic base ServerResponse vtable `0x18022cb58`** (shared by dozens of structs) — i.e. **no struct-specific deserializer / no parsed fields**. The neighboring `0x18014fcb0` is the *request* serializer, not a response reader.
- Method/Path: **PUT/POST** `ut/game/fifa17/{champId}/draft/choose` and `ut/game/fifa17/draft/choose/difficulty` (fragments @0x2254c0 / 0x2254a0). Commits one pick (or the difficulty choice).
- Body: an empty ack — the client re-reads state via GetDraftCurrentState / GetDraftChoices afterward.
- MINIMAL known-good: `{}`

---

### 6. FutPickDraftAutoChoiceServerResponse — CONFIDENCE: HIGH (delegates to squad deser)
- Struct name `0x180226208`; factory `0x18014fd80`; **deserializer `0x18014fdf0`**. The body is parsed by a single call to the SQUAD deser `0x18013d1f0` (@0x18014fe82) into the struct's squad member.
- Method/Path: **POST** `ut/game/fifa17/{champId}/draft/autocomplete` (fragment @0x225638). Auto-fills the remaining picks and returns the completed squad.
- Body: a **squad object** [freeze-risk] (same schema as `GET /squad/0`).
- MINIMAL known-good:
```json
{"id":0,"personaId":0,"formation":"f442","squadType":"REGULAR_SQUAD","chemistry":100,"starRating":5,"captain":0,"changed":0,"manager":[],"actives":[],"players":[{"index":0,"kitNumber":0}],"kicktakers":[]}
```
  (Emit a fully-populated `players[].itemData` for a real auto-drafted XI.)

---

### 7. FutPurchaseDraftModeServerResponse — CONFIDENCE: MEDIUM-HIGH (field set fully reversed; response-variant ambiguity)
- Struct name `0x180224fb0`; factory `0x18014c0bd`; **deserializer `0x18014c260`** (begin-obj @0x18014c2d5, dispatch @0x18014c3bb).
- Method/Path: **POST** `ut/game/fifa17/purchase/mode/{price}/draft` (fragment @0x2257a8). Buys entry into draft mode; returns the fresh draft session summary.
- Keys (all scalar int):
  `championEventId`(0x7b), `expectedTierLevel`(0x115), `gamesPlayed`(0x139), `gamesRemaining`(0x13c), `rank`(0x26d), `score`(0x29a), `tierLevel`(0x317).
- Note: a **second, larger struct (size 0x38, deser @0x180150379, factory @0x18015028d)** also references this name string. It carries no visible scalar field ladder and is likely an alternate/summary envelope; the `0x18014c260` field-parser above is treated as authoritative. Confirming which body the live client reads is the remaining gap.
- MINIMAL known-good:
```json
{"championEventId":0,"expectedTierLevel":1,"gamesPlayed":0,"gamesRemaining":4,"rank":0,"score":0,"tierLevel":1}
```

---

### Implementation notes for utas_server.py
- Add routes under `G + r"/draft"`: `GET …/draft/state`, `GET …/{id}/draft`, `POST …/{id}/draft/choose`, `POST …/draft/choose/difficulty`, `POST …/{id}/draft/autocomplete`, `POST …/purchase/mode/{n}/draft`, plus GetDraftStats / GetDraftAward on `…/draft/mode`.
- Reuse the existing squad/item JSON builders (`fut_seed.player_item`, `_base_squad`) for `squad`, `itemData`, `item[]` — those objects are already known-good through deser `0x18013d1f0` / `0x18013fe00`.
- Freeze-risk containers to never send as scalars: `choices`, `choices[].itemData`, `squad`, `entranceCriteria`, `roundsInfo`, `item`, and the PickAutoChoice squad body.
## Match / Seasons / Tournaments

Reversed from `cardsdll.dll` (base `0x180000000`) per RECIPE.md. CardsDLL base `0x180000000`.
Parser shared internals: each JSON key → FNV-1a → atom int; deserializer dispatches on atom via
`cmp/sub/dec + je` binary tree. **Unknown atoms are routed to the container-aware SKIP handler
`0x180135ff0` — extra keys are SAFE.** Leaf getters: int `0x1801c79d0`, bool `0x1801c7620`,
str `0x1801c7aa0`. Nested object/array = `call` to a sub-deserializer or array loop. Feeding a
scalar getter an object/array desyncs the SAX reader → infinite spin freeze at `0x1801c7f1a`.

**SAX token-type constants seen in every loop (NOT keys, ignore):** atom 6 (`=` end-object token),
atom 10 (end-array/container token). Atom values > 906 in a raw dump are jump-table offsets, not atoms.

**Key-spelling caveat:** the wire key is FNV-hashed, so JSON keys must match EA's *exact* spelling
incl. typos — notably `boostConis` (atom 96, not "boostCoins") and `matchCoinMultipliers`.

### Two response families found
1. **Bespoke deserializer** (custom atom switch) — most Season/Match/Tournament *load/list* responses.
2. **Ack-only / base-response** — `FutMatchReady`, `FutPlayGame`, `FutUpdateSeason`,
   `FutUpdateFriendlySeason`, `FutGetStoryModeReward`. Their factory allocates a 0x28-byte object and
   calls **only the base ServerResponse constructor `0x18011f850`** (sets vtable `0x18022cb58`,
   timeout `0x7530`, status `-1`). They parse **NO** body fields → the client accepts any body incl.
   `{}`. **The match reward/coins are NOT in FutPlayGame — they are carried by FutDestroyMatch.**

### HTTP method/path note
Endpoint path strings live in an `.rdata` string-pool (pointer table at fileoff `0x21d480`), not
lea-referenced from code, and the request-descriptor table carries the verb in a parallel array that
is not statically recoverable here. **Methods below are inferred from REST/UTAS semantics — confidence
LOW on method, HIGH on struct fields.** Path templates (verified in binary, `%s`="game/fifa17"):
`ut/%s/match`, `ut/%s/season`, `ut/%s/season/user`, `ut/%s/season/%s/user`, `ut/%s/season/%s/reset`,
`ut/%s/season/friendly`, `ut/%s/tournament`, `ut/%s/tournament/user`, `ut/delete/%s/tournament/user`,
`/season/user/history`.

### Baseline (utas_server.py) status
All of these are currently **GAP / stubbed**: `/season` → `(200, {})`, `/match/keepalive` → `(204)`.
No match/season/tournament body is currently reversed in the baseline.

---

## HIGHEST VALUE — MATCH RESULT / REWARD RECORD

### FutDestroyMatchServerResponse — the post-match coin/credit reward record  ★CONFIDENCE: HIGH
- **Deserializer VA:** `0x180121b60` (factory `0x180121700`, name-lea `0x18012170c`)
- **Method+path (inferred):** `DELETE ut/game/fifa17/match/{matchId}` — closes the match and returns
  the credited rewards. This is where a completed match reports its coin/XP award. **Score/win-loss are
  NOT here — the client SENDS the result in the request; the server RESPONDS with the coins.**
- **Ordered reward fields (all int unless noted; offsets in the parsed struct):**

  | atom | key | type | offset | notes |
  |---|---|---|---|---|
  | 20  | `allCoins`               | int  | 0x28 | new total coin balance |
  | 436 | `matchCoins`             | int  | 0x2c | coins awarded for this match |
  | 809 | `tournamentCoins`        | int  | 0x30 | |
  | 776 | `teamOfTournamentWinner` | bool | 0x34 | |
  | 670 | `seasonCoins`            | int  | 0x38 | |
  | 149 | `coins`                  | int  | 0x3c | reward amount (this txn) |
  | 529 | `participationAward`     | int  | 0x44 | |
  | 96  | `boostConis`             | int  | 0x48 | (EA typo — exact key) |
  | 617 | `qualifiedChampionEventId` | int | 0xb0 | |
  | 310 | `gameModeAward`          | nested | — | object, SKIP-safe |
  | 437 | `matchCoinMultipliers`   | nested | — | array/object, SKIP-safe |
  | 619/805/852/887 | `quantity`/`total`/`type`/`value` | nested item | — | reward-item sub-object fields (prize list element; `type` is str) |
  | 877 | `userData`               | nested | via `0x180142470` | user snapshot object (FREEZE-RISK: must be object) |

- **Coin fields cluster tightly at 0x28–0x48**, confirming a coherent credits struct. All top-level
  coin fields are scalar int → safe.
- **MINIMAL known-good JSON** (scalars only, zero freeze risk):
```json
{
  "coins": 400,
  "allCoins": 15400,
  "matchCoins": 400,
  "seasonCoins": 0,
  "tournamentCoins": 0,
  "boostConis": 0,
  "participationAward": 0,
  "qualifiedChampionEventId": 0,
  "teamOfTournamentWinner": false
}
```
- **GAP** (baseline has no match reward body).

---

## SINGLE-PLAYER PLAYABLE LOOP

### FutCreateMatchServerResponse  ★CONFIDENCE: HIGH
- **Deserializer VA:** `0x180120380` (name-lea `0x18011ffbd`)
- **Method+path (inferred):** `POST ut/game/fifa17/match` — creates the match, returns match/squad info.
- **Fields:**
  | atom | key | type | offset | notes |
  |---|---|---|---|---|
  | 740 | `startDateTime`  | int  | 0x28 | epoch |
  | 641 | `reportIdEnabled`| bool | — | |
  | 717 | `squad`          | nested (`0x18011a830`) | — | squad/ITEM array — FREEZE-RISK (must be array) |
- **MINIMAL JSON:** `{"startDateTime": 1580000000, "reportIdEnabled": false}`  (omit `squad` — SKIP-safe)
- **GAP.**

### FutMatchReadyServerResponse  ★CONFIDENCE: HIGH (ack-only)
- **Deserializer:** none — factory `0x180120810` → base ctor `0x18011f850` only (name `0x18021d380`).
- **Method+path (inferred):** `PUT ut/game/fifa17/match/{matchId}` (mark ready).
- Parses **no** fields. **MINIMAL JSON:** `{}`.  **GAP.**

### FutPlayGameServerResponse  ★CONFIDENCE: HIGH (ack-only)
- **Deserializer:** none — factory `0x180162170` → base ctor `0x18011f850` only (name `0x180228050`).
- **Method+path (inferred):** `POST ut/game/fifa17/match/{matchId}` (submit game result; body carries
  score/stats client→server). Response body is ignored by the client. **Rewards arrive via DestroyMatch.**
- **MINIMAL JSON:** `{}`.  **GAP.**

### FutResetMatchServerResponse  ★CONFIDENCE: HIGH
- **Deserializer VA:** `0x18016fd10` (name-lea `0x18016fccd`)
- **Method+path (inferred):** `POST ut/game/fifa17/season/{seasonId}/reset`.
- **Fields:** atom 643 `reset` = **bool** @0x28.
- **MINIMAL JSON:** `{"reset": true}`.  **GAP.**

### FutSeasonListServerResponse  ★CONFIDENCE: HIGH (root shape CORRECTED 2026-08-04)
- **Deserializer VA:** `0x1801683f0`. The previously documented `0x180167740` is the
  per-ELEMENT parser, not the response deserializer.
- **THE ROOT IS AN OBJECT WITH ONE KEY.** `0x1801683f0` runs a key loop and matches
  exactly one atom, `seasons`(0x2ad); the array opens only inside it. This entry
  previously described an array root, because someone read the element parser and
  documented its key set at the document level. A bare array populates nothing, and
  `utas_server.py` served one for months on the strength of this row.
- **Method+path:** `GET ut/game/fifa17/season`.
- **Element ordering matters:** `type` MUST precede `divisionId`, because the
  `divisionId` branch reads the already-parsed type field at `elem+0x1b4`.
- **Element stride is `0x318`.** (`0x1f8`, recorded elsewhere, is the offset of the
  compared short WITHIN an element.) The short the online path matches on is written
  from `divisionId` as `(0xb - divisionId)`, not from `id`.
- `eligibilityKey`/`eligibilitySlot`/`eligibilityValue` below are inner members of
  `elgReq` and are **inert at element level**, so the old minimal body was wrong twice.
- **Element fields:**
  | atom | key | type | notes |
  |---|---|---|---|
  | 348 | `id`               | int | season/division id |
  | 220 | `divisionId`       | int | |
  | 242 | `eligibilityKey`   | int | |
  | 244 | `eligibilitySlot`  | int | |
  | 245 | `eligibilityValue` | int | |
  | 246 | `elgOperation`     | str | |
  | 247 | `elgReq`           | nested | SKIP-safe |
  | 595 | `prizeSet`         | nested | array — FREEZE-RISK |
- **MINIMAL JSON** (object root, single `seasons` key):
```json
{"seasons":[{"type":"OFFLINE","id":1,"divisionId":10}]}
```
  `prizeSet`(0x253), `elgReq`(0xf7) and `matches`(0x1b8) are all `while (tok != 0xd)`
  ARRAY loops: a scalar in any of them is the `0x1801c7f1a` spin. Omit all three.
  Semantic hazard: omitting `untilEndSeconds` makes the season end timestamp equal now.
- **Do not serve this yet.** Across 486 real client requests (User-Agent `ProtoHttp`,
  roughly 30 boots) the game has **never** requested `/season`. Every `/season` line in
  `/tmp/utas_server.log` is our own `curl` or `Python-urllib`. Serving a body here
  changes nothing observable until something upstream makes the client ask.
- **GAP.** (Note: root JSON shape is an array/object wrapper — verify container before shipping.)

### FutSeasonLoadDataServerResponse  ★CONFIDENCE: HIGH (fully traced switch)
- **Deserializer VA:** `0x180131450` (name-lea `0x18013141c`)
- **Method+path (inferred):** `GET ut/game/fifa17/season/user` — load the user's current season state.
- **Fields (verified from switch at `0x18013153c`):**
  | atom | key | type | offset | notes |
  |---|---|---|---|---|
  | 674 | `seasonId`    | int | 0x5c | |
  | 220 | `divisionId`  | int | 0x58 | (as int here, not str) |
  | 656 | `round`       | int | 0x60 | current round |
  | 881 | `userPoints`  | int | 0x64 | points/progress |
  | 202 | `dataVersion` | str | 0x68 (enum byte) | |
  | 201 | `data`        | str | interned via `0x1801c7d40` | large opaque season-state blob string |
  | —   | root/`friendlySeasonHistory` | nested | via `0x180136880` | container — FREEZE-RISK |
- Unknown atoms → SKIP `0x180135ff0` (safe).
- **MINIMAL JSON:**
```json
{"seasonId":1,"divisionId":10,"round":1,"userPoints":0,"dataVersion":"1","data":""}
```
- **GAP.**

### FutUpdateSeasonServerResponse  ★CONFIDENCE: HIGH (ack-only)
- **Deserializer:** none — factory `0x180168b10` → base ctor `0x18011f850` only (name `0x18022a748`).
- **Method+path (inferred):** `PUT/POST ut/game/fifa17/season/user` (submit season match result).
- **MINIMAL JSON:** `{}`.  **GAP.**

### FutSeasonQuitServerResponse  ★CONFIDENCE: MEDIUM
- **Deserializer VA:** `0x180131a30` (name-lea `0x18013197d`)
- **Method+path (inferred):** `DELETE ut/delete/game/fifa17/season/user` (quit current season).
- **Fields:** atom 490 `offlineDivision` = nested (`0x18011a830`) — division-descriptor object.
- **MINIMAL JSON:** `{}` (offlineDivision optional/SKIP-safe; if included must be an object).  **GAP.**

---

## SECONDARY (best-effort catalog)

### FutUpdateFriendlySeasonServerResponse  ★CONFIDENCE: HIGH (ack-only)
- Factory → base ctor `0x18011f850` only (name `0x18022a840`). Method (inferred): `PUT ut/game/fifa17/season/friendly`.
  Parses no fields. **MINIMAL JSON:** `{}`.  **GAP.**

### FutGetFriendlyHistoryDataServerResponse  ★CONFIDENCE: MEDIUM
- **Deserializer VA:** `0x18014d570` (name-lea `0x18014d36d`). Path: `GET ut/game/fifa17/season/friendly` history.
- Fields: atom 675 `seasonGamesDraw` int @0x24; atom 151 `coinsEarned` int; (siblings `seasonGamesWon/Lost`
  in same offset cluster likely present via SKIP). **MINIMAL JSON:** `{"seasonGamesDraw":0,"coinsEarned":0}`.  **GAP.**

### FutTournamentListServerResponse  ★CONFIDENCE: MEDIUM
- **Deserializer VA:** `0x180169ef0` (name-lea `0x180169d7d`). Path (inferred): `GET ut/game/fifa17/tournament`.
- Clean scalar fields: atom 348 `id` int, 212 `difficulty` int, 149 `coins` int, 652 `rewardMultiplier` int,
  19 `aigroup` int, 37 `assetName` str, 243 `eligibilityOperation` str. Many nested (rounds, prizeSet, staff,
  stadiumid, kit atoms 13/16 — treat as FREEZE-RISK objects, SKIP-safe if omitted).
- **MINIMAL JSON (array root):**
```json
[{"id":1,"difficulty":1,"coins":500,"rewardMultiplier":1,"assetName":"","eligibilityOperation":""}]
```
- **GAP.**

### FutTournamentLoadDataServerResponse  ★CONFIDENCE: MEDIUM
- **Deserializer VA:** `0x180147cb0` (name-lea `0x180147c7c`). Path (inferred): `GET ut/game/fifa17/tournament/user`.
- Fields: atom 202 `dataVersion` str, atom 656 `round` int, atom 810 `tournamentData` (str/opaque blob,
  same interned-getter `0x1801c7d40` as season `data`), atom 92 `bonus` nested. Mirrors SeasonLoadData shape.
- **MINIMAL JSON:** `{"round":1,"dataVersion":"1","tournamentData":""}`.  **GAP.**

### FutTournamentQuitServerResponse / FutGetTournamentTeamsServerResponse  ★CONFIDENCE: LOW
- Both resolve to deserializer `0x18016bcf0` (names `0x18022af10` / `0x18022b068`) — near-empty switch;
  only nested/opaque atoms seen (teamId, activeBadge). Likely thin/ack-ish. Paths (inferred):
  Quit = `DELETE ut/delete/game/fifa17/tournament/user`; Teams = `GET ut/game/fifa17/tournament/user`.
  **MINIMAL JSON:** `{}` (or `[]` for Teams if array-rooted).  **GAP.**

### FutUpdateTournamentServerResponse  ★CONFIDENCE: LOW
- **Deserializer VA:** `0x1801758c0` (name-lea `0x18017580d`). Only enum/opaque atoms
  (LOCKED_PERMANENT/LOCKED_RETRY/LOCKED_TROPHIES/SUCCESS) — a status-enum response.
  Path (inferred): `PUT/POST ut/game/fifa17/tournament/user`. **MINIMAL JSON:** `{}`.  **GAP.**

### FutGetActiveTournamentsServerResponse  ★CONFIDENCE: LOW
- **Deserializer VA:** `0x18016b660` (name-lea `0x18016b55d`). Atoms `tournamentId`(811), `awardCount`(64)
  seen but resolve nested/opaque. Path (inferred): `GET ut/game/fifa17/tournament`. Likely array of active
  tournament ids. **MINIMAL JSON:** `[]` or `{}`.  **GAP.**

### FutGetHistoricalServerResponse  ★CONFIDENCE: MEDIUM
- **Deserializer VA:** `0x180172930` (name-lea `0x18017279c`). Path (inferred): `GET ut/game/fifa17/season/user/history`.
- Fields: atom 336 `halId` int @0xc; atom 363 `itemData` = **ITEM** (shared card/item deser `0x18013fe00`)
  → FREEZE-RISK (array of item cards); atom 653 `rewardQuantity` int @0x8; atom 654 `rewardType` int;
  atom 655 `rewardValue` int; atom 656 `round` nested.
- **MINIMAL JSON:** `{"halId":0,"rewardQuantity":0,"rewardType":0,"rewardValue":0}` (omit itemData or send `[]`).  **GAP.**

### FutGetStoryModeRewardServerResponse  ★CONFIDENCE: HIGH (ack-only)
- Factory → base ctor `0x18011f850` only (name `0x18022bc30`). Parses no fields. **MINIMAL JSON:** `{}`.  **GAP.**

---

### Endpoint → response cross-reference (summary)
| Method(inf) | Path | Response struct | Reversal |
|---|---|---|---|
| POST | ut/%s/match | FutCreateMatchServerResponse | FULL |
| PUT | ut/%s/match/{id} | FutMatchReadyServerResponse | FULL (ack) |
| POST | ut/%s/match/{id} | FutPlayGameServerResponse | FULL (ack) |
| DELETE | ut/%s/match/{id} | **FutDestroyMatchServerResponse (rewards)** | FULL |
| POST | ut/%s/season/{id}/reset | FutResetMatchServerResponse | FULL |
| GET | ut/%s/season | FutSeasonListServerResponse | FULL |
| GET | ut/%s/season/user | FutSeasonLoadDataServerResponse | FULL |
| PUT | ut/%s/season/user | FutUpdateSeasonServerResponse | FULL (ack) |
| DELETE | ut/delete/%s/season/user | FutSeasonQuitServerResponse | PARTIAL |
| PUT | ut/%s/season/friendly | FutUpdateFriendlySeasonServerResponse | FULL (ack) |
| GET | ut/%s/season/friendly (hist) | FutGetFriendlyHistoryDataServerResponse | PARTIAL |
| GET | ut/%s/season/user/history | FutGetHistoricalServerResponse | PARTIAL |
| GET | ut/%s/tournament | FutTournamentListServerResponse | PARTIAL |
| GET | ut/%s/tournament/user | FutTournamentLoadDataServerResponse | PARTIAL |
| PUT | ut/%s/tournament/user | FutUpdateTournamentServerResponse | PARTIAL |
| GET | ut/%s/tournament | FutGetActiveTournamentsServerResponse | PARTIAL |
| GET | ut/%s/tournament/user | FutGetTournamentTeamsServerResponse | PARTIAL |
| DELETE | ut/delete/%s/tournament/user | FutTournamentQuitServerResponse | PARTIAL |
| — | (story reward grant) | FutGetStoryModeRewardServerResponse | FULL (ack) |
## Club / Cards / Consumables

Clean-room RE of the FIFA17 CardsDLL (base `0x180000000`). Method: each `FutXServerResponse`
class registers a vtable; **vtable slot `+0x08` is the JSON deserializer**. Located via
struct-name string → `VA = 0x1801e5000 + (fileoff - 0x1e4400)` → factory `lea r8` xref →
vtable install → slot `+0x08`. Every deserializer shares the same skeleton:

```
scratch init (0x1801c63e0/0x180008130/0x1801c8270) → NextToken×2 →
get target model (0x18011a830) → key-loop { KEYRD 0x180141ee0 → atom → dispatch } →
unknown atom → SKIP 0x180135ff0 (container-aware, extra keys are SAFE)
```

**KEYRD `0x180141ee0`** = read next key, FNV-hash (`0x180180d00`) → atom int, advance to value.
Leaf getters: int `0x1801c79d0`, bool `0x1801c7620`, string `0x1801c7aa0`.
Shared **ITEM/card element** deserializer = **`0x18013fe00`** (see `CARD_SYSTEM.md`; produces the
0x100-byte resolved-card record). **TYPE FIDELITY IS MANDATORY**: an array/object key fed to a
scalar getter desyncs the SAX reader → infinite tokenizer spin (freeze at `0x1801c7f1a`). Arrays
are flagged **[FREEZE-RISK]** below — they must be emitted as JSON arrays, never scalars.

### Key structural finding (contradicts the initial brief)
`FutGetClubInfoServerResponse` does **NOT** return `itemData`/card items. Its only recognized
top-level key is **`user`** (atom `0x36c`), an array of **club-user summary records** (element
parser `0x18012c990`, which never calls the card element `0x18013fe00`). The card list that
actually renders in the club UI is **`FutViewCardsServerResponse`** (`itemData`, via
`0x18013fe00`) served on `ut/%s/item`. So "club-wide item search → cards" is ViewCards on `/item`,
while GetClubInfo/GetClubUsers carry club-user stat records. `data/` mental model of GetClubInfo =
{itemData,count,actives} is wrong for this binary. Actives (homekit/awaykit/badge/etc.) appear as
**fields inside each user record**, not as top-level keys.

---

### Per-struct table

Legend: method/path from the `ut/%s/...` templates in the binary (`%s="game/fifa17"`).
GAP = not (correctly) served by `utas_server.py`. All unknown keys are SKIP-safe, so `{}` never
freezes any of these — GAPs are "feature missing", not "crash".

| # | Struct | Deser VA | Method + Path | Top-level keys (atom → type) | Status | Conf |
|---|--------|----------|---------------|------------------------------|--------|------|
| 1 | FutGetClubInfo | `0x18012d280` | GET `ut/%s/club` | `user`(0x36c) → **array[user-record]** [FREEZE-RISK] | GAP (utas serves `itemData`, which is SKIP'd here → empty user list) | deser HIGH / element PARTIAL |
| 2 | FutGetClubUsers | `0x180145c00` | GET `ut/%s/clubUser` | `user`(0x36c) → **array[user-record]** (elem `0x180145480`) [FREEZE-RISK] | GAP (utas `/clubUser` → `{}`) | deser HIGH / element PARTIAL |
| 3 | FutChangeClubName | `0x1801642c0` | PUT `ut/%s/club` (changeClubName) | **none** (deser is immediate `ret`) | ack — any parseable JSON works; `{}` fine | HIGH |
| 4 | FutViewCards | `0x1801293d0` | GET `ut/%s/item` | `itemData`(0x16b) → **array[card-item]** via `0x18013fe00` [FREEZE-RISK] | HANDLED (utas `/item` `defs_route` serves `itemData`) | HIGH |
| 5 | FutActivateCard | `0x1801642c0` | PUT `ut/%s/item` (FUT_CLUB_ACTIVATE_ITEM_DP) | **none** (immediate `ret`) | ack — `{}` fine | HIGH |
| 6 | FutApplyCard | `0x18012a710` | PUT `ut/%s/item` (apply by itemId) | `itemData`(0x16b) → **array[updated card-item]** via `0x18013fe00` [FREEZE-RISK] | GAP | HIGH |
| 7 | FutApplyCardByRes | `0x18012ad10` | PUT `ut/%s/item` (apply by resourceId) | `itemData`(0x16b) → **array[updated card-item]** [FREEZE-RISK] | GAP | HIGH |
| 8 | FutDiscardCard | `0x180127300` | DELETE `ut/delete/%s/item` (CardsDiscardCard) | `items`(0x171) → **array[int ids]** [FREEZE-RISK]; `totalCredits`(0x326) → int; `id`(0x15c) → int | GAP | HIGH |
| 9 | FutDiscardCardByRes | `0x1801279c0` | DELETE `ut/delete/%s/item` (by res) | `totalCredits`(0x326) → int | GAP | HIGH |
| 10 | FutMoveCard | `0x180128600` | PUT `ut/%s/item` (move) | `itemData`(0x16b) → **array** [FREEZE-RISK]; `chemistry`(0x81) → bool | GAP | HIGH |
| 11 | FutMoveCardByRes | `0x180128e30` | PUT `ut/%s/item` (move by res) | `itemData`(0x16b) → **array** [FREEZE-RISK]; `chemistry`(0x81) → bool (+ 2 str/1 int minor) | GAP | HIGH / extra-fields MED |
| 12 | FutConsumablesSearch | `0x180130d10` | GET `ut/%s/item?type=…` (GetFilteredConsumableSearchResults) | `itemData`(0x16b) → **array[consumable-item]** via `0x18013fe00` [FREEZE-RISK]; `displayGroupUseDefaultImage`(0xdb) → int + count scalars | GAP | deser HIGH / scalars MED |
| 13 | FutStaffBonus | `0x18012b730` | GET `ut/%s/…` (CardsGetStaffBonuses) | `bonus`(0x5c) → **nested** (branch sets bool @rbp+0x51) [FREEZE-RISK]; `assetId`(0x23) → int | GAP | MED |
| 14 | FutGetAvailableLoanPlayers | `0x18014e030` → sub `0x18013a1c0` | GET `ut/%s/item` (FUT_AVAILABLE_LOAN_PLAYERS_DP) | `loans`(0x19b) → **array** [FREEZE-RISK]; `itemData`(0x16b) → **array[card-item]** [FREEZE-RISK]; `default`(0xcd) → int | GAP | deser HIGH / fields MED |
| 15 | FutSignLoanPlayer | `0x1801642c0` | PUT `ut/%s/item` (sign loan) | **none** (immediate `ret`) | ack — `{}` fine | HIGH |
| 16 | FutStickerBookSearch | `0x18012eff0` | GET `ut/%s/…` (stickerbook search) | `itemData`(0x16b) → **array[card-item]** via `0x18013fe00` [FREEZE-RISK] | GAP | HIGH |

Notes:
- **`0x1801642c0`** is a shared no-op deserializer (function body = `ret`). Three responses
  (ChangeClubName, ActivateCard, SignLoanPlayer) use it → their HTTP body is fully ignored; only a
  200 + valid JSON (`{}`) is required. This is why "change name / activate / sign loan" succeed
  with a bare stub.
- **user-record** element (`0x18012c990`, GetClubInfo) is a jump-table parser; caught fields
  include `established`(0x110,str), `homekit`(0x159), `awaykit`(0x3f), `cleansheets`(0x84,int),
  `attributeList`(0x31), `passing`(0x214), `awardType`(0x44), `categoryId`(0x73),
  `controls`(0xb9), `changed`(0x7e), `currentChampionEvent`(0xc2). Full field map is PARTIAL
  (jump table at `[0x1412c4]` not fully decoded); all fields optional/SKIP-safe.
- **card-item** element = `0x18013fe00`, already reversed in `CARD_SYSTEM.md`
  (rating/position/nation/teamid/attributeList/name/resourceId…). Not re-derived here.
- Atom→key mapping from `atoms.tsv` (FNV-indexed sequential IDs 0x0–0x38a).

---

### Minimal known-good JSON

```jsonc
// 1 GetClubInfo — GET ut/game/fifa17/club   (club-user summary list)
{ "user": [ { "established": "2026", "cleansheets": 0, "homekit": 0, "awaykit": 0 } ] }
// or safe empty:  { "user": [] }

// 2 GetClubUsers — GET ut/game/fifa17/clubUser
{ "user": [] }

// 3 ChangeClubName — PUT ut/game/fifa17/club
{}

// 4 ViewCards — GET ut/game/fifa17/item   (THE card list; renders real cards)
{ "itemData": [ /* card items, deser 0x18013fe00 shape */ ] }

// 5 ActivateCard — PUT ut/game/fifa17/item
{}

// 6 ApplyCard / 7 ApplyCardByRes — PUT ut/game/fifa17/item
{ "itemData": [ /* the single updated card item */ ] }

// 8 DiscardCard — DELETE ut/delete/game/fifa17/item
{ "items": [ 123456789 ], "totalCredits": 15000, "id": 123456789 }

// 9 DiscardCardByRes — DELETE ut/delete/game/fifa17/item
{ "totalCredits": 15000 }

// 10 MoveCard / 11 MoveCardByRes — PUT ut/game/fifa17/item
{ "itemData": [ /* moved item */ ], "chemistry": true }

// 12 ConsumablesSearch — GET ut/game/fifa17/item?type=<consumable>
{ "itemData": [ /* consumable items */ ], "displayGroupUseDefaultImage": 0 }

// 13 StaffBonus — GET ut/game/fifa17/... (staff bonuses)
{ "bonus": [], "assetId": 0 }

// 14 GetAvailableLoanPlayers — GET ut/game/fifa17/item (loans)
{ "loans": [], "itemData": [ /* loan card items */ ], "default": 0 }

// 15 SignLoanPlayer — PUT ut/game/fifa17/item
{}

// 16 StickerBookSearch — GET ut/game/fifa17/... (sticker book)
{ "itemData": [] }
```

### utas_server.py integration status
- `/item` (`defs_route`) already serves `{"itemData":[…]}` → satisfies **ViewCards, ApplyCard,
  ApplyCardByRes, MoveCard, ConsumablesSearch, StickerBook, loan itemData** shape (though action
  semantics — updated item / chemistry / credits — are not modelled).
- `/club` serves `{"itemData":…}` but GetClubInfo wants **`user`** → the itemData is SKIP'd; club
  card render still works because it goes through `/item` ViewCards, not `/club`. To populate the
  club-user summary, serve `{"user":[…]}` on `/club`.
- `/clubUser` → `{}` (GAP: should be `{"user":[…]}`; `{}` is non-freezing).
- No routes for discard-credits, move-chemistry, staff-bonus, loan `loans[]`, stickerbook → all
  fall to catch-all `{}` (safe, feature-inert).
```
## Store / Packs / Purchases

Reversed from CardsDLL (base `0x180000000`) via the RECIPE deserializer method. All
struct-name VAs computed as `0x1801e5000 + (fileoff - 0x1e4400)`; deserializers located
from the `.text` `lea r8,[name]` xref; atoms translated through `atoms.tsv`.

Parser primitives: INT `0x1801c79d0` · BOOL `0x1801c7620` · STR `0x1801c7aa0` ·
value-SKIP (unknown atom, safe) `0x180135ff0` · next-token `0x1801c7f10` ·
begin-object `0x1801c8270` · key→atom FNV `0x180180d00` · shared ITEM element deser
`0x18013fe00`. **Type fidelity is mandatory** — feeding a scalar getter an object/array
desyncs the SAX reader → tokenizer freeze at `0x1801c7f1a`.

### Endpoint → struct map (paths use %s = "game/fifa17")

| Method | Path | Request→Response struct | Blaze cmd token |
|---|---|---|---|
| GET | `ut/%s/store` (`store/purchasegroup/...`) | FutStoreGetPackTypesServerResponse | STOREPACKTYPES |
| GET | `ut/v2/%s/store` | FutStorePackQuantitiesServerResponse | STOREPACKQUANTITIES / V2STORE |
| POST/PUT | `store/transaction` | FutCreatePackServerResponse (buy=create) / FutPurchaseItemsServerResponse | CREATEPACK / PURCHASEITEMS |
| GET | `ut/%s/purchased` | FutGetPurchasedItemsServerResponse | PURCHASEDITEMS |
| — | credits refresh (embedded) | FutUpdateCreditsServerResponse | UPDATECREDITS |

---

### 1. FutStoreGetPackTypesServerResponse  — confidence: HIGH
- **name VA** `0x18021de20`  · **deserializer** `0x1801234e0`  · **pack element deser** `0x18013af30`
- **Root keys** (deser `0x1801234e0`):
  - `purchase` (atom **0x260**) → **ARRAY** of pack objects (each → `0x18013af30`)  *(freeze-risk: must be array)*
  - `timestamp` (atom **0x31b**) → INT scalar → `[rdi+0x5c]`
- **Pack object fields** (deser `0x18013af30`, all optional; unknown keys skipped):

  | key | atom | type | notes |
  |---|---|---|---|
  | `assetId` | 0x23 | INT | **real pack identity** → `[rbp-0x3c]` |
  | `actionType` | 0x08 | INT | |
  | `bonus` | 0x5c | INT | |
  | `dealType` | 0xcc | STR | |
  | `description` | 0xd1 | STR | display name |
  | `displayGroup` | 0xd9 | **ARRAY** | nested (freeze-risk) |
  | `displayGroupAssetId` | 0xda | INT | `[rbp-0x80]` |
  | `displayGroupUseDefaultImage` | 0xdb | BOOL | |
  | `currencies` | 0xc5 | **ARRAY** | coin price: `[{name,funds,finalFunds}]` (freeze-risk) |
  | `extPrice` | 0x119 | **OBJECT** | → `finalPrice`(0x125,obj `0x180139070`) + `originalPrice`(0x205,obj `0x18013aae0`); inner uses `amount`(0x1b)/`currency`(0xc4) (freeze-risk) |
  | `packContentInfo` | 0x20c | **OBJECT** | → `bronzeQuantity`(0x63), `silverQuantity`(0x2c6), `goldQuantity`(0x149), `rareQuantity`(0x273), `itemQuantity`(0x170), `start`(0x2e3), `unopened`(0x35d,bool) (freeze-risk) |
  | `sortPriority` | 0x2cb | INT | |

- **Status: already handled (renders in-game), but with CORRECTIONS**
  - `store_catalog()` currently emits `id, packType, quantity, purchaseLimit, purchaseCount, isPremium, saleType` — **none of these atoms exist in the pack deser** (`id`=0x15c, `quantity`=0x26b, `saleType`=0x298, `packType`=0x20f, `isPremium`=0x176 are all routed to SKIP `0x180135ff0`). They are harmless no-ops but do nothing.
  - The **real identity field is `assetId` (0x23)**, which the current handler does NOT send. Recommend adding `assetId` per pack (packs currently work off `currencies`+`extPrice`+`packContentInfo` presence + the transaction-body `packId`, but `assetId` is the field the client actually deserializes).
  - `extPrice.finalPrice/originalPrice` inner keys are NOT `mtx` (no such atom) — real inner atoms are `amount`/`currency`; current `{"mtx":N}` is skipped, so extPrice objects are effectively empty-but-present (enough to pass validation; FIFA-Points price shown comes from elsewhere).
- **Minimal known-good** (corrected):
```json
{"purchase":[{"assetId":101,"description":"Gold Pack","sortPriority":1,
  "currencies":[{"name":"coins","funds":5000,"finalFunds":5000}],
  "extPrice":{"finalPrice":{"amount":100,"currency":"fifapoints"},"originalPrice":{"amount":100,"currency":"fifapoints"}},
  "packContentInfo":{"bronzeQuantity":0,"silverQuantity":0,"goldQuantity":7,"rareQuantity":1,"itemQuantity":7}}],
 "timestamp":1596326400}
```

---

### 2. FutStorePackQuantitiesServerResponse  — confidence: HIGH  ⟵ GAP (UNBUILT)
- **name VA** `0x18022d440`  · **deserializer** `0x1801758c0` (token loop `0x180175920`)
- **This is the `ut/v2/%s/store` response.** It is NOT a per-pack quantity list — it is a single
  **eligibility/result gate**. The deser reads exactly ONE key:
  - `result` (atom **0x288**) → STRING; the string VALUE is FNV-hashed (`0x180180d00`) and mapped to an enum stored at `[rdi+0x28]`:

  | string value | atom hit | enum |
  |---|---|---|
  | `SUCCESS` | 0x2fb | 0 |
  | `TOO_MANY_TOURNAMENTS` | 0x324 | 1 |
  | `LOCKED_PERMANENT` | 0x1a1 | 2 |
  | `LOCKED_RETRY` | 0x1a2 | 3 |
  | `LOCKED_TROPHIES` | 0x1a3 | 4 |

  All other keys are skipped. Constructor default of `[rdi+0x28]` is 0 (SUCCESS), so an empty
  `{}` also parses as SUCCESS, but send `result` explicitly.
- **Status: GAP** — no route builds this. Add a handler for `ut/v2/%s/store`.
- **Minimal known-good**: `{"result":"SUCCESS"}`

---

### 3. FutCreatePackServerResponse  — confidence: HIGH
- **name VA** `0x180228318`  · **deserializer** `0x180162880`
- Wrapper key `createPackResponse` (atom **0xbe**) → OBJECT with:

  | key | atom | type | store |
  |---|---|---|---|
  | `itemList` | 0x16e | **ARRAY** of items (element deser `0x18013fe00`) | freeze-risk |
  | `numberItems` | 0x1dd | INT | `[rsi+0x28]` |
  | `purchasedPackId` | 0x264 | INT | `[rsi+0x70]` |
  | `duplicateItemIdList` | 0xec | **ARRAY** (int list) | freeze-risk |

- **Status: already handled — VERIFIED byte-exact** against `store_buy()`.
- **Minimal known-good**:
```json
{"createPackResponse":{"itemList":[<item>],"numberItems":1,"purchasedPackId":101,"duplicateItemIdList":[]}}
```

---

### 4. FutPurchaseItemsServerResponse  — confidence: MEDIUM-HIGH
- **name VA** `0x1802203a0`  · **deserializer** `0x180126a04` (token loop `0x180126a63`)
- Purchase/transaction confirmation. Fields (dispatch is a cumulative sub-ladder off atom in `r8d`):

  | key | atom | type | store |
  |---|---|---|---|
  | `transactionId` | 0x33a | INT | `[rdi+0x28]` |
  | `firstPartyStoreId` | 0x127 | INT | `[rdi+0x9c]` |
  | `packId` | 0x20b | INT | |
  | `purchasePackType` | 0x266 | STR | `[rdi+0x48]` |
  | `state` | 0x2eb | STR | |
  | `useAuth` | 0x367 | INT/BOOL | credit-block fields at `[rdi+0xa0/0xa4/0xa8]` |

- **Status: GAP (optional).** The current buy flow uses CreatePack (`createPackResponse`), not this.
  Only needed if a capture shows FIFA expecting a PURCHASEITEMS response on `store/transaction`.
  Fields above are the confirmed lower branch; a few upper-branch credit fields are approximate.
- **Minimal known-good**: `{"transactionId":1,"packId":101,"purchasePackType":"GOLD","state":"SUCCESS"}`

---

### 5. FutGetPurchasedItemsServerResponse  — confidence: HIGH
- **name VA** `0x18021fca8`  · **deserializer** `0x180124ed0` (body sub-parser `0x18013bd40`)
- Single root key `itemData` (atom **0x16b**) → **ARRAY** of item objects (element deser `0x18013fe00`).
  (also tolerates `duplicateItemIdList` 0xec.) *(freeze-risk: itemData must be array)*
- **Status: already handled — VERIFIED** against `purchased_items()` → `{"itemData":[...]}`.
- **Minimal known-good**: `{"itemData":[]}` (or `[<item>...]`)

---

### 6. FutUpdateCreditsServerResponse  — confidence: MEDIUM
- **name VA** `0x18022cc10`  · **deserializer** `0x1801738b2` (delegates whole body to shared object parser `0x180139610`, `rdx=[rdi+0x10]`)
- Carries the credits/currencies balance object. The verified coin-binding path is the shared
  currencies parser (deser `0x180122c50`, atom `currencies` 0xc5 → `[].funds`), already served by
  `credits_route()` as `{"credits":N,"currencies":[{name,funds,finalFunds}]}`. Exact per-offset
  field map of `0x180139610` not fully traced (it is a large shared parser); no correction needed
  since the coin counter already binds correctly.
- **Status: effectively handled** via `credits_route`. Keep `{"credits":N,"currencies":[...]}`.

---

## Definitive store-availability config flag list (for Blaze client-config)

The store "not available" screen is gated by (a) a **resolution check** — `GetSystemMetrics`
must be > 1024×768 — and (b) the following Blaze **client-config** flags. `FUT_STORE_DISABLED`
is a UI **message** string, not a gate. Confirmed present in `cardsdll.strings`:

**Blaze client-config booleans (must be "1"/true):**
- `IS_STORE_ENABLED`
- `IS_COIN_PURCHASABLE`
- `IS_FIFAPOINT_AVAILABLE`
- `IS_FIFAPOINT_PURCHASABLE`   ← additional (found adjacent)
- `IS_EASTORE_SERVICE_READY`   ← additional (found adjacent)
- `COINS_PURCHASE_ENABLED`
- `POINTS_PURCHASE_ENABLED`
- `MONEY_PURCHASE_ENABLED`     ← additional (found adjacent)

**FUT data/config flags (lowercase, JP-region variants exist):**
- `cardPackStoreEnabled` / `cardPackStoreEnabled_JP`
- `coinEnabled` / `coinEnabled_JP`

Set all Blaze booleans to enabled and ensure the client renders above 1024×768.
# Section: User / Hub / Settings / Objectives / Leaderboards / Champions

Clean-room RE of the FIFA 17 FUT boot/hub API from `cardsdll.dll` disassembly
(base `0x180000000`). Method: RECIPE.md — struct-name string → `lea r8` xref →
deserializer field-loop → FNV atom immediates (cumulative sub/cmp jump-ladders)
→ `atoms.tsv`. `%s = "game/fifa17"`.

**Shared token-type noise** (excluded from field lists below): after every
`NextToken` (`0x1801c7f10`) the parser does `cmp eax,0xa` / `cmp eax,0x6` /
`cmp eax,0xd` — these are JSON token-TYPE checks, NOT field atoms.

**Leaf getters:** int/num `0x1801c79d0`, bool `0x1801c7620`, string `0x1801c7aa0`.
**Value-SKIP** (unknown atom, safe extra keys): `0x180135ff0`. **Card/item**
sub-deser: `0x18013fe00`. Feeding a scalar getter an object/array desyncs the SAX
reader → infinite spin at `0x1801c7f1a` (the hub freeze).

---

## BOOT / HUB path (gates reaching the FUT hub — highest priority)

### FutCreateUserServerResponse — CONFIDENCE: HIGH ✅ HANDLED
- **Deser:** `0x18014cc60` (lea r8 @ `0x18014cc2c`)
- **HTTP:** `POST ut/%s/user`
- **Fields (exact, matches util baseline):**
  - `login` (0x1a5) — bool
  - `userData` (0x36d) — nested → userInfo record (deser `0x18013ec10`)
  - `squad` (0x2cd) — nested object (→ squad deser `0x18013d1f0`)
  - `starterPack` (0x2e5) — nested object
  - `bonusPacks` (0x5d) — array
- **Handled:** `utas_server.USER_POST`. Min JSON:
  ```json
  {"login":true,"userData":{...userInfo...},"squad":{},"starterPack":{},"bonusPacks":[]}
  ```

### FutGetUserInfoServerResponse — CONFIDENCE: HIGH (wrap) / MEDIUM (full typing) ✅ HANDLED
- **Wrapper parser:** `0x180146970` — does `Parse` + TWO `NextToken` before
  deserializing, so the body MUST be wrapped in exactly one member (member NAME
  not compared, nesting level required).
- **userInfo record deser:** `0x18013ec10` (freeze-critical — shared with massinfo).
- **HTTP:** `GET ut/%s/user` (NOT called at boot; only reachable via massinfo).
- **userInfo fields — confirmed atoms (cumulative-ladder):**
  - `personaId` (0x21b) — int
  - `sessionCoinsBankBalance` (0x2bb) — int
  - `trophies` (0x340) — int
  - `won` (0x387) — int
  - `purchased` (0x262) — bool
  - `feature` (0x11c) — **nested object** (e.g. `{trade:bool}`) ⚠ freeze-risk
  - `fifaPointsFromLastYear` (0x121) — int
  - `squadList` (0x2d4) — **array** ⚠ freeze-risk
  - `unopenedPacks` (0x35e) — **nested object** ⚠ freeze-risk; children
    `preOrderPacks`(0x24b), `recoveredPacks`(0x27b), `count`(0xbc)
  - nested notification sub-obj: `notification`(0x1da), `outbid`(0x206), `winning`(0x384)
- **From validated baseline (util) — also parse (unknown→SKIP):** `clubName`,
  `clubAbbr`, `established`, `clubNameChangeAllowed`, `currencies`[] (array of
  `{name,value}` ⚠ array), `won/draw/loss`, `divisionOffline/divisionOnline`,
  `reliability`{reliability,matchUnfinishedTime}, `bidTokens`{count,updateTime},
  `actives`[] (array).
- **Handled:** `utas_server.USER_GET = {"userInfo": user_info()}`.

### FutGetUserMassInfoServerResponse — CONFIDENCE: HIGH (schema) ✅ SERVED POPULATED (live-verify)
- **Deser:** `0x180174630` (full decompile: `/tmp/ghidra_fut/massinfo.txt`).
- **HTTP:** `GET ut/%s/userMassInfo`
- **Shape: FLAT object, NO wrapper.** (Corrected 2026-08-03 — the earlier "wrapper key is
  `user`" note was wrong; `user`(0x36c) occurs only nested inside `clubUser`. The prologue,
  2×NextToken before the key loop, is identical to the proven-flat CreateUser parser
  `0x18014cc60`.)
- **Top-level keys this deser dispatches:**
  - `userInfo` (0x370) → userInfo deser `0x18013ec10`
  - `squad` (0x2cd) → LoadActiveSquad deser `0x18013d1f0` (loads the ACTIVE squad model)
  - `settings` (0x2bf) → settings deser `0x18013c6d0`
  - `userData` (0x36d) → `0x180142470`
  - `clubUser` (0x91), `errors` (0x10c), `loanPlayerClientData` (0x199),
    `loanPlayers` (0x19a), `pileSizeClientData` (0x227)
  - everything else → SKIP (`0x180135ff0`)
- **Freeze history:** the documented "any content desyncs" was recorded before the squad
  schema was reversed; the prime suspect is the malformed `squad` member fed to
  `0x18013d1f0`. Every `user_info()` field type-checks against `0x18013ec10`.
- **Handled:** `utas_server.massinfo()` → `{userInfo, squad, settings, userData}`;
  `FUT_MASSINFO=full|squad|userinfo|settings|empty` bisects it one member per relaunch.

### FutGetSettingsServerResponse — CONFIDENCE: HIGH ✅ HANDLED
- **Deser:** `0x18013c6d0`
- **HTTP:** `GET ut/%s/settings`
- **Fields:** single wrapper key `configs` (0xa2) → array of config entries
  `{ type (0x354), value (0x377) }`.
- **Handled:** `utas_server.SETTINGS = {"configs": []}`. Min JSON: `{"configs":[]}`.

### FutGetHubDataServerResponse — CONFIDENCE: LOW (full schema) / HIGH (served {} works) — GAP
- **Wrapper:** `0x1801736ad` → inner `0x180173a50` / `0x180173b10` / `0x180173c00`.
- **HTTP:** `GET ut/%s/hub`
- **Note:** uses **C++ reflection / vtable dispatch** (`call [rax+0x10]`,
  `call [rdx+0x1f8]`), NOT an inline atom ladder — no static field ladder to
  read. It aggregates sub-objects (userInfo, settings, messages, etc.), each with
  its own deser. Empty `{}` is tolerated (fields default).
- **Handled:** `utas_server` serves `{}` (validated hub-reaching). Deep populate = GAP.

### FutUserDataServerResponse — CONFIDENCE: MEDIUM
- **Deser:** `0x18016dd50` (lea r8 @ `0x18016d98d`)
- **Fields:** `actives` (0xb, array), `key` (0x177), `value` (0x377) — key/value
  user-data entries. Part of create/user path. Served inside `userData`.

---

## THEN — user lifecycle structs

| Struct | Deser VA | HTTP | Fields (atoms) | Status |
|---|---|---|---|---|
| FutGamerSetInfoServerResponse | 0x18016d85d | POST `ut/%s/user` (set gamer info) | none (empty ack) | GAP-trivial |
| FutKeepAliveServerResponse | 0x18016232d | `ut/%s/match/keepalive` | none | ✅ HANDLED (204) |
| FutLogoutServerResponse | 0x18017006d | logout | none (empty ack) | GAP-trivial |
| FutResetUserServerResponse | 0x18017548d | `DELETE ut/delete/%s/user` | none | GAP-trivial |
| FutGetUserActionServerResponse | 0x1801781ad | GET user actions | `actions`(0x7,array), `actionType`(0x8) | partial |
| FutUpdateUserActionServerResponse | 0x18012378d | PUT user action | none (empty ack) | GAP-trivial |
| FutSetFavFeatureServerResponse | 0x18016f2cd | set fav feature | none | GAP-trivial |
| FutLiveMessageUpdateServerResponse | 0x180153a2d | `ut/%s/livemessage` | none (empty ack) | GAP-trivial |
| FutGetTrustedConsoleListServerResponse | 0x18012a01d | `ut/%s/phishing/trusteddevice` | `changed`(0x7e,bool), `exists`(0x117,bool), `locked`(0x19e,bool), `trusted`(0x351,bool) | ✅ HANDLED (`trusted:true` skips security Q) |

Min JSON for the empty-ack structs: `{}` (200) or `204` — all validated safe.
`activeMessage` (`ut/%s/activeMessage`), `clientdata` (`ut/%s/clientdata`),
`livemessage` — no dedicated field-ladder deser; served generically ({} / 200).

---

## OBJECTIVES / CHALLENGES

FUT 17 objectives = **ManagerQuests** (viewmodel `futmanagerquestsviewmodel`;
data providers `FUT_MQ_QUESTS_DATA_DP`, `FUT_SQUAD_QUESTS_DP`,
`FUT_PLAYER_IDENTITY_QUESTS_DP`). Enable flags live in **settings**:
`enableObjectives` (0xfd), `enableObjectivesAsManagerTasks` (0xfe).
Relevant atoms: `objectives`(0x1e2), `objectivesForCurrentUser`(0x1e3),
`allObjectivesForCurrentGameSpaceId`(0x15), `challenges`(0x76),
`challengesCount`(0x78), `challengesCompletedCount`(0x77), `challengeId`(0x74),
`challengeImageId`(0x75), `grantedChallengeAwards`(0x14a), `squadChallenge`(0x2d1).

- **FutManagerQuestGetRewardServerCall** — deser `0x1801516cd` — reward-claim call;
  0 inline atoms (response reuses shared item/award desers). No dedicated
  `...ServerResponse` struct exists — the quests UI is client-driven via the
  viewmodel + generic item/award payloads. **GAP** (no route yet).
- **FutGetTowChallengeServerResponse** — deser `0x18016dbcd` — challenge progress as
  `key`(0x177)/`value`(0x377) pairs, `actives`(0xb). GET. **GAP**. Min: `{}`.
- **FutSetTowChallengeServerResponse** — deser `0x18016ef6d` — none (empty ack). PUT. GAP-trivial.
- **FutLoadSetChallengesResponse** — deser `0x18017b9ed` — SBC set-challenge defs (adjacent):
  `awards`(0x47), `categoryId`(0x73), `elgReq`(0xf7), `endTime`(0x106),
  `formation`(0x12b), `eligibilityKey`(0xf2), `eligibilitySlot`(0xf4). partial/GAP.

---

## LEADERBOARDS

### FutGetLBEntriesServerResponse — CONFIDENCE: MEDIUM — GAP
- **Deser:** `0x180144c8d`
- **HTTP:** `GET ut/%s/leaderboards`
- **Entry fields (array):** `clubName`(0x8e), `badge`(0x49), `est`(0x10f,
  established), `score`(0x29a), `seasonOnlineDraws`(0x2a6), `TalkRating`(0x303),
  `insetUrl`(0x166), `inset`(0x165).
- **Min JSON:** `{"entries":[]}` (empty list safe).

### FutGetLBOptionsServerResponse — CONFIDENCE: MEDIUM — GAP
- **Deser:** `0x18014351c`
- **HTTP:** `GET ut/%s/leaderboards/options`
- **Fields:** `category`(0x70), `id`(0x15c), `period`(0x218), `view`(0x37a), `url`(0x366).

---

## CHAMPIONS

### FutChampionsRegistrationServerResponse — CONFIDENCE: MEDIUM — GAP-trivial
- **Deser:** `0x18014980d` — no inline atoms (empty/status ack). `POST ut/%s/champion`. Min `{}`.

### FutGetChampionsFriendsServerResponse — CONFIDENCE: MEDIUM — GAP
- **Deser:** `0x18014b7ad`. `GET ut/%s/champion` (friends).
- **Fields:** `stats`(0x2ec, nested), `gamesPlayed`(0x139), `persona`(0x21a). Min `{}`.

### FutGetChampionsTopXServerResponse — CONFIDENCE: MEDIUM — GAP
- **Deser:** `0x18014a09d`. `GET ut/%s/champion` (topX).
- **Fields:** `entries`(0x109, array) of `{ clubName(0x8e), badge(0x49), est(0x10f) }`. Min `{"entries":[]}`.

---

## CAPTCHA / PHISHING / TFA

### FutGetCaptchaServerResponse — CONFIDENCE: MEDIUM — GAP
- **Deser:** `0x18014e78d`. `GET ut/%s/captcha`.
- **Fields:** `encodedImg`(0x101, string base64), `sequence`(0x2ba, int),
  `sizeBeforeEncode`(0x2c7, int).

### FutValidateCaptchaServerResponse — `0x18014ed5d` — none (status ack). `POST ut/%s/captcha`. GAP-trivial. Min `{}`.

### FutExchangeCaptchaServerResponse — CONFIDENCE: MEDIUM — GAP
- **Deser:** `0x180177d5d`. Fields: `token`(0x321, string). Min `{"token":"..."}`.

### FutGetPhishingQuestionServerResponse — CONFIDENCE: HIGH ✅ HANDLED
- **Deser:** `0x18012980d`. **HTTP:** `GET ut/%s/phishing/question?deviceId=%s`.
- **Fields:** `question`(0x26c, int id), `answer`, `attempts`(0x28, int),
  `recoverAttempts`(0x27a, int).
- **Handled:** util `{"question":0,"answer":"","attempts":5}`.

### FutSetPhishingAnswerServerResponse — `0x180129b9d` — none. `POST /question?deviceId=%s&question=%d&answer=%s`. ✅ HANDLED.

### FutValidatePhishingAnswerServerResponse — CONFIDENCE: HIGH ✅ HANDLED
- **Deser:** `0x180129ddd`. Returns a trust `token`. **HTTP:** `ut/%s/phishing/validate`.
- **Handled:** util `{"token":"OPENFUT-TRUST-..."}`.

### TFA — `ut/%s/tfa` — endpoint present; no dedicated field-ladder deser (served generically). GAP-untriggered.

---

## Summary of atom / path cross-checks
- Path templates confirmed in binary strings: `ut/%s/{user,user/list,hub,clientdata,
  activeMessage,livemessage,leaderboards,leaderboards/options,champion,phishing,
  captcha,tfa}`, plus `/question?deviceId=%s(&question=%d&answer=%s)`.
- Freeze-risk nested fields (must be object/array, never scalar): userInfo.`feature`,
  userInfo.`unopenedPacks`, userInfo.`squadList`, userInfo.`currencies`,
  userInfo.`actives`; and the whole userMassInfo body (keep `{}`).


---

## Regenerating the analysis inputs

```
cp "/mnt/games/FIFA 17/CardsDLL_Win64_retail.dll" /tmp/fut/cardsdll.dll
objdump -d -M intel /tmp/fut/cardsdll.dll > /tmp/fut/cardsdll.asm
strings -t x /tmp/fut/cardsdll.dll > /tmp/fut/cardsdll.strings
python3 tools/atomdump.py > /tmp/fut/atoms.tsv   # 907 atom->key rows
```
Recipe: docs/OPENCODE_ENDPOINT_PROMPT.md · atom table VA 0x1802d2760 · deser locator in header above.

---

## Implementation status & leads (updated 2026-08-02, autonomous session)

- **Store** — FIXED (v2/store SUCCESS gate + 14 Blaze flags + catalog assetId/extPrice). Live-verified server-side.
- **Transfer market** — IMPLEMENTED read path: `auctionhouse` search serves 18 real-player
  listings (auction record 0x18013e410, itemData via proven 0x18013fe00); tradePile/watchList
  empty; validated freeze-safe offline by tools/test_fut_contract.py (311 checks). Toggle FUT_MARKET=empty.
  NEXT: buy/bid flow (stateful — deduct coins, grant card, echo updated auction) — needs live test.
- **SBC** — LEADS (not yet enabled; need live test): feature gate flag `enableSquadBuildingSetsFeature`;
  `FUT/SBC_USE_STUBS` (BRICK) may enable client-side stub SBCs with NO server content (safest path — try first);
  set-list deser 0x180154990 (vtable 0x180226fc0 slot+0x08) is a string-scanning/callback parser, not a
  clean atom ladder (envelope key not cleanly resolvable statically); requirements are `SBC_ELG_KEY_*`
  eligibility triples. `SBC_TIMER_EXPIRED` present. Recommended: set enableSquadBuildingSetsFeature +
  FUT/SBC_USE_STUBS in Blaze config and observe whether the SBC menu populates from client stubs.
- **Draft** — deferred (stateful pick-progression state machine; broken state soft-locks — needs live test).
- **Match rewards** — reversed (FutDestroyMatch 0x180121b60 coin fields) but stateful — needs live test.
