Everything below is in the package. Nothing here talks to a service you do not run: the relay is one Node process, the machines are one binary each, and the only thing crossing the network between them is your own agent traffic.
| The relay | A Linux host with Node.js 20+, a DNS name, and a TLS terminator (nginx, Caddy). About 300 MB of disk with the binaries it hands out. It holds no agent credentials. |
| A donor machine | Linux or Windows, with the coding agent (Claude Code, Codex) installed and signed in as its owner. This is the machine whose subscription gets lent. |
| A consumer machine | Linux, macOS or Windows. Nothing installed — one command does it. |
tar xzf tinybox-network-0.5.477-swallowed.tar.gz
cd tinybox-network-0.5.477-swallowed
cp config.example.json config.json
Edit config.json. The five fields that matter:
| instance.id | Short id for this node, e.g. node1. It appears in the console and in federation. |
| domains.node | The public name this node answers on, e.g. tinybox.example.com. No domain is hardcoded anywhere else — this is the only place one appears. |
| server.port | The loopback port the app listens on (default 5320). Your TLS terminator proxies to it. |
| server.dataDir | Where accounts, sessions and the usage ledger live. Outside the release directory on purpose, so an upgrade can never take it with it. |
| access.invitationCode | Registration is by invitation. Change this. |
cd server && npm ci --omit=dev
AMERC_CONFIG=../config.json AMERC_PORT=5320 \
AMERC_DATA_DIR=/var/lib/tinybox-network node app.js
As a service, the package ships deploy/amerc.service.template; fill in the paths
and drop it in /etc/systemd/system/. In front of it, any reverse proxy that
forwards websockets:
location / {
proxy_pass http://127.0.0.1:5320;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade; # "upgrade" when $http_upgrade is set
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 3600s;
}
Upgrade gives you a
console that loads and a relay that never connects.The package's deploy/deploy.py does all of the above over ssh from a
deploy/nodes.json if you would rather describe the boxes than log into them.
A fresh node ships no accounts at all. Nothing is pre-seeded, so there is no default login to look up and none to forget to change.
Open the node and go to /setup. It is open exactly once: you choose the
first administrator's username and password there, and the moment that account exists
/setup stops answering and every other page redirects to sign-in.
/setup, whoever reaches the port first becomes the
administrator. On a box that is already public, start the service bound to loopback, run
/setup through an SSH tunnel, and only then open the proxy.That first account is also the node's first donor and consumer — it is given the whole pool, so you can put your own machine on it immediately without editing anything.
Everyone after you registers at /register with an invitation code. The one
in access.invitationCode is what a new person types; an administrator can also
issue single-use codes from Administration → Accounts & access → Invitations,
each carrying the rights and shards that account should start with.
| Forgot the password | There is no recovery link. Another administrator can set it from Accounts & access (Set password / Reset password). If it is the only administrator, stop the service and edit status/passwordHash in <dataDir>/users/<name>.json — it is plain JSON on purpose. |
| Locked out entirely | Move <dataDir>/users/ aside and restart: with no users, /setup answers again. Sessions, donors and the usage ledger are separate files and survive. |
Sign in, open TinyBox → Donor (or Consumer), copy the line. The address carries that account's token, so nothing is typed on the machine itself:
# the machine that LENDS its agent
curl -fsSL 'https://tinybox.example.com/donor.sh?k=<token>&config=claude' | sh
irm 'https://tinybox.example.com/donor.ps1?k=<token>&config=claude' | iex
It installs into ~/.local/share/amerc/donor-<agent> (or
%LOCALAPPDATA%\amerc\donor-<agent>), starts the supervisor, and prints the
pid, the folder, the log and how to stop it. The supervisor keeps the binary up to date from
your node — no cron job, no scheduled task.
Administration → TinyBox relay network → Donors. Each machine reads OK, or BAD with the one reason that stops it — not answering, not signed in, renewing its login, out of quota, or its agent will not start. Quota is every window the seat publishes, not just the worst one.
Sessions shows who is attached to whom, with per-leg round trips. Usage is what each account has run up, historical and ongoing. Binaries is every build this node serves and which channel it is on.
Give every node the same federation.serverKey (64 hex characters; any
one of them can generate it) and list them all in federation.peers:
"federation": {
"enabled": true,
"serverKey": "<the same 64 hex characters on every node>",
"bridge": { "enabled": true, "host": "0.0.0.0", "port": 30453 },
"peers": [
{ "id": "node1", "url": "https://tinybox.example.com",
"bridge": { "host": "203.0.113.10", "port": 30453, "dialFrom": ["node2"] } },
{ "id": "node2", "url": "https://tinybox.example.cn",
"bridge": { "host": "198.51.100.20", "port": 30453, "dialFrom": [] } }
]
}
dialFrom says which peers dial this one, so a node behind NAT can list
nobody and still be reached — the other side dials it. Accounts, invitations and donor
rosters are then shared: a machine reaches the node nearest to it and can still be served by
capacity donated on the other side of the world.
| The console loads, no donor appears | The websocket upgrade is not getting through the proxy, or the account has no pool. Administration → Logs & requests shows the request ring: a 403 on /ws/relay is a pool that does not include that agent. |
| A donor says BAD | The row carries the reason. "not signed in" means exactly that: sign in to the agent on that machine, as its owner. The relay cannot and will not do it for you. |
| A donor keeps reconnecting | Two donor programs for the same agent. Stop the extra one; the row says so once it has happened twice in two minutes. |
| Sessions die mid-conversation | The donor's login expired. The row shows how long each login has left, and a donor renews its own if it can. |
It does not hold your provider credentials, proxy your model API, or see inside a session beyond what it must relay. A donor lends its own login, in place, on its own machine; a session can refresh it (which is how it stays alive at all), and a session that signs it out signs it out. That is a trust boundary, not a file permission, and it belongs with who you lend to.