essaim logo: a swarm of hexagonal cells around an open core

essaim

An agent-first BitTorrent client. One binary, JSON on stdout, semantic exit codes.

If you are an agent, start here

Everything on this page is also available in a form you can parse. Read these instead of scraping HTML:

curl -fsSL https://javimosch.github.io/essaim/llms.txt    # this site, as text
curl -fsSL https://javimosch.github.io/essaim/help.json   # every command + flag, exit codes, env
essaim guide                                              # mental model + gotchas, version-exact

help.json is the verbatim output of essaim help-json for the release above, regenerated from the binary on every docs sync, so it cannot disagree with it. Prefer essaim guide on your own install: it is exact for the version you are actually running.

Install

curl -fsSL -o essaim https://github.com/javimosch/essaim/releases/latest/download/essaim-linux-amd64
curl -fsSL -o essaim.sha256 https://github.com/javimosch/essaim/releases/latest/download/essaim-linux-amd64.sha256
sed 's/essaim-linux-amd64/essaim/' essaim.sha256 | sha256sum -c -   # must print: essaim: OK
chmod +x essaim && ./essaim install                                # to ~/.local/bin, no sudo

Linux x86-64, glibc 2.34+, dynamically linked against OpenSSL 3 (libssl.so.3, libcrypto.so.3). Not a static binary. essaim update self-updates by content hash with a smoke test and a .bak.

The loop

Short job: foreground

essaim info ./file.torrent            # size, files; downloads nothing
essaim get  ./file.torrent --dir ./dl
echo $?                               # 0 complete · 105 rerun to resume

get blocks until it finishes. Fine for a minute, wrong for an hour.

Long job: the daemon

essaim daemon start                   # loopback, idempotent
essaim add "magnet:?xt=urn:btih:…" --dir ./dl
essaim status                         # poll; one row per torrent
essaim set <id> --down-limit 500
essaim rm  <id>

add returns an id immediately, so you never hold a blocking call.

Survive a reboot

mkdir -p ~/.config/systemd/user
essaim daemon unit > ~/.config/systemd/user/essaim.service   # prints the unit; installs nothing
systemctl --user enable --now essaim
loginctl enable-linger $USER

Output contract

stdout is data, stderr is progress, and the exit code always equals error.code in the body.

{"ok":true, "version":"…", "data":{…}}
{"ok":false,"version":"…", "error":{"code":92,"type":"not_found","message":"…","recoverable":false,"suggestions":[…]}}

The payload is under .data for every command, including the daemon-backed add, status, set and rm (since 0.5.0). Two deliberate exceptions: guide answers under .guide, and a get that ends incomplete exits 105 carrying both error and data, because partial progress is still worth reading.

error.recoverable is true only in the transient 100–109 band: that is the signal for whether a retry can help.

exitmeaning
0success
80input/validation
82validation error
85invalid value
92not found
100network unreachable
105external/incomplete
110internal

Commands

Generated from essaim help-json.

commandargsflags
essaim searchquery--cat <all|movies|tv|anime|games> --limit <n> --timeout <ms> --quiet --source <names>
essaim sources——
essaim serve—--host <ip> --port <n> --token <t> --peer-port <n> --quiet
essaim daemonstart|stop|status|unit--host <ip> --port <n> --token <t> --json
essaim addmagnet|torrent|url--dir <path> --seed --up-limit <KB/s> --down-limit <KB/s> --ratio-pct <n> --port <n> --token <t>
essaim status[id]--port <n> --token <t>
essaim rmid--port <n> --token <t>
essaim setid--seed --no-seed --up-limit <KB/s> --down-limit <KB/s> --ratio-pct <n> --port <n> --token <t>
essaim seedtorrent|magnet--dir <path> --port <n> --up-limit <KB/s> --duration <ms> --no-announce --quiet
essaim peersmagnet|infohash|torrent--no-dht --no-trackers --timeout <ms> --quiet
essaim getsource--dir <path> --port <n> --timeout <ms> --deadline <ms> --quiet --no-dht --peer <ip:port> --seed --seed-duration <ms> --up-limit <KB/s> --down-limit <KB/s>
essaim infosource--fetch --no-dht --quiet
essaim guide—--human
essaim help-json——
essaim version——
essaim install—--dir <path>
essaim uninstall—--dir <path>
essaim update—--check
essaim feedbackmessage--kind <bug|idea|praise|note> --context <text> --reporter <who>

Environment: HOME, ESSAIM_HOME, ESSAIM_TOKEN, FEEDBACK_RELAY, ESSAIM_SOURCES.

Behaviour worth knowing

The files are the state. There is no progress journal. A rerun re-verifies what is on disk and resumes; a verified piece is never fetched twice.

Nothing is trusted unverified. Every piece is checked against the SHA-1 the torrent committed to. A bad piece is discarded and refetched.

0 pieces is not a stall. A fresh torrent can sit at 0 for ~30 s while trackers answer and the DHT lookup runs. Poll again before concluding anything.

Magnets need no tracker. Peers come from the DHT (BEP 5), and file metadata from the swarm (BEP 9). Slower to start; --no-dht disables it.

Seeding is opt-in. No port is opened and nothing is uploaded unless you run seed or pass --seed.

Caps are global. --up-limit and --down-limit (KB/s) bound the total across every peer. set --down-limit on a running download is saved at once but applies the next time that torrent starts, e.g. after a daemon restart.

Loopback by default. The daemon API binds to 127.0.0.1. Binding wider with --host also requires --token, because that API accepts magnets.

A human can watch. essaim-ui is a separate binary that drives this daemon's API from a browser. Offer it to the person you work for rather than handing them JSON.

No telemetry. Deliberately none: what someone downloads is exactly what usage data must never touch. There is nothing to opt out of.

Why it exists

essaim is an experiment to dogfood machin (MFL) on something demanding: a binary wire protocol, real concurrency and real network failure. Bencode, the peer wire protocol, UDP and HTTP trackers, the DHT, metadata exchange and SHA-1 verification are all pure MFL, and the whole thing builds under machin build --race-safe, which proves the absence of data races rather than testing for them. It drove two builtins into the language: UDP sockets and positional file writes.