# FIFA 17 — "account information" retrieval path, end to end

**Date:** 2026-07-30 · **Target:** live `FIFA17.exe` (base `0x140000000`, Wine flat PE map),
plus the saved live-string dump `modstrings.txt` after the process exited mid-analysis.

**Clean-room provenance:** every fact below comes from (a) static/dynamic analysis of binaries we
own (`FIFA17.exe` as mapped in our own process, `stp-origin_emu.dll`), (b) our own client's observed
LSX/Blaze wire traffic (`/tmp/lsx.log`, `/tmp/blaze_responder.log`), and (c) the client's own
runtime reflection/metadata. **No 2021 EA/FIFA leak material was used or consulted.**

Builds on `auth_schema_reflection.md`, `auth_statemachine.md`, `origin_nucleus.md`.

---

## 0. Verdict (short)

**Account information is carried on the Blaze channel, but it is *gated* by the LSX/Origin channel.
The Nucleus/account HTTP channel is not part of the PC login path at all.**

* **LSX/Origin (`127.0.0.1:4216`)** supplies the *identity* and the *credential*:
  `GetProfile` → the OriginSDK's default **user id / persona id**; `QueryEntitlements` →
  `ONLINE_ACCESS`; **`GetAuthCode` → the one-time Origin auth code**.
* **Blaze (Fire2, `:42130`)** supplies the *account record*: `Authentication::login (1/0x0A)` takes
  the auth code in `LoginRequest.AUTH` and returns `LoginResponse.SESS` (session key, blazeUserId,
  userId, email, persona details) — this **is** "account information" for the login flow. The
  explicit account RPCs (`getAccount 1/0x1E → AccountInfo`, `getPersona 1/0x5A`,
  `listPersonas 1/0x64`, `listUserEntitlements2 1/0x1D`) all live *behind* that login.
* **Nucleus/account HTTP (`:42131`)** is unused **by design of this code path**, for two independent
  reasons proven below (§4). It is not a missing piece; it is a dead end for PC/Origin login.

**The exact missing piece:** `LoginRequest.AUTH` (`authCode`, a plain string) can only be filled by
LSX `GetAuthCode`, and the client never issues it. No auth code → no `Authentication::login` → no
`LoginResponse` → nothing that can answer "account information" → popup. Everything downstream
(`getAccount`, `listPersonas`, FUT `user/accountinfo`) is unreachable.

---

## 1. Channel 1 — LSX / Origin on `127.0.0.1:4216`

### 1.1 Complete OriginSDK LSX surface (from the client's own template instantiations)

`Origin::LSXRequest<>` / `LSXEnumeration<>` instantiations present in the image:

| Request | Response | Notes |
|---|---|---|
| `GetConfigT` | `GetConfigResponseT` | |
| `GetProfileT` | `GetProfileResponseT` | **sets the default user/persona — see §1.2** |
| `GetSettingT` | `GetSettingResponseT` | |
| `GetGameInfoT` | `GetGameInfoResponseT` | `UPTODATE` gate |
| `GetInternetConnectedStateT` | `InternetConnectedStateT` | the online gate |
| **`GetAuthCodeT`** | **`AuthCodeT`** | handler `0x1439385b0`, callback `0x143938660` |
| `QueryEntitlementsT` | `QueryEntitlementsResponseT` → `OriginItemT` | enumeration, `0x1439484e0` |
| `GetUserProfileByEmailorEAIDT` | `…ResponseT` → `OriginFriendT` | |
| `IsProgressiveInstallationAvailableT`, `AreChunksInstalledT`, `QueryChunkStatusT`, `SetDownloaderUtilizationT`, `QueryOffersT`, `GetWalletBalanceT`, `CheckoutT`, `ConsumeEntitlementT`, `GetPresenceT`, `SetPresenceT`, `QueryFriendsT`, `GetBlockListT`, `SendInviteT`, `ShowIGOT`, `GrantAchievementT`, `ExtendTrialT`, `SelectStoreT` | | not on the login path |

### 1.2 `GetProfile` is what defines "who is logged in" — **proven**

`OriginGetDefaultUser()` @ `0x1470da6d0` and `OriginGetDefaultPersona()` @ `0x1470da680` are bare
field reads:

```
1470da6d0  call 0x1470e2840        ; SDK-ready predicate
1470da6f4  call 0x1470e3560        ; get impl singleton
1470da6f9  mov  rax,[rax+0x3a0]    ; <<< default USER id
1470da680  ... mov rax,[rax+0x3a8] ; <<< default PERSONA id
```

Both fields are zeroed in the SDK constructor (`0x1470deb2f` / `0x1470deb36`, `rsi = 0`) and are
written in exactly **one** place — `Origin::OriginSDK::Initialize` @ ~`0x1470e5a30`:

