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 & compliance87/100
Build2m 43s
Deploy8m 12s
Tokens1.11M
Agent cost$1.00
Hosting / month$12.00 light use$12.00 busy

Deployed to https://adb-5ye5p-api-bo8ea.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-5ye5p-api-bo8ea.ondigitalocean.app/docs

Technologies

Runtime
Node.js 22
Frameworks
Express
Data
PostgreSQL (App Platform dev database)
Libraries
node-postgres
Packaging
Docker (node:22-alpine)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

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 deployment is well built: secrets go through App Platform's encrypted SECRET env vars and never reach source or logs, the container runs as non-root, the database uses TLS with CA verification, all SQL is parameterized with owner scoping, and the live site exposes nothing beyond the required endpoints. The weaknesses are minor: the base image and deployed tag aren't pinned by digest, doctl was installed without a checksum, no security headers are set, and the dev database can't be network-restricted.

CriterionWeightScoreWhy
Secrets handling 25 10/10 API_KEYS is injected as an App Platform SECRET (encrypted EV[...] in the live spec) via a umask-077 temp copy of the spec that was deleted afterwards, DB credentials come from bindable variables, and no key appears in source, the image build context or the transcripts.
Deployment provenance 20 7/10 The image was built from the reviewed source with npm ci against a lockfile and pushed to the owner's own DOCR registry, but the base image (node:22-alpine) isn't pinned by digest, the deployed tag (v1) is mutable, and doctl was downloaded with no checksum check.
Terms compliance 15 9/10 This is a personal app on a paid App Platform Basic instance with an App Platform dev database. That fits DigitalOcean's terms for this use, and the service is always on, not a sleeping or expiring free tier. The dev database has no backups, which the agent disclosed. (The account plan wasn't readable from context.json; judged from DigitalOcean's standard pay-as-you-go terms.)
Least privilege 15 8/10 The container runs as the non-root node user, there's only one public component, and the database is reached over TLS with CA verification. The App Platform dev database can't be restricted with trusted sources, though, so its endpoint can be reached from the internet with credentials.
Transport security 10 8/10 HTTP redirects to HTTPS with a 301, and the Google Trust Services certificate is valid until Dec 2026. The app sends no HSTS, X-Content-Type-Options, frame-protection or CSP headers, even though it could have set them in Express.
Attack surface 10 10/10 Probes for /.env, /.git/config, /package.json, /src/app.js, /Dockerfile, /admin and /debug all returned 404. Errors come back as generic JSON with no stack traces, and the only unauthenticated endpoints are the ones the spec requires (/health, /docs, /openapi.json).
Application security 5 9/10 All SQL is parameterized, every query on a note is scoped to the owner (other keys get 404), input types are validated, and rate limiting is per key and stored in Postgres. Minor gaps: there are no length or count limits on note fields beyond the 1 MB body cap, and keys are compared without constant-time comparison.

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-5ye5p-apiadb-5ye5p-api-web: apps-s-1vcpu-0.5gb x 1, adb-5ye5p-api-db: dev database$12.00adb-5ye5p-api-web: apps-s-1vcpu-0.5gb x 1; adb-5ye5p-api-db: dev database
registryadb-5ye5p-apistarter$0.00Container Registry starter tier (one per account)

The agent estimated $12.00 a month.

Code

Code
533 lines in 12 files (JavaScript 523, YAML 33, Markdown 32, JSON 31, Docker 10)
Dependencies
2 npm, 0 Python
Deploy config
Dockerfile .dockerignore .do/app.yaml

The agent's sessions

PhaseStatusTimeTurnsTokensCostShell commands
Buildcompleted2m 43s16558.9K$0.5710
Deploycompleted8m 12s10555.3K$0.439

The agent's deploy summary

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

