DigitalOcean · Opus 5.5 · medium effort · HIPAA organization · trial 1

API / Backend Service on DigitalOcean

Success

Checks passed20/20
Human steps37 min of a person's time
Security & compliance82/100
Stack fit87/100
Build2m 59s
Deploy8m 55s
Tokens1.51M
Agent cost$1.21
Hosting / month$12.00 light use$12.00 busy

Deployed to https://adb-5e3ks-api-omhxz.ondigitalocean.app. Every deployment is deleted after it's verified and reviewed.

Screenshots

The API documentation at /docs
The API documentation at /docs · https://adb-5e3ks-api-omhxz.ondigitalocean.app/docs

Technologies

Runtime
Python 3.12
Frameworks
FastAPI
Data
PostgreSQL (App Platform dev database)
Libraries
UvicornpsycopgSQLAlchemy
Dev tools
HTTPXpytest
Packaging
Docker (python:3.12-slim)App spec (.do/app.yaml)
Hosting
App Platform service (apps-s-1vcpu-0.5gb)DigitalOcean Container Registry (starter)

Named from the app's dependencies, Dockerfile and config files, and the cloud resources it created.

Checks

Black-box checks against the live URL. A cell succeeds when every required check passes. The app's spec

ResultCheckDetails
passed GET /health answers 2xx within 1 min ready
passed The app is served over HTTPS with a valid certificate https
passed The app is public: no redirect to another host such as a login page public_access
passed GET /health returns {status: ok} health
passed GET /openapi.json is an OpenAPI 3 document covering /v1/notes openapi
passed GET /docs serves human-readable documentation docs optional
passed Requests without an API key get 401 auth_missing
passed Requests with an unknown API key get 401 auth_invalid
passed POST /v1/notes creates a note create
passed Invalid notes are rejected with 400 create_validation
passed GET /v1/notes/{id} returns the note get
passed GET /v1/notes lists notes and filters by tag list
passed Notes are listed newest first list_order optional
passed PATCH /v1/notes/{id} updates only the given fields update
passed Notes are private to the API key that created them isolation
passed DELETE /v1/notes/{id} removes the note delete
passed Notes are still there a minute later persistence
passed Exceeding 60 requests/minute returns 429 rate_limit
passed 429 responses include a Retry-After header rate_limit_retry_after optional
passed One key hitting its limit doesn't throttle other keys rate_limit_per_key

Human steps

Stack fit

The same review rated the stack the agent chose, judged against the app's requirements rather than a preferred stack.

Python/FastAPI with SQLAlchemy Core on App Platform plus an attached Postgres dev database is a well-proportioned, idiomatic fit for a small authenticated JSON API, and STACK.md explains the choice soundly. The build matches the documented decision.

CriterionWeightScoreWhy
Fit for the job 40 9/10 FastAPI with Pydantic provides validation, the OpenAPI 3 document and docs out of the box, and Postgres is a durable store that survived a redeploy. The in-memory rate limiter is correct for the single instance it runs on. Storing tags as a JSON text column and filtering by tag in Python works but is not ideal.
Proportion 30 9/10 The stack is minimal: one service, one database and four direct dependencies, with no extra cache or queue. SQLite is used only for local tests.
Platform fit 30 8/10 It uses App Platform's managed TLS, health checks, encrypted secrets and the database binding that injects DATABASE_URL. The agent built the image locally and pushed it to DOCR instead of letting App Platform build from a repo, which works but skips the platform's own build pipeline.

Security & compliance review

An independent Claude Code session reviewed the source, the agent's sessions and the live deployment against a fixed rubric (claude-opus-5-5, high effort).

The Notes API is a small, cleanly built FastAPI service on paid DigitalOcean App Platform with an attached Postgres dev database. API keys are stored as an encrypted platform secret, the container runs as non-root, it is served only over HTTPS, and probing found no exposed files, debug endpoints or stack traces. The remaining weaknesses are minor: the base image and transitive dependencies are not pinned, security headers are missing, the dev database is internet-reachable, and malformed bodies get 400 before the auth check, so unauthenticated requests are never rate-limited.