```
1470e5ac7  call 0x147118d80        ; sync GetProfile(index=0), 15000 ms timeout (0x3a98)
1470e5acc  test eax,eax
1470e5ace  jne  ...                ; on failure, leave 0
1470e5ad0  mov  rax,[rsp+0x70]
1470e5ad5  mov  [rdi+0x3a0],rax    ; <<< userId   from GetProfileResponse
1470e5adc  mov  rax,[rsp+0x78]
1470e5ae1  mov  [rdi+0x3a8],rax    ; <<< personaId from GetProfileResponse
```

`0x147118d80` builds the request through `0x147117fe0` with `index = 0` and waits via `0x1471186f0`.
The service name used is **`"EbisuSDK"`** (`0x143937d58`), which is why our responder must answer
`GetProfile` with `sender="EbisuSDK"` — it already does.

> **Consequence:** our `GetProfileResponse` with `UserId="33068179" PersonaId="33068179"` *is* the
> mechanism that populates the SDK's identity. That part is working; the client asked `GetProfile`
> three times (ids 3, 10, 17) and we answered correctly each time.

### 1.3 The auth-code request path — fully mapped

`FifaOnline::FirstPartyAuthTokenRetriever::DoTick` @ **`0x146f199c0`**:

```
  rbx = this+8 ; rbp = 2                      ; two request slots: this+0x08, this+0x10
loop:
  rsi = [rbx];  if (!rsi) goto next            ; nothing pending -> nothing happens, silently
  [rsp+0x60] = 0                               ; out: auth-code buffer
  [rsp+0x58] = 0                               ; out: auth-code length
  rax = call 0x1470da6d0                       ; OriginGetDefaultUser()   -> sdk[+0x3a0]
  r9  = &len ; r8 = &buf ; rdx = rsi+0x18      ; rdx = ClientId string from the request object
  rcx = rax                                    ; user handle
  eax = call 0x1470db3c0                       ; Origin::OriginSDK::RequestAuthCodeSync
  if (eax != 0) -> 0x146f19aab                 ; log "[%s] Origin Error(%d)"      (0x1438f5e18)
  if (buf == 0 || len == 0) -> 0x146f19a7e     ; log "[%s] Invalid authcode"      (0x1438f5e00)
  ; success: store the code at rsi+0xd8, set rsi+0xe8 = 1
```

`Origin::OriginSDK::RequestAuthCodeSync` @ **`0x1470db3c0`**:

```
1470db3da  lea rdx,[0x143936158]   ; trace "OriginRequestAuthCodeSync entered"
1470db3f2  call 0x1470dbf30        ; trace
1470db3f7  call 0x1470e2840        ; <<< SDK-ready predicate
1470db3fe  je   0x1470db424        ; NOT ready -> error 0xa0010000, NO LSX TRAFFIC AT ALL
1470db400  call 0x1470e3560        ; impl
1470db41d  call 0x1470e67f0        ; impl->RequestAuthCodeSync(user, clientId, &buf, &len, 0)
```

**Signature (recovered):** `OriginRequestAuthCodeSync(OriginUserT user, const char* clientId,
char** outBuf, size_t* outLen, ...)`. `clientId` comes from the request object at `+0x18` and is
what lands in the LSX `<GetAuthCode ClientId="…"/>` attribute (attribute name `ClientId`
@ `0x14394e098`).

**Note the wire-silent failure path:** if `0x1470e2840` returns false, `RequestAuthCodeSync` logs and
returns `0xa0010000` **without ever touching the socket**. That is a failure mode that looks exactly
like our symptom (nothing in the LSX log). It is, however, unlikely here, because the same predicate
guards `OriginGetDefaultUser`, `OriginCheckOnline`, `OriginGetProfile` etc., all of which demonstrably
worked. Ranked below in §5.

### 1.4 Pushed LSX **Events** — the SDK does support them, including `<Login>`

Our responder is request-driven only and never pushes. The client's OriginSDK *does* register
event handlers. Complete inventory of `Origin::EventHandler<lsx::…T, X>::HandleMessage`:

| Element (name table `0x14393cfd8`–`0x14393d208`) | Payload type |
|---|---|
| **`Login`** @ `0x14393d0ac` | `unsigned int` |
| `OnlineStatusEvent` @ `0x14393d120` | `bool` |
| `ProfileEvent` @ `0x14393d0b8` | `OriginProfileChangeT` |
| `CurrentUserPresenceEvent`, `PresenceVisibilityEvent`, `BroadcastEvent`, `IGOEvent`, `IGOUnavailable`, `MinimizeRequest`, `RestoreRequest`, `MultiplayerInvite`, `MultiplayerInvitePending`, `UserInvitedEvent`, `PurchaseEvent`, `ChatMessageEvent`, `GameMessageEvent`, `CoreContentUpdated`, `BlockListUpdated`, `AchievementSets`, `ChunkStatus`, `GroupEvent`, `GroupEnterEvent`, `GroupLeaveEvent`, `GroupInviteEvent`, `VoipStatusEvent` | various |