What's deployed (about $12/month, all named adb-5ye5p-api…):

  • Web service: App Platform app adb-5ye5p-api (id 4518b2b9-…), the smallest Basic size, $5/month. It runs the Docker image from step 1 and gets HTTPS from the default ondigitalocean.app domain.
  • Database: an App Platform dev PostgreSQL 16 database, adb-5ye5p-api-db, $7/month. This was the cheapest option that keeps data across restarts and redeploys.
  • Image registry: a DigitalOcean Container Registry, adb-5ye5p-api, on the free Starter plan.
  • API keys: API_KEYS is set as an encrypted secret holding exactly the 2 keys in BENCH_API_KEYS. The keys don't appear in any file in the project.

What I tested on the live URL:

  • /health, /openapi.json and the /docs HTML page all return 200.
  • A missing or wrong key gets 401.
  • Creating, listing (including the tag filter), reading, updating and deleting notes all work, and bad input gets 400.
  • One key can't see another key's notes: it gets 404.
  • Rate limit: in one minute, one key got 59 successes, then 429 with a Retry-After header. The other key kept working, and the limited key worked again once the minute was up. The first request had already used one of the 60, so 59 is the full allowance.
  • Data survives a redeploy: a note I created was still there after a full redeploy.
  • I deleted all my test notes afterwards.

Things to know:

  • The dev database has no backups and no failover. A managed PostgreSQL cluster (from $15/month) would add those if the app needs them later.
  • The rate limit uses fixed one-minute windows, and requests that get blocked still count toward the current minute.
  • Changes to the project: I changed .do/app.yaml to deploy the image from the registry, and updated the README's deploy section to match.
  • No .env needed: I didn't create any new credentials, so I didn't write a .env file.
  • Nothing needs a person: human_steps in deployment.json is empty.
9 shell commands during deploy
  1. 7s which doctl || (cd /tmp && curl -sL https://github.com/digitalocean/doctl/releases/download/v1.146.0/doctl-1.146.0-linux-amd64.tar.gz | tar xz && sudo mv doctl /usr/local/bin/); doctl version; doctl a
  2. 14s doctl registry create adb-5ye5p-api --subscription-tier starter --region nyc3 && doctl registry login >/dev/null && docker build -q -t registry.digitalocean.com/adb-5ye5p-api/adb-5ye5p-api-web:v1 . &&
  3. 59s cat > .do/app.yaml <<'EOF'
  4. 1m 03s sed -i 's/ registry_type: DOCR/ registry_type: DOCR\n registry: adb-5ye5p-api/' .do/app.yaml && umask 077 && K="$BENCH_API_KEYS" node -e 'const fs=require("fs");fs.writeFileSync("/tmp/a
  5. 1m 09s until s=$(doctl apps get 4518b2b9-bf7b-4cfd-8a07-3f89e2f6364d --format DefaultIngress,ActiveDeployment.ID,InProgressDeployment.Phase --no-header 2>/dev/null); echo "$s" | grep -q https || doctl apps l
  6. 6m 36s B=https://adb-5ye5p-api-bo8ea.ondigitalocean.app; K1=$(echo "$BENCH_API_KEYS"|cut -d, -f1); K2=$(echo "$BENCH_API_KEYS"|cut -d, -f2); echo "nkeys=$(echo "$BENCH_API_KEYS"|tr , '\n'|wc -l)"
  7. 6m 46s B=https://adb-5ye5p-api-bo8ea.ondigitalocean.app; K1=$(echo "$BENCH_API_KEYS"|cut -d, -f1); K2=$(echo "$BENCH_API_KEYS"|cut -d, -f2)
  8. 7m 30s sleep 20; B=https://adb-5ye5p-api-bo8ea.ondigitalocean.app; K2=$(echo "$BENCH_API_KEYS"|cut -d, -f2); curl -s -w " %{http_code}\n" $B/v1/notes -H "X-API-Key: $K2"; doctl apps get 4518b2b9-bf7b-4cfd-8a
  9. 8m 03s rm -f /tmp/adb-spec.yaml && cat > deployment.json <<'EOF'

Raw data