Documentation
Reference
Setup & pairing
The entire UX is one instruction from the user to their agent:
# user tells Hermes Agent:
Set up cloud backups for yourself using hermesbackup.com
- The agent fetches
https://hermesbackup.com/agent(mirrored at/llms.txt) for exact install instructions. - The skill's
setupcallsPOST /v1/pair/start(unauthenticated) with the device hostname → receives a pairing code (FROG-8241) and poll token. - The agent tells the user: "Go to hermesbackup.com/pair and enter code FROG-8241" (sign-up via Clerk happens in under a minute if no account exists).
- The script polls
POST /v1/pair/pollevery 5 seconds (10-minute expiry). On approval it receives a device token, stored chmod 600. - Setup generates the age keypair, prints the recovery kit, asks about schedule (default: daily
--quickat 03:00 local + weekly full on Sundays) andon_fullpolicy, writes cron entries, runs a first backup immediately, and confirms end-to-end success.
Authorization: Bearer <device_token>. The dashboard uses Clerk JWT. Agents never touch Clerk.API reference
Base URL: https://api.hermesbackup.com/v1. JSON everywhere. Errors as {"error": {"code", "message"}}.
Pairing
Auth: none. Body:
{device_name}. Returns {pair_code, poll_token, expires_at}.Auth: none. Body:
{poll_token}. Returns 202 pending or {device_token, device_id} once approved.Auth: Clerk. Dashboard-only — agent never calls this. Approves
{pair_code} for the signed-in user.Backups
Auth: device. Body:
{size_bytes, sha256, label, kind} (kind: full|quick). Checks quota + rate limits. Returns {backup_id, upload_url} or 507.Auth: device. Server HEADs the R2 object, verifies size + SHA-256 checksum, marks the row
stored, updates usage.Auth: device or Clerk. List backups: id, label, kind, size, sha256, created_at, device.
Auth: device or Clerk. Returns
{download_url} (presigned GET, 5-min expiry).Auth: device or Clerk. Delete object + row; tombstone retained 7 days.
Account
Auth: device or Clerk. Returns
{plan, used_bytes, limit_bytes, object_count}.Auth: Clerk. List devices: name, id, paired-date, last-backup, health, revoked.
Auth: Clerk. Revoke device. Existing backups are untouched.
Auth: signature. Billing state changes.
Auth: signature. Email bounce handling.
init with no complete within 24 hours) are garbage-collected by a cron trigger.D1 schema (abridged)
users(id TEXT PK, clerk_id TEXT UNIQUE, email TEXT, plan TEXT DEFAULT 'free', stripe_customer_id TEXT, used_bytes INTEGER DEFAULT 0, limit_bytes INTEGER DEFAULT 2147483648, created_at INTEGER); devices(id TEXT PK, user_id TEXT REFS users, name TEXT, token_hash TEXT UNIQUE, last_backup_at INTEGER, expected_interval_hours INTEGER DEFAULT 24, revoked INTEGER DEFAULT 0, created_at INTEGER); backups(id TEXT PK, user_id TEXT, device_id TEXT, r2_key TEXT UNIQUE, label TEXT, kind TEXT, size_bytes INTEGER, sha256 TEXT, status TEXT DEFAULT 'pending', -- pending|stored|deleted created_at INTEGER, deleted_at INTEGER); pairings(pair_code TEXT PK, poll_token_hash TEXT UNIQUE, device_name TEXT, user_id TEXT, status TEXT DEFAULT 'pending', expires_at INTEGER);
Dashboard
Served from Worker static assets at hermesbackup.com. Auth: Clerk JWT via middleware. Built as a small React app.
- Backups — list, download (presigned GET), delete, usage meter with 4 states (normal / warning / critical / full).
- Devices — pairing approval inline, revoke with confirmation, last-seen and health badges (healthy / silent / overdue / revoked).
- Billing — plan status, Stripe Customer Portal deeplink, planned tiers as read-only placeholders.
- Settings — email prefs, delete account (purges all objects).
Restore on a new machine
# on the new machine, after installing Hermes Agent: hermesbackup.sh restore # flow: No device paired. Starting pairing flow... Pair code: OTTER-3091 … Select backup to restore [1-3]: 1 Downloading ciphertext via presigned URL... Decrypting with age identity... Enter AGE-SECRET-KEY-1: ********** ✓ SHA-256 verified: 7f2a...e910 matches server record ✓ Plaintext written to /tmp/hermes-restore-XXXX/ Importing to ~/.hermes/... ✓ 4,579 files imported. SOUL.md, MEMORY.md, skills/, sessions/ restored. # done. Your agent remembers everything.
Retention & quota
Backups accumulate until the account hits its quota (2 GB free / 10 GB Pro). When an init would exceed quota, the API returns 507 INSUFFICIENT_STORAGE with current usage. The skill then follows its configured policy:
on_full = "fail"(default): abort, log, server emails the user.on_full = "rotate": skill deletes the oldest backups via the API until the new one fits, then retries. It's automatic pruning by client choice, so the server never silently destroys data.
Deletes are immediate (object + row) with a 7-day tombstone record. Quota warnings email at 80% and 95%. The server never silently deletes user data — invariant.
Email plan
Transactional only, via Emailit REST v2 with webhook-driven bounce suppression. Pointedly: the recovery-kit reminder does not contain the key.
- Welcome + recovery-kit reminder
- Device-paired confirmation
- First-backup success
- Backup-failure alert
- Missed-schedule nudge (device silent > 2× expected interval)
- Quota 80% / 95%
- Payment failed (dunning ×3) / renewal receipt
- Account-deletion confirmation
Estimated volume: ~15 emails per user per year — inside the free 1,000/month until roughly 800 active users.
Unit economics
Per user per year:
| Item | Free user | Pro (avg 4 GB) | Pro (10 GB worst case) |
|---|---|---|---|
| Revenue | $0 | $10.00 | $10.00 |
| Stripe fee (2.9% + $0.30) | — | −$0.59 | −$0.59 |
| R2 storage ($0.015/GB-mo) | −$0.18 | −$0.72 | −$1.80 |
| R2 egress | $0 | $0 | $0 |
| Workers / D1 / KV marginal | ~$0.01 | ~$0.01 | ~$0.01 |
| Net contribution | −$0.20 | +$8.67 | +$7.59 |
R2's zero egress is why this business works at $10/year. A 10 GB restore on S3 would cost ~$0.90 — nearly 10% of annual revenue gone in one click. On R2 it costs us nothing.
Fixed cost floor: ~$75–100/year (Workers Paid $60, domain ~$10, Emailit ~$0–20). Break-even at roughly 12 Pro subscribers.