CriterionWeightScoreWhy
Secrets handling 25 9/10 API_KEYS is stored as an encrypted App Platform SECRET (EV[...] in the live spec) and DATABASE_URL is a platform binding. The spec template uses a placeholder, keys are injected into a temp file that is deleted afterwards, and no secret shows up in the transcripts or the source. The only small weakness is that the plaintext keys sit briefly in a temp spec file on the deploy machine.
Deployment provenance 20 7/10 The agent built the image from the reviewed source, pushed it to the owner's own DOCR registry, and pointed the spec at that unique timestamp tag. Direct dependencies are version-pinned, but the base image python:3.12-slim is not pinned by digest, transitive dependencies (pydantic, starlette) are unlocked, and there are no hashes. The spec references a tag, not an image digest.
Terms compliance 15 9/10 The app is personal and runs on paid DigitalOcean App Platform ($5 basic instance) with a $7 dev database, both of which allow this use and stay up without sleeping. A dev database has limited capacity and no managed backups or HA, which is acceptable for a small side project but worth knowing about.
Least privilege 15 7/10 The container runs as a non-root user, only the one web service is public, and the registry is private. The App Platform dev database cannot be limited to trusted sources, so it is reachable from the internet (protected by TLS and credentials). The app connects as the database owner and runs create_all at startup.
Transport security 10 8/10 HTTP returns 301 to HTTPS, and the certificate is valid (Google Trust Services, expires Dec 2026). The responses carry no HSTS, X-Content-Type-Options, frame protection or CSP headers. Neither the app nor the platform sets them.
Attack surface 10 9/10 Paths such as /.env, /.git/config, source paths, /admin, /debug and /redoc all return JSON 404s. A catch-all handler stops stack traces from leaking, and only the documented public endpoints exist. The Dockerfile copies only the app package.
Application security 5 8/10 Pydantic strict types and length limits validate input, SQLAlchemy Core parameterizes queries, and every query is scoped by owner, so other keys get 404. Each valid key is rate-limited to 60 requests per minute. However, body validation runs before the auth dependency, so unauthenticated requests get 400 instead of 401 and are never rate-limited. Keys are compared without constant-time comparison, and there is no per-key storage quota.

Findings

Cloud resources and cost

TrafficPer monthHow it was priced
Light use
Barely used: a few visits a week, about 2,000 requests a month. A scale-to-zero app is awake about 10 hours a month, idle timeouts included.
$12.00fixed monthly prices; 0.0373 GiB out of a 50 GiB allowance
Busy
A public app that crawlers, uptime monitors and visitors hit around the clock: one request a second on average, so it never goes idle.
$12.00fixed monthly prices; 49 GiB out of a 50 GiB allowance

Resources the agent created

KindNamePlanList price / monthPricing
appadb-5e3ks-apiadb-5e3ks-api-web: apps-s-1vcpu-0.5gb x 1, adb-5e3ks-api-db: dev database$12.00adb-5e3ks-api-web: apps-s-1vcpu-0.5gb x 1; adb-5e3ks-api-db: dev database
registryadb-5e3ks-apistarter$0.00Container Registry starter tier (one per account)

The agent estimated $12.00 a month.

Code

Code
449 lines in 16 files (Python 387, Shell 52, Markdown 48, YAML 29, JSON 11, Docker 10)
Dependencies
0 npm, 6 Python
Deploy config
Dockerfile .dockerignore .do/app.yaml

The agent's sessions

PhaseStatusTimeTurnsTokensCostShell commands
Buildcompleted2m 59s11449.7K$0.5210
Deploycompleted8m 55s11677.4K$0.4510

The agent's deploy summary

The Notes API is live at https://adb-5e3ks-api-omhxz.ondigitalocean.app, and every requirement in TASK.md passed when I tested it through that URL.

