Skip to content

CLI Reference — driving Mercy SF from another program

Deutsche Fassung: cli-api.de.md

mercy-cli has a non-interactive mode: arguments in, one JSON object out. It is meant for dashboards, supervisors and scripts.

This page is the promise. The commands and field names below will not change without notice. Everything else about the CLI — the menu, its wording, the layout of its output — is free to change at any time, so please do not build on it.

Run with no arguments and the interactive menu starts exactly as before.

Every response carries "api". It is 1 today. Adding a field will not change it — a caller reading only the fields it knows stays unaffected. It changes when a field is removed or its meaning changes, so you can refuse to run against a shape you were not built for.


Logging in

Accounts created through the S&F single sign-on need nothing extra. An account that belongs to one server and was never part of the SSO does not exist as far as it is concerned, and needs --server:

bash
mercy-cli --status --user <account> --server https://f1.sfgame.net/

--server works with every command below.

Output

Every command except --start prints JSON and nothing else, so --json is already the default and passing it changes nothing. It exists for callers that would rather say so than rely on a default, and for --start, which otherwise writes a human log.


Reading

List the characters on an account

bash
mercy-cli --characters --user <account>
json
{
  "ok": true,
  "characters": [
    { "character": "Hero", "server": "https://s1.sfgame.net/", "level": 345 },
    { "character": "Alt",  "server": "https://s42.sfgame.net/", "level": 88 }
  ]
}

One login covers every character on the account, across servers. level is null if that character's state could not be fetched — the entry is still listed, because knowing it exists is the point of the call.

Read one character's state

bash
mercy-cli --status --user <account> --character <name>

--character may be omitted when the account has exactly one.

json
{
  "ok": true,
  "character": "Hero",
  "server": "https://s1.sfgame.net/",
  "level": 345,
  "class": "DemonHunter",
  "race": "DarkElf",
  "experience": { "current": 740300000, "next_level": 769800000 },
  "silver": 725000000,
  "mushrooms": 8,
  "honor": 12043,
  "rank": 1800,
  "action": "CityGuard",
  "attributes": {
    "strength":     { "base": 4077, "bonus": 1200 },
    "dexterity":    { "base": 1000, "bonus": 300 },
    "intelligence": { "base": 900,  "bonus": 250 },
    "constitution": { "base": 3000, "bonus": 800 },
    "luck":         { "base": 700,  "bonus": 150 }
  },
  "equipment": [
    {
      "slot": "Weapon",
      "model_id": 55,
      "color": 1,
      "class": "DemonHunter",
      "epic": true,
      "legendary": false,
      "armor": 0,
      "min_damage": 900,
      "max_damage": 1400,
      "upgrades": 3,
      "attributes": { "strength": 420, "constitution": 210 },
      "rune": { "type": "FireDamage", "value": 60 },
      "enchantment": "SwordOfVengeance",
      "gem": { "type": "Strength", "value": 300 }
    }
  ],
  "arena": {
    "fights_for_xp": 7,
    "next_free_fight": "2026-08-06T12:04:11+02:00",
    "opponents": [50099, 34316, 13668]
  },
  "tavern": {
    "thirst_for_adventure_sec": 3600,
    "beer_drunk": 2,
    "mushroom_skip_allowed": true,
    "quests": [
      { "length_sec": 600, "silver": 120000, "experience": 45000,
        "location": "Wolves", "item": true }
    ]
  },
  "guild": {
    "name": "Blvck Clover",
    "rank": 12,
    "honor": 90000,
    "members": [
      { "name": "Hero", "level": 345, "last_online": "2026-08-06T09:12:00+02:00" }
    ]
  },
  "scrapbook_items": 1102
}

Everything comes from a single state fetch, so there are no sections to request and nothing to save by asking for less.

guild is null when the character is not in one. scrapbook_items is null until the scrapbook has been read at least once. An empty equipment slot is omitted rather than listed as null.

On item names: the protocol carries a model id and a slot, not a display name, so that is what you get. It is also what the artwork is addressed by, so a dashboard can render exactly the picture the desktop app does. Inventing a name here would be a guess you could not check.

Repeat a read on an interval

