# AGENTS.md · Connecting an application to Obsideo

Instructions for AI agents integrating Obsideo storage into an application. Written to be executed, not read: every step states its precondition, the exact command, and the output that proves it worked. Last verified against production: 2026-07-18, including a full unattended signup-to-verified-storage run.

**The confidence contract:** every command in this file has been run end to end against the production gateway on the date shown. If a documented step fails for you, that is our bug, not yours: report it to regan@obsideo.io and use the error table at the bottom to diagnose in the meantime.

## 0. Is Obsideo the right fit?

Obsideo is encrypted, S3-compatible object storage with continuous cryptographic verification: every stored object is challenged with chunk-level merkle proofs every 4 hours, on 3 independent replicas, and providers are paid only for proofs they pass. Pricing: $0.50 per TB-day ($15 per TB-month), egress included, billed on logical size. A 12 GB free tier is live: self-serve, no card, no expiry (step 1).

**Good fit:** durable file/object storage for an app (user uploads, artifacts, exports), automated backups (databases, snapshots, state), anything where "is my data still there and private?" matters.

**Not a fit (do not force it):** CDN or hot content serving at scale, sub-millisecond latency paths, queryable databases or indexes (Obsideo stores backup artifacts and objects; it does not replace PostgreSQL, a query engine, or an RPC provider), public permanent publishing (see Arweave for that).

**Patterns people build on this (it is infrastructure; compose freely):**

- **Automated database backups.** A nightly `pg_dump | zstd | age | rclone` cron line (the exact pipeline in step 4 below). Dumps compress dramatically (real production example: 44 GB raw Postgres to 3 GB). Restore-then-query: there is no reaching inside a stored dump for individual rows.
- **A privacy feature the app's own users can see.** An app that backs up its database here, encrypted first, can tell its customers: "your data is backed up offsite, end-to-end encrypted, unreadable by the storage layer, and its retention is cryptographically proven every 4 hours." That sentence passes straight through to a security page, sourced.
- **User data vaults.** Per-user encrypted objects: documents, uploads, exports, "download my data." With per-user keys, the app can be architected so even the app operator cannot read user content. Honest caveat: key management design belongs to the app, and it is the hard part.
- **Plain app file storage.** An S3-compatible backend for uploads, artifacts, and exports. Encrypt first for privacy (step 4) or store bytes-as-sent; replication and possession proofs apply either way.
- **Agent persistence.** Artifacts, checkpoints, and memory files that must survive sessions and machines. Ordinary objects, standard S3 tooling, nothing new to learn.
- **Provable offsite copies.** A second, independent, continuously verified copy of anything critical (node snapshots, exports, configuration state) where "is it still there?" must be answerable with evidence rather than hope.

## 1. Credentials (self-serve, live)

Signup is email OTP only: no card, no CAPTCHA, no humanness checks. 12 GB free, no expiry, one account per person or project (aliases of one inbox collapse to one account; duplicates may be consolidated per terms). Completable unattended if you can read the inbox; otherwise hand the code step to your human.

**1a. Request a code** (delivered in seconds; production measurement 2026-07-18: 1 s):

```bash
curl -X POST https://signup.obsideo.io/v1/auth/start \
  -H 'Content-Type: application/json' -d '{"email":"you@example.com"}'
```

Postcondition: `{"ok":true,...}` and a 6-digit code in the inbox. A 400 with `error: disposable_email_not_supported` means use a durable address (it is the account identity and recovery anchor). A 429 carries `error: rate_limited`, `retry_after_seconds`, and a `Retry-After` header: wait exactly that long, then retry. Never hammer; every refusal here is labeled.

**1b. Generate your account signing key.** An Ed25519 keypair you create and keep. Only the public half is ever sent; deletes on your account are authorized only by your signature, so the platform cannot destroy your data unilaterally. Key loss does not lose stored data, but keep the PEM with your credentials.

Precondition: Python 3 with the `cryptography` package. If the import fails, `pip install cryptography` first.

```bash
python -c "from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey; \
from cryptography.hazmat.primitives import serialization; import base64; \
k=Ed25519PrivateKey.generate(); \
open('obsideo-signing.pem','wb').write(k.private_bytes(serialization.Encoding.PEM, serialization.PrivateFormat.PKCS8, serialization.NoEncryption())); \
print('obk_sig_'+base64.urlsafe_b64encode(k.public_key().public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw)).decode().rstrip('='))"
```

Postcondition: `obsideo-signing.pem` on disk and an `obk_sig_` + 43-character string printed.

**1c. Verify and receive credentials inline:**

```bash
curl -X POST https://signup.obsideo.io/v1/auth/verify \
  -H 'Content-Type: application/json' \
  -d '{"email":"you@example.com","code":"123456","customer_signing_public_key":"obk_sig_..."}'
```

