Appearance
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 60One 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 50json
{ "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> --eventsOne 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=falsejson
{ "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 --versionjson
{ "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 someonebash
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"
doneKeep a dashboard tile current
bash
MERCY_SF_PASSWORD='…' mercy-cli --status --user someone --character Hero --watch 60One 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.ndjsonCheck 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=SIGTERMSIGTERM 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.