bash
mercy-cli --status --user <account> --character <name> --watch 60

One login, then one object every 60 seconds until the process is stopped. Use this rather than re-running the command in a loop: each run is a fresh login, which is the most expensive way to watch a number and the most conspicuous from the server's side. The floor is 30 seconds. A failed round is printed and the loop carries on — a watcher that gives up on the first hiccup looks exactly like one where nothing is wrong.

What it will fight next, and why

bash
mercy-cli --plan --user <account> --character <name>
json
{
  "ok": true,
  "outcome": "planned",
  "reason": "Next: Someone — 3 scrapbook items, 72% odds",
  "candidates": [
    {
      "name": "Someone",
      "level": 340,
      "source": "pool",
      "missing_items": 3,
      "rank_gain": 12,
      "favourite": false,
      "win_chance": 0.72,
      "item_score": 300.0,
      "xp_score": 60.0,
      "rank_score": 25.0,
      "base": 385.0,
      "score": 277.2
    }
  ]
}

The same decision the app draws: candidates scored on items, daily XP and rank, then multiplied by the odds. outcome is planned, waiting, blocked or empty, and reason says which in one sentence.

This spends no fight and no mushroom. It does crawl for candidates, so it is not free in requests — do not put it on a tight loop.

Rehearse a fight

bash
mercy-cli --simulate --user <account> --character <name> --against <player>
json
{ "ok": true, "against": "Someone", "against_level": 340, "win_chance": 0.63, "iterations": 1000 }

Arithmetic. One lookup goes to the server to find out who they are; the fight itself is never sent. win_chance is wins over decided fights, so a simulation that cannot resolve reports on fewer than it ran.

What is carried

bash
mercy-cli --inventory --user <account> --character <name>
json
{ "ok": true, "carried": 12, "backpack": [ { "slot_index": 0, "model_id": 55 } ] }

One list, because the game keeps one. Items have the same shape as in --status, plus slot_index.

Scrapbook

bash
mercy-cli --scrapbook --user <account> --character <name>
json
{ "ok": true, "unlocked": true, "owned": 1102, "monsters": 214 }

unlocked is false below the level that grants a scrapbook, and owned is null there. That is not an error.

Recorded fights

bash
mercy-cli --history --user <account> --character <name> --limit 50
json
{ "ok": true, "total": 4210, "returned": 50, "battles": [] }

Newest first. These are recorded by the installation that ran the bot, not fetched from the server — a process that has just started has nothing to show, and that is not a fault. total is what exists, returned is what --limit left.


Acting

Run the bot

bash
mercy-cli --start --user <account> --character <name>

Runs until the process is stopped, logging as it goes. --character is required here: listing and reading state are harmless, but starting the bot plays the game, and picking the wrong character is not something you can undo.

--all runs every character of the account in this one process instead. That is one login for the account rather than one per character, which is cheaper and stops the characters invalidating each other's session.

On Ctrl-C (or SIGTERM) the current move is allowed to finish before the process exits, rather than being cut off mid-request.

Events instead of prose

bash
mercy-cli --start --user <account> --character <name> --events

One JSON object per line, so a supervisor reads a line at a time:

json
{"at":"2026-08-06T17:22:04+02:00","api":1,"event":"arena_win","character":"Hero"}
{"at":"2026-08-06T17:22:31+02:00","api":1,"event":"quest_done","character":"Hero"}

event is one of started, stopping, stopped, arena_win, arena_loss, dungeon_win, dungeon_loss, quest_done, scrapbook_win, scrapbook_loss, scrapbook_items (which carries count).

This list will grow. Treat an event you do not recognise as one to ignore, not as an error — that is the whole reason the names are spelled out rather than being whatever Rust happened to print.

Use this rather than reading the human log. That log is written for a person and is free to be reworded at any time; parsing it means your tool breaks when somebody improves a sentence.

Collect what is waiting

bash
mercy-cli --claim --user <account> --character <name>

Calendar, daily tasks, pending unlocks. --character is required, for the same reason as --start.

Read or change the configuration