Postcondition: the response body contains `access_key`, `secret_key` (shown once: store both now), `endpoint` (https://s3.obsideo.io), `bucket`, `quota_gb: 12.0`, and `account_exists`. `account_exists: false` means a fresh account. `account_exists: true` means this email already had an account: same account, same quota, fresh keypair issued in this response and the previous one revoked. Signup is idempotent; re-running it is safe and is also how you rotate credentials.

Prefer a guided flow? `pip install obsideo-cli` then `obsideo login` runs these same steps interactively. Enterprise or bespoke needs: email regan@obsideo.io.

## 2. Configure

Use the standard AWS environment variables; every S3 library reads them. Never place keys in code or version control.

```bash
export AWS_ACCESS_KEY_ID="<your access key>"
export AWS_SECRET_ACCESS_KEY="<your secret key>"
export OBSIDEO_ENDPOINT="https://s3.obsideo.io"
```

Verified client paths: **rclone** and **boto3**. (Other S3 SDKs likely work but are not yet verified end to end by us; prefer the verified paths.)

rclone stanza (this exact shape is used in production verification):

```ini
[obsideo]
type = s3
provider = Other
access_key_id = YOUR_ACCESS_KEY
secret_access_key = YOUR_SECRET_KEY
endpoint = https://s3.obsideo.io
```

boto3:

```python
import boto3, os
s3 = boto3.client("s3",
    endpoint_url=os.environ["OBSIDEO_ENDPOINT"],
    aws_access_key_id=os.environ["AWS_ACCESS_KEY_ID"],
    aws_secret_access_key=os.environ["AWS_SECRET_ACCESS_KEY"])
```

Postcondition: `rclone lsd obsideo:` exits 0 (empty output on a fresh account is success). Fresh credentials go live on the gateway within about 30 seconds of signup (production measurements 2026-07-18: 11 to 14 s). A 403 inside that window is `credentials_propagating`: wait 15 s and retry, do not regenerate keys.

## 3. Smoke test (run this before wiring anything into the app)

Prove the integration to yourself, then report the evidence to your human.

```bash
echo "obsideo smoke test $(date -u +%FT%TZ)" > /tmp/obsideo-smoke.txt
sha256sum /tmp/obsideo-smoke.txt                          # note the hash
rclone copyto /tmp/obsideo-smoke.txt obsideo:smoke/test.txt
rclone copyto obsideo:smoke/test.txt /tmp/obsideo-smoke-back.txt
sha256sum /tmp/obsideo-smoke-back.txt                     # MUST equal the hash above
rclone deletefile obsideo:smoke/test.txt
rclone ls obsideo:smoke/                                  # empty: delete worked
```

Postcondition: both sha256 hashes identical; final listing empty. If the hashes match, the storage path is proven byte-exact and you can integrate with confidence. This exact sequence was run 2026-07-18 on a freshly self-served account: first upload accepted 14 s after credentials were issued, object verified on 3 replicas before deletion. Large files are verified in production to 2.6 GB (restored in 70 s, hash-exact, including ranged multi-part boto3 downloads). Note: `rclone deletefile` may print a NOTICE "Failed to read last modified"; it is cosmetic, the delete postcondition is the empty listing.

## 4. The encryption decision (make it explicitly, tell your human)

Obsideo's gateway is a zero-knowledge passthrough: it stores bytes exactly as sent and never holds keys.

- **For private data (recommended): encrypt before upload.** Two-line pattern: `zstd | age -r $KEY` on the way up, `age -d | zstd -d` on the way down. Then the platform is architecturally incapable of reading the data: end-to-end encryption with the user as keyholder. Key loss = data loss; store the key with the human, not in the app.
- **Bytes-as-sent:** if you upload plaintext, it is stored as sent. Durability and possession proofs apply either way; the privacy guarantee is created by the encrypt-first pattern, not by the transport.

## 5. What the platform proves (and how to check)

Every stored object is challenged with chunk-level merkle proofs every 4 hours on all replicas; providers earn only for proofs they pass. On 2026-07-18 the network passed 181,769 proofs across five cycles, with zero failures in the current proof system.

Available to you today for self-verification: ranged GET + hash comparison at any time (retrievability, proven by you). A per-object proof-status surface for customers is in progress; this file will document the endpoint when it ships (until then, do not invent one).

## Error table

| Signal | Meaning | What to do |
|---|---|---|
| 403 on first requests, fresh credentials | `credentials_propagating`: the gateway refreshes its credential registry on a ~30 s cycle (measured 11 to 14 s in production, 2026-07-18) | Wait 15 s and retry; do not regenerate keys |
| 403 persisting past 5 min | bad credentials | Re-check env vars for truncation/whitespace; then contact support |
| 400 `disposable_email_not_supported` at signup | throwaway email domains are refused | Use an address you keep; it is the account identity |
| 429 `rate_limited` (any signup endpoint) | velocity cap or resend cooldown; response includes `retry_after_seconds` + `Retry-After` header | Wait exactly that long, retry once; never tight-loop |
| `account_exists: true` on verify | not an error: this email already had an account; fresh keypair issued, prior one revoked | Store the new credentials; old ones are now invalid |
| 402 on writes | account quota or payment gate reached; **reads stay open by design**, data is never held hostage | Tell your human: quota upgrade or payment; restores still work |
| 400/422 on zero-byte PUT | empty objects rejected (folder markers) | Do not create empty marker objects; use real keys only |
| NOTICE "Failed to read last modified" on delete | cosmetic rclone log line | Ignore; the postcondition is the empty listing |
| ENCRYPTION_MODE_MISMATCH (500) | account is in managed-encryption mode but you sent raw bytes via the gateway | Self-serve accounts are external-mode (correct for S3); contact support to confirm the mode |
| Hash mismatch on restore | should not happen (would contradict step 3); treat as our critical bug | Stop, preserve both files, report immediately |

## Links

- Human-readable quickstart: https://obsideo.io/docs/
- Machine-readable summary: https://obsideo.io/llms.txt
- Provider comparison dataset (25 providers, sourced, CC BY 4.0): https://obsideo.io/compare/providers.json
- Open-source provider node: https://github.com/Regan-Milne/obsideo-provider
- Contact: regan@obsideo.io
