apps/api/ is the SlopCamera tool surface for agent platforms that cannot run a local CLI — Muse connectors, Grok-style bots, Instinct-class clients, and direct HTTP integrations. It serves the checked MCP tool registry over REST and a stateless MCP endpoint, executes each call inside a per-request ephemeral workspace, and returns binary outputs as ticketed artifact URLs backed by Cloudflare R2.
The canonical tool semantics live in src/mcp/tools.ts. This app owns transport, admission, billing, and storage only; it adds no operation semantics of its own.
| Route | Purpose |
|---|---|
GET /v1/health |
Configured capabilities (storage, billing, models). |
GET /v1/models |
Sorted paid-model allowlist (provider/model ids). Provider costs never leave the service. |
GET /v1/tools |
Hosted tool registry with tiers and input schemas. |
POST /v1/tools/{name}/call |
Invoke one tool. Body: {arguments, files?, idempotencyKey?}. |
POST /v1/uploads |
Mint a 15-minute presigned R2 PUT for one inbound file. |
GET /v1/artifacts/{id} |
Artifact ticket metadata. |
GET /v1/artifacts/{id}/content |
302 to a one-hour presigned download. |
POST /v1/mcp |
Stateless JSON-RPC MCP endpoint (protocol 2025-11-25). |
GET /v1/openapi.json |
The OpenAPI contract connector submissions reference. |
- free – anonymous, rate-limited validation, inspection, planning, and audit tools.
- render – anonymous with a tighter limit;
render_diagramis the only member. - paid –
execute_slopcameranarrowed toslopcamera.image.generate. Requires a Hraness Credits device token (Authorization: Bearer cr_dev_…). The service holds credits up to a ceiling priced from the model's worst case (largest size, highest quality, and every prompt token) before any provider call, then settles with the provider cost priced from the usage the provider returns. When work fails after the provider call, it releases the hold and reports that cost. Margin lives inside the Credits pricing revision; this service never sees or quotes it.
Callers without a token get 401 with a signup block describing the Credits claim flow. Insufficient balance returns 402 with a topup.url the caller's human can pay.
Inputs travel as a files map on the call: { "name": { "text": … } | { "base64": … } | { "upload": "<id>" } }. Names are root-relative paths; traversal, absolute paths, and control bytes are rejected, and each call admits at most 24 files. Inline payloads are capped at 4 MiB by default; larger inputs go through POST /v1/uploads and a presigned PUT.
Outputs are harvested by diffing the workspace after the call. Each new file becomes an R2 object at a/<id>/<name> plus a t/<id>.json ticket record, both expiring under the bucket lifecycle rule — currently seven days. Tickets carry sha256, byte count, content type, and a contentUrl that redirects to a presigned GET. Artifact URLs are unauthenticated but unguessable; treat them as bearer references.
| Variable | Purpose |
|---|---|
SLOPCAMERA_API_BASE_URL |
Public base URL used in ticket and OpenAPI URLs. |
R2_PROXY_URL, R2_PROXY_SECRET |
Signed object-proxy worker (apps/objects/) and its shared HMAC secret — the preferred storage path. Without a working storage group, upload and artifact routes answer 503. |
R2_ACCOUNT_ID, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY |
Direct R2 S3 credentials, used only when no proxy pair is configured. |
R2_BUCKET |
Bucket name for the S3 path; default slopcamera-api-artifacts. |
R2_ENDPOINT |
Optional custom S3 endpoint for the bucket. |
SLOPCAMERA_API_CREDITS_PRODUCT_KEY |
Credits product key (cr_prod_…). Without it, paid tools answer 503. |
CREDITS_BASE_URL |
Credits origin; default https://credits.hraness.com. |
SLOPCAMERA_API_PAID_MODELS |
Comma-separated provider/model ids admitted for paid generation. Only models with a cited list price in src/generation-pricing.ts are admitted; others are dropped. The keys of the older SLOPCAMERA_API_MODEL_COSTS_JSON map are still read as admitted ids, and its cost values are ignored. |
AI_GATEWAY_API_KEY |
Vercel AI Gateway credential used by slopcamera.image.generate. |
SLOPCAMERA_API_ARTIFACT_TTL_DAYS |
Reported artifact lifetime (1–30, default 7); the bucket lifecycle enforces it. |
SLOPCAMERA_API_FREE_CALLS_PER_HOUR |
Anonymous free-tool limit (default 120). |
SLOPCAMERA_API_RENDER_CALLS_PER_HOUR |
Anonymous render limit (default 30). |
SLOPCAMERA_API_UPLOADS_PER_HOUR |
Upload-ticket limit (default 60). |
SLOPCAMERA_API_MAX_INLINE_BYTES |
Inline file ceiling (default 4 MiB). |
SLOPCAMERA_API_MAX_UPLOAD_BYTES |
Presigned-upload ceiling (default 32 MiB). |
PORT, HOST |
Listen address; default 0.0.0.0:8787. |
Rate limiting is in-memory per process. Paid calls are bounded by Credits holds, not the limiter.
The SlopCamera CLI can drive the paid operation directly: slopcamera credits topup prints a Credits checkout URL, slopcamera credits wait polls the claim and stores the returned cr_dev_… device token under the CLI state root in owner-only files, and slopcamera ai image generate --hosted calls execute_slopcamera with {model, prompt, outputPath} and writes the verified artifact below artifacts/slopcamera/generated/. When no Gateway credential is configured, a stored token routes ai image generate here automatically; SLOPCAMERA_CREDITS_TOKEN overrides the stored token for ephemeral environments.
Register the product once against the Credits admin CLI with a cost-plus operation named image_generate, then set SLOPCAMERA_API_CREDITS_PRODUCT_KEY to the issued cr_prod_… key. Set SLOPCAMERA_API_PAID_MODELS to the models to offer; prices come from the cited table in src/generation-pricing.ts, which a reviewed change updates when a provider changes its list price. Callers top up through the topup.url returned in 402 responses; the service never touches payment pages.
One bucket with two lifecycle rules: a/ and t/ expire after seven days, u/ expires after one day.
The preferred storage path needs no Cloudflare-issued S3 credential: apps/objects/ deploys as the slopcamera-objects worker with an R2 binding to the bucket. The API mints HMAC-signed bearer URLs ({method, key, expiry, maxBytes}); the worker verifies the signature, enforces the declared cap and the a//t//u/ prefix allowlist, and performs the bucket op. Provision it with:
cd apps/objects
bunx wrangler deploy
bunx wrangler secret put OBJECT_PROXY_SECRET # shared with the APISet R2_PROXY_URL to the worker's workers.dev URL and R2_PROXY_SECRET to the same secret on the API project. As a fallback, direct S3 credentials (R2_ACCOUNT_ID, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY) scoped to object read/write/head on that bucket also work. Presigned PUTs are capped by the declared byte count and expire in 15 minutes either way.
bun install
bun run --cwd apps/api devRun the focused gate before handoff:
bun run check:api- No accounts, OAuth, sessions, or stored credentials. The only credential is the caller's Credits device token, used once per hold and never persisted or logged.
execute_slopcameraadmits onlyslopcamera.image.generate; no caller-selected operations, source code, or subprocess execution reaches the runtime.- Every call runs in a fresh temporary directory that is deleted afterward. Nothing crosses requests except TTL'd R2 objects.
- Request bodies are capped at 16 MiB; file names, counts, and bytes are bounded before any provider I/O.
- Paid-call retries are safe: the hold is idempotent on the caller's
idempotencyKeywhile that call is in progress, and a failed tool run releases the hold before responding. Once a call finishes, its key is spent; reusing it returns409 idempotency_key_reusedwithout running the tool, so send a new key for each new call.