`Origin::LSXEvent<>` (the *handshake* event path) is instantiated for `ChallengeT` **only** — that
is the `<Event sender="EALS"><Challenge …/></Event>` we already send. Everything in the table above
goes through the `EventHandler`/`EventEnumerator` dispatch, whose element-name match is at
`0x1471028ee` (it first compares the `sender` attribute — `sender` @ `0x143938028` — then the element
name).

**Relevant attribute names in the pool** (`0x14394de00`+) that belong to this family:
`SessionInformation` `0x14394e0d8`, `IsLoggedIn` `0x14394e0f0`, `Changed` `0x14394e100`,
`userid` `0x14394e108`, `isOnline` `0x14394e180`, `initial` `0x14394e0c8`, `from` `0x14394e0d0`.

So hypothesis (a) from the brief is **structurally possible** — a `<Login>` event exists and the
client can consume it. It is *not* proven to be required (see §5).

---

## 2. Channel 2 — Blaze (Fire2 on `:42130`) — **this is where account info actually lives**

Schemas already reflected out in `auth_schema_reflection.md`; the load-bearing parts:

```
Authentication::LoginRequest   (0x14487ca10, 3)   AUTH authCode:string   EXTB externalBlob:blob
                                                  EXTI externalId:uint64
Authentication::LoginResponse  (0x14487d170, 5)   ANON NTOS SESS SPAM UNDR
  SESS = UserLoginInfo (0x14487cb00, 8)           KEY_ sessionKey  BUID blazeUserId  UID_ userId
                                                  MAIL email  PDTL personaDetails  LLOG FRST 1CON
Authentication::AccountInfo    (0x14487c810, 16)  <- reply of getAccount (1/0x1E), request is EMPTY
  AMU anonymousUser:bool   ASRC authenticationSource:string   CO country:string   DOB dOB:string
  DTCR dateCreated:string  GOPT globalOptin:int8              LATH lastAuth:string
  LN language:string       MAIL email:string      PML parentalEmail:string   RC reasonCode:enum
  STAS status:enum         STAT emailStatus:enum  TPOT thirdPartyOptin:int8
  UDU underageUser:bool    UID userId:int64
Authentication::Entitlements   (0x14487d4e0, 1)   NLST list<Entitlement>   <- listUserEntitlements2 (1/0x1D)
Authentication::Entitlement    (0x14487d490, 16)  TAG entitlementTag  PRID productId  STAT status
                                                  PID personaId  GDAY grantDate  UCNT useCount …
```

Command ids (recovered by calling the client's own `getCommandName`, cross-checked against the
static REST binding at `0x143896a80` → `trustedLogin = 0x0B`):
`login 0x0A · trustedLogin 0x0B · listUserEntitlements2 0x1D · getAccount 0x1E · getAuthToken 0x24 ·
listPersonaEntitlements2 0x30 · expressLogin 0x3C · logout 0x46 · getPersona 0x5A · listPersonas 0x64 ·
getOriginPersona 0x104`.

**Observed wire behaviour (14:46 session):**
`Util::preAuth (9/0x07)` ✓ → `Util::ping` ✓ → 6× `Util::fetchClientConfig` (`OSDK_CORE`,
`OSDK_CLIENT`, `OSDK_NUCLEUS`, `OSDK_WEBOFFER`, `OSDK_ABUSE_REPORTING`, `OSDK_XMS_ABUSE_REPORTING`) ✓
→ **`Authentication::logout (1/0x46)`, empty payload** → disconnect → 3-second transport-ping
reconnect loop forever.

`logout` has no request and no response TDF (confirmed by absence of `LogoutRequest`/`LogoutResponse`
anywhere in the client's type index). It is the OSDK `LoginStateLogout` state — i.e. **the login state
machine aborted between `LoadConfig` and `Login`.**

### 2.1 The OSDK login state machine (recovered state list)

`LoginStateMachineImpl` states, from the client's `GetStateName` thunks at `0x14719b360`+:

```
LoginStateShowMaintenance  LoginStateIsp            LoginStateLoadIspAccountInfo
LoginStateConnect          LoginStateLoadConfig     LoginStateVersionCheck
LoginStateLogin            LoginStatePCLogin        LoginStateVerifyAccount
LoginStateUpgradeAccount   LoginStateLoginComplete  LoginStateUnsuspend
LoginStateCheckUser        LoginStateRecheckUser    LoginStateWebOffer
LoginStateLogout
```

Mapped to observed traffic: `Connect` = `preAuth`; `LoadConfig` = the 6 `fetchClientConfig` calls;
then it should proceed `VersionCheck → PCLogin → Login → VerifyAccount → LoadIspAccountInfo →
LoginComplete`. It went to `Logout` instead.

Associated OSDK events (`0x143984000`+): `EVENT_LOGIN_FETCH_ACCOUNT_INFO_FAILURE` /
`…_SUCCESS`, `EVENT_LOGIN_FAILURE`, `EVENT_LOGIN_ABORTED`, `EVENT_LOGIN_QUEUED`,
`EVENT_LOGIN_TOS_NOT_ACCEPTED`, `EVENT_LOGIN_TOLLBOOTH`, …
Associated operation names (`0x1439623c0`+): **`FetchAccountInfo`**, `UpdateAccountInfo`,
`LookupOriginPersona`, `FetchOriginPersona`, `CreateOriginPersona`.

`LoginStateVersionCheck` reads three keys out of the Blaze client config —
`SV_ENABLE_SERVER_VERSIONING` `0x14395d148`, `SV_CLIENT_CHANGELIST` `0x14395d168`,
`SV_SERVER_VERSION` `0x14395d180` — and on mismatch emits
`"Client/server version mismatch!  Client is at version (%08d).  Server is at version (%08d).%s"`
(`0x14395d1d0`). **We currently return none of these keys.** Absent ⇒ almost certainly treated as
disabled, but see §6 for the cheap belt-and-braces fix.

---

## 3. Channel 3 — the FUT web API (UTAS) — real, but strictly post-login

This is a *fourth* surface the brief did not list, and it is the one that literally serves a thing
called "account info". It is **not** the current blocker (it is unreachable without a Blaze session),
but it *will* be the next wall, so it is documented here.

```
0x1438dbe88  FUT_RS4_BASE_URL          <- config key; NO hardcoded host anywhere in the image
0x1438dbe58  ut/game/%s/               <- path prefix, %s = sku ("fifa17", 0x1438dac20)
0x1438db6c0  user/accountinfo          <- THE FUT account-info endpoint
0x1438db810  users/club?sku=           <- POST body {"idList":[%I64d,…]}
0x1438db8f8  utStats?sku=
0x1438dbe48  FutServerCall / FutServerCall::RequestBuffer
0x1438dc378  FutGetUserAccountInfoServerCallConfig
0x1438dc3a0  FutGetUtStatsServerCallConfig
0x1438dc3c0  FutGetUsersClubInfoServerCallConfig
```

Request headers for `user/accountinfo` (contiguous at `0x1438db6e0`–`0x1438db731`):

```
Accept: application/json
Content-Type: application/json
Accept-Encoding: gzip
Easw-Session-Data-Nucleus-Id: %lld          <- the nucleus/user id, i.e. 33068179
```

Response JSON keys the client parses (contiguous at `0x1438db760`–`0x1438db8b0`):
`userAccountInfo` · `personas` · `userClubList` · `clubName` · `established` · `assetId` ·
`returningUser` · `userPersonaInfos` · `divisionOnline`.

A separate OSDK "SportsWorld"/EASFC HTTP module uses its own header set
(`0x14396f8b0`+): `EASW-Version: 2.0.5.0`, `EASW-Token:`, `EASW-Session:`,
`EASW-Nucleus-Persona:`, `EASW-Userid:`, `EASW-Request-Signature:`, `EASW-Content-Signature:`.

Because `FUT_RS4_BASE_URL` has no hardcoded default, **we can point the entire FUT web API at our own
HTTP server purely by serving that config key** — no DNS/DNAT needed. Worth banking now.

---

## 4. Why the `nucleusConnect` stub on `127.0.0.1:42131` is unused — answered

Two independent reasons, both proven.

### 4.1 `nucleusConnect` / `nucleusConnectTrusted` are **server-supplied config keys**, and we never send them

`0x14389fef8 = "nucleusConnect"` has exactly one reader, `0x147237862`:

```
147237830  sub rsp,0x28
147237834  call 0x1471995b0            ; get service locator
14723783f  call [rdx+0x188]            ; -> Blaze connection/config object
147237845  mov rcx,[rax+0x750]
147237855  test rcx,rcx ; je -> return 0
147237862  lea rdx,[0x14389fef8]       ; "nucleusConnect"
147237869  call [rax+0x48]             ; getConfigString(key, &out)
14723786c  mov rax,[rsp+0x38]          ; return the string
```

`[vtbl+0x48]` is the Blaze config-string getter. The value therefore comes from the **server** —
the `CONF` map in our `PreAuthResponse`, or a `fetchClientConfig` section. Our `PreAuthResponse`
`CONF` map contains only timing values (`pingPeriod` etc.) and our six `fetchClientConfig` replies
are fabricated key sets that contain no `nucleusConnect*`. **The client therefore gets an empty
string and never dials anything.** Zero requests on `:42131` is the expected, correct outcome of
what we are currently serving.

### 4.2 The Nucleus path that *does* exist is S2S/client-cert-only, and belongs to `trustedLogin`

`nucleusConnectTrusted` (`0x14389fdf8`) is read at `0x146e1658b` and immediately feeds a URL builder:

```
146e1658b  lea rdx,[0x14389fdf8]       ; "nucleusConnectTrusted"
146e16595  call [rax+0x48]             ; getConfigString
14e1659d   lea r8,[0x14389fe10]        ; "%s/connect/token"
146e165ae  call 0x146dc0950            ; snprintf(buf, 0x400, "%s/connect/token", url)
146e165db  movups xmm0,[0x14389fe28]   ; "grant_type=client_credentials"
```

Surrounding literals, contiguous, all in the BlazeSDK `LoginStateMachine` block:

```
0x14389fd90  Content-Type: application/x-www-form-urlencoded
0x14389fdc1  enable-client-cert-auth: true
0x14389fde0  LoginStateMachine
0x14389fdf8  nucleusConnectTrusted
0x14389fe10  %s/connect/token
0x14389fe28  grant_type=client_credentials
0x14389fe50  NEXUS_S2S 
0x14389fe60  "access_token" : "
0x14389fef8  nucleusConnect
```

So the shape, if it were ever used, is:

```
POST {nucleusConnectTrusted}/connect/token
Content-Type: application/x-www-form-urlencoded
enable-client-cert-auth: true
Authorization: NEXUS_S2S <credential>

grant_type=client_credentials
→ 200 {"access_token" : "…"}
```

…and the token then goes into `TrustedLoginRequest {ID_ id, ITYP idType, TOKN accessToken}` =
**`Authentication::trustedLogin (1/0x0B)`**, whose REST binding at `0x143896a80` confirms the shape
(`GET`, headers `Authorization: accessToken`, `X-Forwarded-UserType: idType`,
`X-Forwarded-UserId: id`).

**This is the console/dedicated-server trusted path. It requires a client certificate
(`enable-client-cert-auth: true`) and is not what the PC/Origin build uses.** The PC build uses
`login (1/0x0A)` with `AUTH = <Origin auth code>`.

### 4.3 The hardcoded EA account hosts are for the web UI, not for login

```
0x143b8b528  https://accounts.int.ea.com/     0x143b8b588  https://accounts.ea.com/
0x143b8b548  https://gateway.int.ea.com/      0x143b8b5a8  https://gateway.ea.com/
0x143b8b568  https://signin.int.ea.com/       0x143b8b5c0  https://signin.ea.com/
0x143b8b700  Nucleus::gNucleusLocale          0x143b8b718  Nucleus::gNucleusBaseUrl
0x143b8b738  Nucleus::gNucleusBaseProxyUrl    0x143b8b758  Nucleus::gNucleusBasePortalUrl
0x143b8b778  Nucleus::gNucleusClientSideRedirectUri
```

These back the EAWebKit account-management pages, reached through the OSDK WebOffer config keys
`NUCLEUS_CREATE_URL` / `NUCLEUS_ADDED_URL` / `NUCLEUS_INCOMPLETE_URL` / `NUCLEUS_CREATE_INFO_URL` /
`NUCLEUS_DUPACCT_INFO_URL` / `NUCLEUS_DEACTIVATED_INFO_URL` (`0x14395eb60`–`0x14395ec18`, sitting in
the middle of the `WEB_OFFER_URL` / `NEWS_URL` / `FAQ_URL` / `TOSA_URL` block) — i.e. the
`OSDK_NUCLEUS` `fetchClientConfig` section is **a set of account-web-page URLs**, not login plumbing.
That the game does not dial `accounts.ea.com` is therefore *correct behaviour*, not a symptom.

> **Bottom line on channel 3:** do not build a Nucleus HTTP server. If you want the `:42131` stub to
> ever receive traffic you would have to (a) advertise `nucleusConnectTrusted` in the Blaze config
> and (b) satisfy `enable-client-cert-auth` over TLS — and that would put you on the `trustedLogin`
> path, which is *not* the path this build's login state machine takes. Serve the auth code instead.

---

## 5. The dependency chain, and where it actually breaks

```
 LSX GetInternetConnectedState connected="1"        ✅ done
   -> g_originOnline @0x1443337f8 (writer 0x146f1e6b0) + FE::FIFA::OriginOnlineEvent
   -> origin.nav emits OriginIsOnlineTrue -> startFutBlazeLogin
 LSX GetGameInfo UPTODATE="true"                    ✅ done
 LSX GetConfig / GetProfile                         ✅ done -> sdk[+0x3a0]=UserId, [+0x3a8]=PersonaId
 --------------------------------------------------------------------------------
 OSDK LoginStateConnect   -> Blaze Util::preAuth                       ✅ answered
 OSDK LoginStateLoadConfig-> Blaze Util::fetchClientConfig × 6         ✅ answered (fabricated data)
 OSDK LoginStateVersionCheck -> SV_* keys from config                  ⚠️ keys absent
 OSDK LoginStatePCLogin   -> FirstPartyAuthTokenRetriever::DoTick
                             -> OriginGetDefaultUser()
                             -> LSX GetAuthCode(ClientId)              ❌ NEVER SENT   <<< BREAK
                          -> Blaze Authentication::login AUTH=<code>   ❌ never sent
 OSDK LoginStateVerifyAccount / LoadIspAccountInfo
                          -> Blaze getAccount 1/0x1E -> AccountInfo    ❌ unreachable
                          -> QueryEntitlements / listUserEntitlements2 ❌ unreachable
 OSDK LoginStateLoginComplete                                          ❌ unreachable
 --------------------------------------------------------------------------------
 FUT: CheckFUTRosters -> {FUT_RS4_BASE_URL}ut/game/fifa17/user/accountinfo   ❌ unreachable
```

Observed instead: `LoadConfig` → **`LoginStateLogout`** → `Authentication::logout (1/0x46)` →
disconnect → reconnect ping loop. In the most recent run the client did not even re-reach `preAuth`;
it sat in a bare PING/close loop every 3 s, i.e. the connection manager gave up.

### Ranked hypotheses for "no `GetAuthCode`", with the discriminating test for each

| # | Hypothesis | Evidence for | Evidence against | Discriminating probe |
|---|---|---|---|---|
| **H1** | The login state machine aborts *before* `LoginStatePCLogin` — i.e. nothing is ever enqueued into `DoTick`'s two slots, so `RequestAuthCodeSync` is never called. | `logout` arrives immediately after the last `fetchClientConfig`, with **no** intervening RPC and no LSX traffic. `QueryEntitlements` is *also* never issued — consistent with "the whole PC-login step never ran", not with "the auth-code call failed". | — | Breakpoint `0x146f199c0` (`DoTick`) and `0x1470db3c0`. If `DoTick` runs but both slots are null → H1 confirmed. **Do this first.** |
| **H2** | Our fabricated `fetchClientConfig` payloads are wrong/insufficient, so `LoadConfig` "succeeds" but a required key is missing and the next state fails. | All six replies contain keys we invented. `SV_ENABLE_SERVER_VERSIONING` / `SV_CLIENT_CHANGELIST` / `SV_SERVER_VERSION` are read by `LoginStateVersionCheck` and we send none. `OSDK_NUCLEUS` should be six `NUCLEUS_*_URL` keys; we send four invented `OSDK_NUCLEUS_*` keys. | Missing keys usually degrade to `""`/0 in OSDK. | Serve the corrected config (§6.1) and re-run; watch whether the client advances past `LoadConfig`. Cheap, no RE needed. |
| **H3** | The OriginSDK needs a *pushed* `<Login>` / `<OnlineStatusEvent>` event before it considers a user authenticated. | `Origin::EventHandler<lsx::LoginT, unsigned int>` exists (`0x14393f900`), element `Login` @ `0x14393d0ac`; attributes `IsLoggedIn` / `SessionInformation` / `userid` exist in the pool. Our responder never pushes anything. | The Steampunks emu never pushed events either, yet the game still got as far as `preAuth`. `OriginGetDefaultUser` is fed by `GetProfile`, *not* by the Login event (§1.2) — so the SDK's notion of "who" does not depend on it. | Push `<LSX><Event sender="EbisuSDK"><Login userid="33068179" IsLoggedIn="1"/></Event></LSX>` (encrypted, right after `ChallengeAccepted`) and see whether behaviour changes. Cheap; try alongside H2. |
| **H4** | `RequestAuthCodeSync` early-outs at the SDK-ready predicate `0x1470e2840` and returns `0xa0010000` without wire traffic. | It is the only *silent* failure path inside the auth-code call. | The same predicate guards `OriginGetDefaultUser` / `OriginGetProfile` / `OriginCheckOnline`, all of which demonstrably worked in this session. | Breakpoint `0x1470db3f7`; check `al` after `0x1470e2840`. Only worth doing if H1's `DoTick` probe shows the call *is* being made. |

H1 is by far the most likely. H2 is the cheapest thing that could plausibly cause H1.

---

## 6. What we must serve

### 6.1 Blaze `Util::fetchClientConfig` — replace the invented keys

Serve authentic key *names* (values can be conservative). Recovered key names, by section:

* **`OSDK_CORE`** — include the version-check trio explicitly so nothing is ambiguous:
  `SV_ENABLE_SERVER_VERSIONING = 0`, `SV_CLIENT_CHANGELIST = 0`, `SV_SERVER_VERSION = 0`.
  Keep `OSDK_PRESENCE_DELAY = 5`, `OSDK_PRESENCE_POLL = 60` (real key names, `0x143962848`/`0x143962860`).
* **`OSDK_NUCLEUS`** — the *real* keys are web-page URLs, not the `OSDK_NUCLEUS_*` we invented:
  `NUCLEUS_CREATE_URL`, `NUCLEUS_ADDED_URL`, `NUCLEUS_INCOMPLETE_URL`, `NUCLEUS_CREATE_INFO_URL`,
  `NUCLEUS_DUPACCT_INFO_URL`, `NUCLEUS_DEACTIVATED_INFO_URL` (`0x14395eb60`–`0x14395ec18`).
  Empty strings are fine and honest; invented `OSDK_NUCLEUS_ENABLED=1` is not.
* **`OSDK_WEBOFFER`** — real keys: `WEB_OFFER_URL`, `NEWS_URL`, `NEWS_TIME_STAMP_URL`, `FAQ_URL`,
  `TOSA_URL`, `TOSAC_URL`, `MENU_ESPN_URL`, `MENU_WEBGM0_URL`…`MENU_WEBGM2_URL`.
* **`OSDK_ABUSE_REPORTING` / `OSDK_XMS_ABUSE_REPORTING`** — `OSDK_ABUSE_REPORTING_ENABLED = 0`,
  `OSDK_ABUSE_NUM_TYPES = 0` (already correct).
* **`OSDK_CLIENT`** — the `OSDK_CLUBS_*` limits we already send are real key names.
* Consider adding **`FUT_RS4_BASE_URL = http://127.0.0.1:<port>/`** — see §6.4.

### 6.2 LSX `GetAuthCode` — be ready the instant it is asked

```xml
<LSX><Response id="N" sender="EbisuSDK">
  <AuthCode Code="QUXbLm3…opaque…" Return="QUXbLm3…opaque…"/>
</Response></LSX>
```

Emit **both** `Code` and `Return` until the log shows which one the client consumes (the attribute
pool position between `ClientId` and `connected` is suffix-shared, so `Code` is the strong
candidate). The code is opaque — we author both ends — but it **must be echoed verbatim into
`LoginRequest.AUTH`** and accepted there. Log the incoming `ClientId` value; that tells us which EA
client id FIFA 17 presents and is worth recording.

### 6.3 LSX `QueryEntitlements` — pre-stage the answer

```xml
<LSX><Response id="N" sender="EbisuSDK"><QueryEntitlementsResponse>
  <OriginItem ItemId="ONLINE_ACCESS" EntitlementId="1" ResourceId="1027460" OfferId="1027460"
              GrantDate="2016-09-01T00:00:00Z" bIsOwned="true" Uses="0"/>
</QueryEntitlementsResponse></Response></LSX>
```

### 6.4 Blaze `Authentication` — the account-info replies to have ready

| Cmd | Reply | Must contain |
|---|---|---|
| `login 0x0A` | `LoginResponse` | `ANON=0 NTOS=0 UNDR=0 SPAM=1`; `SESS.KEY_` non-empty; `SESS.BUID = SESS.UID_ = 33068179`; `SESS.PDTL.PID_ = 33068179`; `SESS.PDTL.DSNM = "CAGE"`; `SESS.PDTL.STAS = 0`; `SESS.PDTL.PLAT` = the `PLAT` from `PreAuthResponse` |
| `getAccount 0x1E` | `AccountInfo` (empty request) | `UID = 33068179`, `CO = "US"`, `LN = "en_US"`, `MAIL` = any well-formed address, `STAS` = active, `STAT` = verified, `UDU = false`, `AMU = false`, `ASRC = "cem_ea_id"` (matches the `NASP` we already return in `PreAuthResponse`), `DTCR` / `LATH` ISO-8601 |
| `listPersonas 0x64` | `ListPersonasResponse` | one persona: id `33068179`, name `CAGE`, status active |
| `getPersona 0x5A` | `GetPersonaResponse` | same single persona |
| `listUserEntitlements2 0x1D` | `Entitlements` | one `Entitlement`: `TAG = "ONLINE_ACCESS"`, `PRID` tied to offer `1027460`, `PID = 33068179`, `STAT` active |
| `logout 0x46` | empty REPLY | already correct — but treat its arrival as the failure signal it is |

Also serve `UserSessions` notification `UserAuthenticated (0x08)` after login (notification ids
already decoded: `0x141b03f70`).

### 6.5 FUT web (`{FUT_RS4_BASE_URL}ut/game/fifa17/user/accountinfo`) — the next wall, pre-built

```
GET  {FUT_RS4_BASE_URL}ut/game/fifa17/user/accountinfo
     Accept: application/json
     Content-Type: application/json
     Accept-Encoding: gzip
     Easw-Session-Data-Nucleus-Id: 33068179

200 {"userAccountInfo": {
       "personas": [{
         "personaId": 33068179, "personaName": "CAGE",
         "returningUser": 0,
         "userClubList": [{"clubName":"…","established":<epoch>,"assetId":<id>}],
         "userPersonaInfos": [], "divisionOnline": 0
       }]
    }}
```

Because `FUT_RS4_BASE_URL` has no hardcoded default, serving it as a config key is enough to point
the whole FUT web API at us — plain HTTP on `127.0.0.1` is fine, no TLS or DNAT needed.

---

## 7. Immediate next actions (ordered)

1. **Probe H1.** Relaunch, breakpoint `0x146f199c0` (`FirstPartyAuthTokenRetriever::DoTick`) and
   `0x1470db3c0` (`RequestAuthCodeSync`). Whether `DoTick` runs at all, and whether its two slots are
   null, decides H1 vs H4 in one shot.
2. **Ship the corrected `fetchClientConfig` (§6.1)** — real key names, explicit `SV_*` trio. Cheapest
   possible fix for H2 and it removes a class of ambiguity permanently.
3. **Add `GetAuthCode` + `QueryEntitlements` responses (§6.2/6.3)** to `lsx_responder.py` so the
   moment the client asks, it is answered — and log the `ClientId` it presents.
4. **Try the pushed `<Login>` event (§H3)** — one extra encrypted frame after `ChallengeAccepted`.
   Low cost, and it is the only untested structural difference between us and a real Origin client.
5. Keep the Nucleus HTTP stub on `:42131` parked. It is on the `trustedLogin`/client-cert path and is
   not what this build uses.

---

## Appendix — addresses

| What | VA |
|---|---|
| `FifaOnline::FirstPartyAuthTokenRetriever::DoTick` | `0x146f199c0` |
| `Origin::OriginSDK::RequestAuthCodeSync` | `0x1470db3c0` |
| … its SDK-ready predicate (wire-silent bail) | `call 0x1470e2840` @ `0x1470db3f7` |
| … its impl call | `call 0x1470e67f0` @ `0x1470db41d` |
| `OriginGetDefaultUser()` → `sdk[+0x3a0]` | `0x1470da6d0` |
| `OriginGetDefaultPersona()` → `sdk[+0x3a8]` | `0x1470da680` |
| `Origin::OriginSDK::Initialize` — writes `+0x3a0`/`+0x3a8` from `GetProfile` | `0x1470e5ad5` / `0x1470e5ae1` |
| sync `GetProfile(index=0)` helper (timeout `0x3a98` = 15 s) | `0x147118d80` |
| OriginSDK ctor zeroing `+0x3a0`/`+0x3a8` | `0x1470deb2f` / `0x1470deb36` |
| `"[%s] Invalid authcode"` / `"[%s] Origin Error(%d)"` | `0x1438f5e00` / `0x1438f5e18` |
| `"OriginRequestAuthCodeSync entered"` | `0x143936158` |
| `nucleusConnect` (config key) / its only reader | `0x14389fef8` / `0x147237862` |
| `nucleusConnectTrusted` / its reader | `0x14389fdf8` / `0x146e1658b` |
| `%s/connect/token` · `grant_type=client_credentials` · `enable-client-cert-auth: true` · `NEXUS_S2S ` | `0x14389fe10` · `0x14389fe28` · `0x14389fdc1` · `0x14389fe50` |
| `trustedLogin` REST binding (`GET`, `Authorization`/`X-Forwarded-*`) | `0x143896a80` |
| LSX **event** element-name table (incl. `Login` @ `0x14393d0ac`) | `0x14393cfd8`–`0x14393d208` |
| `Origin::EventHandler<lsx::LoginT,unsigned int>::HandleMessage` | `0x14393f900` |
| `Origin::EventHandler<lsx::OnlineStatusEventT,bool>::HandleMessage` | `0x143940050` |
| LSX event dispatch / element-name compare | `0x1471028ee` |
| LSX request element table / attribute pool | `0x14394dc00` / `0x14394de00` |
| `"EbisuSDK"` / `"EALS"` service names | `0x143937d58` / `0x143937c60` |
| OSDK login-state name thunks | `0x14719b360`+ |
| `SV_ENABLE_SERVER_VERSIONING` / `SV_CLIENT_CHANGELIST` / `SV_SERVER_VERSION` | `0x14395d148` / `0x14395d168` / `0x14395d180` |
| version-mismatch message | `0x14395d1d0` |
| OSDK config section names (`OSDK_CORE`…`OSDK_TICKER`) | `0x143962be8`–`0x143962c40` |
| OSDK operations `FetchAccountInfo` / `UpdateAccountInfo` | `0x1439623c0` / `0x1439623d8` |
| `EVENT_LOGIN_FETCH_ACCOUNT_INFO_FAILURE` / `_SUCCESS` | `0x1439840b8` / `0x1439840e0` |
| `NUCLEUS_*_URL` config keys (OSDK_NUCLEUS section) | `0x14395eb60`–`0x14395ec18` |
| FUT `FUT_RS4_BASE_URL` / `ut/game/%s/` / `user/accountinfo` | `0x1438dbe88` / `0x1438dbe58` / `0x1438db6c0` |
| FUT `Easw-Session-Data-Nucleus-Id: %lld` | `0x1438db731` |
| FUT response JSON keys (`userAccountInfo`…`divisionOnline`) | `0x1438db760`–`0x1438db8b0` |
| EASW/SportsWorld header block | `0x14396f8b0`–`0x14396fa50` |
| hardcoded `accounts/gateway/signin.ea.com` (web UI only) | `0x143b8b528`–`0x143b8b5c0` |
| `Nucleus::gNucleusBaseUrl` & friends | `0x143b8b700`–`0x143b8b778` |
| Blaze `AccountInfo` / `LoginRequest` / `LoginResponse` descriptors | `0x14487c810` / `0x14487ca10` / `0x14487d170` |

Tools written this pass (all in `…/scratchpad/`): `acct_probe1.py` (CommandInfo neighbourhood dump),
`strdump.py` (string run at a VA), `wsearch.py` (ASCII+UTF-16 phrase search), `navdump.py`
(nav-flow JSON extractor). Reused: `memtool.py`, `strsearch.py`, `xref.py`, `dis.sh`, `cmdinfo.py`.
