# Speccy x402 — FFMPEG-as-a-Service > Paid FFMPEG transformation endpoint for AI agents on Base mainnet (eip155:8453). x402 protocol, USDC per call. Receiver: `0x0Cb12Df486381A0EBeBc0EF338dA80657f68f9cB`. > > **Base URL:** `https://tools.speccy.cloud` > **Help:** `GET /v1/ffmpeg/help?q=` — recipe lookup in natural language (free). > **Status:** ✅ live --- ## Quickstart 1. POST without payment → 402 with `PAYMENT-REQUIRED` (base64 JSON) 2. Sign EIP-3009 `transferWithAuthorization` for the required USDC amount 3. Retry with `X-Payment` header (base64 of signed payload) 4. Get 200 with the transformed file See: --- ## Endpoints (three tiers) | Endpoint | Price | Use case | Timeout | |---|---|---|---| | `POST /v1/ffmpeg/copy` | **$0.005** USDC | Stream copy only (`-c copy`). Remux, trim, extract. No filters. | 30s | | `POST /v1/ffmpeg/transform` | **$0.05** USDC | Standard transforms. Transcode, simple filters, resize <1080p. | 60s | | `POST /v1/ffmpeg/heavy` | **$0.20** USDC | Heavy / long-running. 4K, multi-filter chains, VP9/WebM, audio normalization. | 90s | **Concurrency cap:** max 2 ffmpeg processes running at once. Beyond that, requests queue for up to 30s, then return 503 + `Retry-After`. All three endpoints share the same request/response shape: ```json { "input": { "url": "https://example.com/video.mp4" }, "args": ["-vn", "-acodec", "libmp3lame", "-q:a", "2"], "outputFormat": "mp3" } ``` ```json { "success": true, "tier": "transform", "result": { "mimeType": "audio/mpeg", "size": 1234567, "durationMs": 1234, "queueWaitMs": 12, "outputBase64": "..." } } ``` **Limits (all tiers):** 50MB input, 50MB output, workdir cleaned after call. **Allowed args:** ffmpeg flags only. No absolute paths. No `..`. Output is determined by `outputFormat` (regex `^[a-zA-Z0-9]{2,5}$`). ### `GET /v1/ffmpeg/help` — free recipe lookup Query params: - `q` (optional): natural-language question, e.g. `?q=how+to+extract+mp3+from+video` - no `q`: full recipe catalog Response: ```json { "match": "mp3-extract", "score": 13, "description": "Extract audio as MP3 from a video file.", "args": ["-vn", "-acodec", "libmp3lame", "-q:a", "2"], "outputFormat": "mp3", "tier": "transform", "requestBody": { "input": {...}, "args": [...], "outputFormat": "mp3" }, "alternates": [{ "id": "aac-extract", "score": 6, ... }] } ``` ### `GET /v1/ffmpeg/concurrency` — free slot status Returns `{ active, queued, acquired, max, queueWaitMs }` so agents can see the load. ### `GET /v1/sample` — paid route example (free) ### `GET /health` — uptime probe (free, always 200) ### `GET /llms.txt` — this file (free) --- ## FFmpeg cheatsheet for agents ### Mental model - **Inputs:** each `-i ` adds an input stream. First `-i` is index 0. - **Outputs:** ffmpeg writes whatever you don't constrain. We always specify one via `outputFormat`. - **Filters** are chains: `-vf "scale=640:-1,transpose=1"` applies scale, then transpose. - **Stream copy** (`-c copy`) re-muxes without re-encoding. Near-instant, near-zero CPU. Use when quality doesn't matter and codec is already what you want. - **`-y`** overwrites output without asking. Implicit in our wrapper. - **`-ss` before `-i`** = input seek (fast, keyframe-accurate-ish). **`-ss` after `-i`** = output seek (accurate but slower). - **`-t`** sets duration. Useful for "first N seconds" extraction. - **`-vn`** drops video. **`-an`** drops audio. **`-sn`** drops subtitles. ### Args you can rely on | Flag | Effect | |---|---| | `-c copy` | Stream copy — no re-encode. Fast. | | `-c:v libx264` / `-c:v libx265` | H.264 / H.265 video codec. | | `-c:a aac` / `-c:a libmp3lame` / `-c:a libopus` | Audio codecs. | | `-crf 18` (libx264) | Quality, 0–51. Lower = better, larger. 18–23 typical. | | `-b:a 192k` | Audio bitrate. | | `-q:a 2` | VBR audio quality (libmp3lame: 0–9, lower = better). | | `-vf "..."` | Video filter chain. | | `-af "..."` | Audio filter chain. | | `-r 30` | Frame rate. | | `-ss 00:00:10` | Seek to 10s. | | `-t 30` | Take 30 seconds. | | `-vframes 1` | Take only first video frame → thumbnail. | | `-f image2` / `-f mp4` / `-f mp3` | Force output format (we set this from `outputFormat`). | | `-movflags +faststart` | MP4 with metadata at front (web-friendly). | ### When you should think - **Scale up** (`scale=2000:2000` on a 800px image): ffmpeg will upscale. Quality loss is real. Prefer lanczos filter: `scale=2000:2000:flags=lanczos`. - **Two-pass encoding**: not supported here (no persistent state). Use `-crf` single-pass. - **Hardware encoders** (`-c:v h264_nvenc`): not exposed. CPU only. - **Custom filter graphs with `sendcmd`/`lavfi`**: not exposed for security. Use named filters. - **Subtitles burn-in**: requires `subtitles=` filter, font config. Brittle. Avoid unless necessary. --- ## Recipes (copy-paste ready) Each recipe has a stable `id` you can pass to `/v1/ffmpeg/help?q=` for exact lookup. ### Audio extraction - **mp3-extract** — Extract audio as MP3 from any video `args: ["-vn", "-acodec", "libmp3lame", "-q:a", "2"]`, `outputFormat: "mp3"` - **aac-extract** — Extract audio as AAC (smaller, slightly better quality) `args: ["-vn", "-c:a", "aac", "-b:a", "192k"]`, `outputFormat: "m4a"` - **opus-extract** — Extract audio as Opus (best for voice) `args: ["-vn", "-c:a", "libopus", "-b:a", "96k"]`, `outputFormat: "ogg"` ### Audio conversion - **wav-to-mp3** — WAV → MP3 `args: ["-acodec", "libmp3lame", "-q:a", "2"]`, `outputFormat: "mp3"` - **mp3-to-wav** — MP3 → WAV (uncompressed for editing) `args: ["-acodec", "pcm_s16le"]`, `outputFormat: "wav"` - **audio-normalize** — Loudness-normalize audio (EBU R128) `args: ["-af", "loudnorm=I=-16:TP=-1.5:LRA=11"]`, `outputFormat: "mp3"` - **audio-trim** — Cut first/last seconds off audio `args: ["-ss", "2", "-t", "60"]`, `outputFormat: "mp3"` (extract from 2s, 60s long) ### Image ops - **resize** — Resize keeping aspect ratio `args: ["-vf", "scale=800:-1"]`, `outputFormat: "webp"` - **resize-jpg** — Resize to JPG `args: ["-vf", "scale=800:-1"]`, `outputFormat: "jpg"` - **crop-center** — Center-crop to square (e.g. 800x800) `args: ["-vf", "crop=min(iw\\,ih):min(iw\\,ih)"]`, `outputFormat: "jpg"` - **thumbnail-stamp** — Make a thumbnail at specific timestamp `args: ["-ss", "00:00:05", "-vframes", "1", "-vf", "scale=640:-1"]`, `outputFormat: "jpg"` ### Video ops - **mp4-compress** — Re-encode MP4 to smaller file (CRF 23, AAC audio) `args: ["-c:v", "libx264", "-crf", "23", "-preset", "medium", "-c:a", "aac", "-b:a", "128k", "-movflags", "+faststart"]`, `outputFormat: "mp4"` - **video-to-gif** — Animated GIF preview (≤10s, scaled to 480px) `args: ["-vf", "fps=10,scale=480:-1:flags=lanczos", "-t", "10"]`, `outputFormat: "gif"` - **trim-clip** — Take a 30s slice starting at 1 minute `args: ["-ss", "00:01:00", "-t", "30", "-c", "copy"]`, `outputFormat: "mp4"` - **webm-encode** — Encode as WebM (VP9, Opus) `args: ["-c:v", "libvpx-vp9", "-crf", "30", "-b:v", "0", "-c:a", "libopus"]`, `outputFormat: "webm"` ### Concatenation / extraction - **first-frame** — Extract the very first frame as image `args: ["-vframes", "1"]`, `outputFormat: "jpg"` - **last-frame** — Extract last frame `args: ["-sseof", "-1", "-vframes", "1"]`, `outputFormat: "jpg"` - **stream-copy-remux** — Change container without re-encoding (e.g. MKV → MP4) `args: ["-c", "copy"]`, `outputFormat: "mp4"` ### Bulk from base64 input When input is base64, send it as `"input": {"base64": "..."}`. Server decodes to file, processes, returns result base64. Same args work — input is just a different source. --- ## Limits & when to escalate - **50MB hard cap** on input and output (all tiers). For larger: split the file first (we can do this with `trim-clip` chained) or contact for a custom endpoint. - **Timeout per tier:** copy 30s, transform 60s, heavy 90s. Filter chains that take longer will be killed. - **No GPU**. Everything is CPU. 4K transcode ≈ 30–90s. Don't queue many in parallel. - **Concurrency cap: max 2 simultaneous ffmpeg instances.** Others queue for up to 30s, then 503 with `Retry-After: 30`. - **Wallet-based rate limits: not enforced yet.** The concurrency cap is the current throttle. For work outside these limits, ask before sending: spec a custom endpoint, or split the work into smaller chunks. --- ## Safety: what you can't do These will be rejected before ffmpeg starts: - Absolute paths in `args`: `/etc/passwd`, `C:\...`, anything starting with `/` or containing `:`. - `..` in args (path traversal). - `outputFormat` not matching `^[a-zA-Z0-9]{1,8}$` (no `.mp4`, no `.exe`, no shell metas). - Input larger than 50MB (decoded from base64 or fetched via URL). - Args that don't look like ffmpeg flags (we sanitize with a denylist of shell metas: `;&|$<>\`!`). --- ## Pointers (deeper docs) - **Official FFmpeg documentation:** - **Filter reference:** - **Codec reference:** - **Format reference:** - **Bazaar discovery:** the endpoint auto-indexes. Findable via CDP facilitator search. --- ## How to pay (recap) 1. POST without `X-Payment` header → 402 with `PAYMENT-REQUIRED` (base64-encoded) 2. Decode the header: it contains the price (`amount`), asset (`USDC`), network (`eip155:8453`), payTo address 3. Sign EIP-3009 `transferWithAuthorization` with your payer wallet 4. Base64-encode the signed payload → `X-Payment` header 5. Retry the POST → server verifies, settles on-chain, returns your file Reference: