# Mock Server

> Talk to the DMK device mock server (apps/device-mock-server, default http://127.0.0.1:9752) with curl. Make fake devices, connect them, add APDU mocks, send APDUs, break commands on purpose. ALWAYS read this skill when the user says "mock server", makes/changes a mock device, adds mocks, forces an APDU error, or pokes a device Ledger Live/DMK is on.

- Skill: `ledgerhq/mock-server` (Agent Skill)
- Install (CLI): `npx skillmds@latest add ledgerhq/mock-server`
- Raw SKILL.md: https://api.skillmd.com/api/skills/ledgerhq/mock-server/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: ledgerhq (https://skillmd.com/u/ledgerhq)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/ledgerhq/mock-server

---


# Mock server

Fake Ledger devices over REST. You do curl. Server hands back JSON.

Big book of every route: `apps/device-mock-server/openapi.yaml`. Read it when
this file is not enough.

## Rules to remember

- URL is `http://127.0.0.1:9752`.
- Every call needs `Authorization: Bearer <token>`. Only `POST /auth` and
  `GET /health` go free.
- Get token: `POST /auth`. Or steal the one Ledger Live/DMK uses.
- Curl to `127.0.0.1` needs `required_permissions: ["all"]`.
- Server forgets everything on restart. No disk.

Set once per shell:

```bash
BASE=http://127.0.0.1:9752
TOKEN=<bearer-token>
AUTH="Authorization: Bearer $TOKEN"
JSON="Content-Type: application/json"
```

## Client device (Ledger Live / DMK)

Client makes its own device when it connects. To touch it:

- Ask user for the FULL token. Logs show only first 8 letters.
- Find device with `GET /devices`. Change it with `PATCH`.
- NEVER delete then remake it. New id = client lost forever.

## The curl commands

```bash
# Token + health
curl -s -X POST $BASE/auth                                   # -> { token, expires_at }
curl -s $BASE/health

# Devices
curl -s $BASE/devices -H "$AUTH"
curl -s $BASE/devices/<id> -H "$AUTH"
curl -s -X POST $BASE/devices -H "$AUTH" -H "$JSON" \
  -d '{"name":"Ledger Flex","device_type":"flex","firmware_version":"1.9.1"}'
curl -s -X PATCH $BASE/devices/<id> -H "$AUTH" -H "$JSON" \
  -d '{"firmware_version":"1.10.0"}'
curl -s -X DELETE $BASE/devices/<id> -H "$AUTH"

# Plug / unplug
curl -s -X POST $BASE/devices/<id>/connect -H "$AUTH"        # or /disconnect

# Send APDU
curl -s -X POST $BASE/devices/<id>/apdu -H "$AUTH" -H "$JSON" -d '{"apdu":"e001000000"}'

# Mocks
curl -s $BASE/devices/<id>/mocks -H "$AUTH"
curl -s -X POST $BASE/devices/<id>/mocks -H "$AUTH" -H "$JSON" -d '{"prefix":"b001","response":"5515"}'
curl -s -X PATCH $BASE/devices/<id>/mocks/<mockId> -H "$AUTH" -H "$JSON" -d '{"prefix":"b001","response":"9000"}'
curl -s -X DELETE $BASE/devices/<id>/mocks/<mockId> -H "$AUTH"
curl -s -X DELETE $BASE/devices/<id>/mocks -H "$AUTH"        # kill all

# Save / load session
curl -s $BASE/export -H "$AUTH"
curl -s -X POST $BASE/import -H "$AUTH" -H "$JSON" -d '<SessionExport json>'

# Speculos seed (⚠️ plaintext — test mnemonics only, never real keys)
curl -s -X PUT $BASE/sessions/current/seed -H "$AUTH" -H "$JSON" \
  -d '{"seed":"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"}'
```

`device_type`: `nanoS`, `nanoSP`, `nanoX`, `stax`, `flex`, `apexp`. `PATCH` keeps
the id and only changes fields you send.

## Open App / Close App

Format: `e0 d8 00 00 <len> <app-name-ascii-hex>` — Close: `b0 a7 00 00 00`.

```bash
# Open Bitcoin (7 chars → 07, "Bitcoin" = 426974636f696e)
curl -s -X POST $BASE/devices/<id>/apdu -H "$AUTH" -H "$JSON" \
  -d '{"apdu":"e0d8000007426974636f696e"}'

# Open Ethereum (8 chars → 08, "Ethereum" = 457468657265756d)
curl -s -X POST $BASE/devices/<id>/apdu -H "$AUTH" -H "$JSON" \
  -d '{"apdu":"e0d8000008457468657265756d"}'

# Close App (any app)
curl -s -X POST $BASE/devices/<id>/apdu -H "$AUTH" -H "$JSON" \
  -d '{"apdu":"b0a7000000"}'
```

All three return `{"response":"9000"}` on success. Open App returns `6807` if
the app is not in the device's `apps` list (add it via `POST /devices` or
`PATCH /devices/<id>`).

Build the hex for any app on the fly:

```bash
app="Solana"
hex=$(printf '%s' "$app" | xxd -p | tr -d '\n')
len=$(printf '%02x' ${#app})
curl -s -X POST $BASE/devices/<id>/apdu -H "$AUTH" -H "$JSON" \
  -d "{\"apdu\":\"e0d800${len}${hex}\"}"
```

## Mocks: how they work

- Mock catches any APDU that STARTS WITH `prefix`.
- One `response`, or a list `responses` (used one by one, then loops).
- Longest prefix wins. Newest wins ties. Mock beats the server's own answer.
- Answer = `<data><status>`. `9000` = ok. `6d00` = no/unknown. `5515` = locked.

```bash
# Say device is locked:
curl -s -X POST $BASE/devices/<id>/mocks -H "$AUTH" -H "$JSON" -d '{"prefix":"b001","response":"5515"}'

# Break a command on purpose:
curl -s -X POST $BASE/devices/<id>/mocks -H "$AUTH" -H "$JSON" -d '{"prefix":"e00300","response":"6d00"}'

# Ok, ok, then fail, on repeat:
curl -s -X POST $BASE/devices/<id>/mocks -H "$AUTH" -H "$JSON" \
  -d '{"prefix":"e0aa0000","responses":["cafe9000","cafe9000","6d00"]}'
```

Add mock = new behavior. Delete mock = old behavior back.

## Speculos seed

Each session starts with the default test mnemonic. Override it per-session
before opening an app (the seed is forwarded to Speculinho on every `/acquire`):

```bash
curl -s -X PUT $BASE/sessions/current/seed -H "$AUTH" -H "$JSON" \
  -d '{"seed":"glory promote mansion idle axis finger extend february uncover one trip resolve toe"}'
```

⚠️ **Not secure.** Stored in plaintext in server memory and sent over plain
HTTP. Use only test/dummy mnemonics — never a real production key.

## Onboarding

Make a fresh, not-onboarded device and walk it through Ledger Live's
SyncOnboarding steps. Ask for `onboarded: false` at create time (omit it, or
`true`, for a normal onboarded device):

```bash
curl -s -X POST $BASE/devices -H "$AUTH" -H "$JSON" \
  -d '{"name":"Ledger Nano X","device_type":"nanoX","firmware_version":"1.3.0","onboarded":false}'
```

State lives in the GetOsVersion (`e001000000`) answer's `seFlags`: byte 0 has
the onboarded bit, byte 3 the step. Poll `e001000000` to read it and to move
the flow along.

- Starts at WELCOME (step `00`), not onboarded. Dwells here until you send
  `e0030000` (enter early check).
- Now at EARLY_CHECK (step `0f`). Dwells here until you send `e0030001`
  (exit early check).
- After exit, each `e001000000` poll auto-advances one step:
  ChooseName `0c` -> Pin `06` -> SetupChoice `05` -> NewDevice `07` ->
  NewDeviceConfirming `08` -> SafetyWarning `0a` -> Ready `0b`.
- At Ready, byte 0 flips to `e6` (onboarded set) and `GET /devices/<id>` now
  says `onboarded: true`.

```bash
# Walk it: enter, exit, then poll until Ready.
curl -s -X POST $BASE/devices/<id>/apdu -H "$AUTH" -H "$JSON" -d '{"apdu":"e0030000"}'  # enter early check
curl -s -X POST $BASE/devices/<id>/apdu -H "$AUTH" -H "$JSON" -d '{"apdu":"e0030001"}'  # exit early check
curl -s -X POST $BASE/devices/<id>/apdu -H "$AUTH" -H "$JSON" -d '{"apdu":"e001000000"}' # poll (repeat to advance)
```

## Newest firmware

Server has no "latest" list. Read the real version from Ledger, then `PATCH`:

- Page: https://support.ledger.com/article/7103926130845-zd (Stax, Flex,
  Nano Gen5, Nano X, Nano S Plus).
- Read the page fresh each time. Numbers change.

## Touch the screen (Speculos)

Some APDUs wait for a screen tap (e.g. Web3 Check `e032010000` on Ethereum).
No tap = hang until timeout (`6d00`). Tap while APDU is flying:

```bash
# Stax 400x672: accept = x=200,y=537 ; reject = x=200,y=604
curl -s -X POST $BASE/devices/<id>/speculos/finger -H "$AUTH" -H "$JSON" \
  -d '{"action":"press-and-release","x":200,"y":537}'
```