bash
mercy-cli --config --user <account> --character <name>
mercy-cli --config --user <account> --character <name> --set auto_arena=false
json
{ "ok": true, "settable": ["auto_arena"], "changed": [ { "key": "auto_arena", "from": true, "to": false } ] }

Reading returns the whole configuration under config. The field names inside that block are not part of this promise — only the keys listed in settable are. The configuration has well over a hundred fields and grows every release; promising all of them would mean never being able to reshape the settings again.

--set accepts true/false, on/off, yes/no, 1/0, takes --character always (a switch belongs to one character, and "the only one" is a guess that is fine to read by and wrong to write by), and can be repeated. The value is read back from disk afterwards and the call fails if it did not land — reporting a change that did not happen is the one outcome you cannot recover from.


Operating

Which build is this

bash
mercy-cli --version
json
{ "ok": true, "version": "2.12.0", "api": 1 }

The only command that needs neither a password nor a network. A supervisor has to be able to ask what it is talking to before it has credentials to talk with.

Is it reachable

bash
mercy-cli --health --user <account> --character <name>
json
{ "ok": true, "character": "Hero", "level": 345, "round_trip_ms": 412 }

Log in, fetch, report, exit. Deliberately small: a health check that returns a full character sheet invites parsing it, and then the check becomes an interface of its own.


The password

There is no --password flag, on purpose. Command line arguments are readable by every other process on the machine — ps, /proc, Task Manager — so a dashboard running this for several people would be handing all of their game passwords to anyone with a shell on that box.

Two ways instead:

bash
MERCY_SF_PASSWORD='…' mercy-cli --status --user someone
bash
printf '%s' "$password" | mercy-cli --status --user someone --password-stdin

--password-stdin reads the first line of standard input, which keeps the secret out of the environment as well.


Exit codes

Code Meaning
0 Success, and for --help
1 It ran and failed — the reason is in error, and ok is false
2 The call was wrong: unknown option, missing value, no action. On stderr

The difference between 1 and 2 matters to a supervisor: 2 will fail the same way every time and needs a person, 1 may well work on the next attempt.


Errors

Failures are JSON too, so one parser handles both:

json
{ "ok": false, "error": "no character called 'Typo' on this account" }

ok is present on every response. Check it before anything else.

Common causes:

Message What to do
--user is required --user is missing (every action except --version)
--start needs --character (or --all) Name the target; it will not be guessed
this account has N characters — name one with --character Several exist, pick one
no character called '…' on this account Typo; --characters lists the valid names
no password: set MERCY_SF_PASSWORD or pass --password-stdin Choose one of the two password routes
login failed: … Check the credentials or the server; non-SSO accounts need --server
'…' is not settable from here Not on the allow-list; --config without --set prints settable
'…' did not save — it still reads back unchanged The change did not land; check write permissions

Recipes

Collect on every account once a day

bash
for account in alpha beta gamma; do
  MERCY_SF_PASSWORD="$(pass mercy/$account)" \
    mercy-cli --claim --user "$account" --character Hero || echo "failed: $account"
done

Keep a dashboard tile current

bash
MERCY_SF_PASSWORD='…' mercy-cli --status --user someone --character Hero --watch 60

One login, one object a minute. Do not re-run this from cron every minute instead — that is a fresh login each time.

Record events to a file

bash
MERCY_SF_PASSWORD='…' mercy-cli --start --user someone --character Hero --events \
  >> events.ndjson

Check what you are talking to before logging in

bash
api=$(mercy-cli --version | jq -r .api)
[ "$api" = "1" ] || { echo "unknown contract version: $api"; exit 1; }

As a systemd service

ini
[Service]
Environment=MERCY_SF_PASSWORD=…
ExecStart=/usr/local/bin/mercy-cli --start --user someone --all --events
Restart=on-failure
KillSignal=SIGTERM

SIGTERM lets the current move finish. Please do not SIGKILL it.


What this does not do yet

Starting several characters and stopping them individually (--all starts them all and stops them all together), a control channel into a running process, and changing anything beyond the module switches.

If you need one of those, say which — it is easier to build the right thing than to guess and have it go unused.

Mercy SF is not affiliated with Shakes & Fidget. Use at your own risk.