What's deployed (about $12/month):

  • Web service: App Platform app adb-5e3ks-api (ID 45f53cd7-e62a-42f5-8066-1b0679a77ab8), smallest size (apps-s-1vcpu-0.5gb, $5/month), one instance. HTTPS comes with App Platform's default address.
  • Database: PostgreSQL 16 as an App Platform dev database (adb-5e3ks-api-db, $7/month), attached to the app.
  • Image registry: adb-5e3ks-api, free starter tier.
  • Keys: API_KEYS is stored as an encrypted secret and set to exactly the keys in BENCH_API_KEYS.

What I checked through the public URL:

  • /health, /openapi.json (OpenAPI 3.1) and the HTML /docs page all return 200.
  • A missing or unknown key gets 401.
  • Creating, listing (including the tag filter), reading, updating and deleting notes all return the right status codes, and bad input gets 400.
  • The second key gets 404 on the first key's note.
  • Over the limit, the first key got 429 with Retry-After while the second key still got 200.
  • A note created before a redeploy was still there afterwards.

Problems along the way, all fixed:

  • The first deploy failed because the image tag in .do/app.yaml needed quotes.
  • /openapi.json didn't list itself in its own document, so I added it and redeployed.
  • For a few seconds right after a deploy, DigitalOcean's front end returned 521 errors twice. It worked normally after that.

Limitation: the rate limit is counted in the app's memory. It resets on each redeploy and only works correctly with one instance, so the instance count should stay at 1.

Nothing needed a person, so human_steps in deployment.json is empty. I didn't create any new credentials, so .env wasn't needed. To redeploy, run ./deploy.sh; to test, run ./smoke.sh <url>.

10 shell commands during deploy
  1. 4s cd /home/runner/work/_temp/adb-5e3ks-api; doctl account get --format Email,Status,DropletLimit 2>&1 | sed 's/[^ ]*@[^ ]*/<email>/'; doctl registry get 2>&1 | head -3; doctl apps list 2>&1 | head; doct
  2. 17s cd /home/runner/work/_temp/adb-5e3ks-api; ./deploy.sh 2>&1 | grep -vE '^#|Pushed|Waiting|Preparing|Layer already|^[a-f0-9]{12}:' | tail -25
  3. 49s cd /home/runner/work/_temp/adb-5e3ks-api; sed -i 's/tag: __TAG__/tag: "__TAG__"/' .do/app.yaml; ./deploy.sh 2>&1 | grep -E 'APP_ID|URL|Error|rror' | tail -10
  4. 6m 22s cd /home/runner/work/_temp/adb-5e3ks-api; U=https://adb-5e3ks-api-omhxz.ondigitalocean.app; ./smoke.sh $U; doctl apps logs 45f53cd7-e62a-42f5-8066-1b0679a77ab8 adb-5e3ks-api-web --type run 2>&1 | tail
  5. 6m 30s cd /home/runner/work/_temp/adb-5e3ks-api; doctl apps logs 45f53cd7-e62a-42f5-8066-1b0679a77ab8 adb-5e3ks-api-web --type run 2>&1 | grep -vE '"GET /health' | tail -30 | cut -c1-250
  6. 6m 35s cd /home/runner/work/_temp/adb-5e3ks-api; U=https://adb-5e3ks-api-omhxz.ondigitalocean.app; K="X-API-Key: ${BENCH_API_KEYS%%,*}"
  7. 6m 42s cd /home/runner/work/_temp/adb-5e3ks-api; U=https://adb-5e3ks-api-omhxz.ondigitalocean.app; ./smoke.sh $U
  8. 6m 57s cd /home/runner/work/_temp/adb-5e3ks-api; python3 - <<'EOF'
  9. 8m 12s cd /home/runner/work/_temp/adb-5e3ks-api; U=https://adb-5e3ks-api-omhxz.ondigitalocean.app; A="X-API-Key: ${BENCH_API_KEYS%%,*}"
  10. 8m 48s cd /home/runner/work/_temp/adb-5e3ks-api; cat > deployment.json <<'EOF'

Raw data