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
  1. The agent fetches https://hermesbackup.com/agent (mirrored at /llms.txt) for exact install instructions.
  2. The skill's setup calls POST /v1/pair/start (unauthenticated) with the device hostname → receives a pairing code (FROG-8241) and poll token.
  3. 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).
  4. The script polls POST /v1/pair/poll every 5 seconds (10-minute expiry). On approval it receives a device token, stored chmod 600.
  5. Setup generates the age keypair, prints the recovery kit, asks about schedule (default: daily --quick at 03:00 local + weekly full on Sundays) and on_full policy, writes cron entries, runs a first backup immediately, and confirms end-to-end success.
Agents authenticate with 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

POST
/pair/start
Auth: none. Body: {device_name}. Returns {pair_code, poll_token, expires_at}.
POST
/pair/poll
Auth: none. Body: {poll_token}. Returns 202 pending or {device_token, device_id} once approved.
POST
/pair/approve
Auth: Clerk. Dashboard-only — agent never calls this. Approves {pair_code} for the signed-in user.

Backups

POST
/backups/init
Auth: device. Body: {size_bytes, sha256, label, kind} (kind: full|quick). Checks quota + rate limits. Returns {backup_id, upload_url} or 507.
POST
/backups/{id}/complete
Auth: device. Server HEADs the R2 object, verifies size + SHA-256 checksum, marks the row stored, updates usage.
GET
/backups
Auth: device or Clerk. List backups: id, label, kind, size, sha256, created_at, device.
GET
/backups/{id}/download
Auth: device or Clerk. Returns {download_url} (presigned GET, 5-min expiry).
DEL
/backups/{id}
Auth: device or Clerk. Delete object + row; tombstone retained 7 days.

Account

GET
/quota
Auth: device or Clerk. Returns {plan, used_bytes, limit_bytes, object_count}.
GET
/devices
Auth: Clerk. List devices: name, id, paired-date, last-backup, health, revoked.
DEL
/devices/{id}
Auth: Clerk. Revoke device. Existing backups are untouched.
POST
/webhooks/stripe
Auth: signature. Billing state changes.
POST
/webhooks/emailit
Auth: signature. Email bounce handling.
Incomplete uploads (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:

ItemFree userPro (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.