# cputools — Full reference _Compiled 2026-06-18. Source pages tagged for: cputools._ ## /docs/cputools-overview # cputools The CPU work your agent shouldn't host itself. One call, fractions of a cent, no server to babysit. ```bash curl -X POST https://api.relaystation.ai/v1/pdf/merge \ -H 'X-Payment: ' \ -H 'Idempotency-Key: merge-invoices-20260605' \ -H 'Content-Type: application/json' \ -d '{"files":[{"inline":""},{"inline":""}]}' ``` ## What it is cputools is a pay-per-call utility API by Relaystation. The suite is **86 operations across nine categories** — plus a **pipeline runtime** that composes them — behind one endpoint: PDF (19 ops, incl. encrypt/decrypt/compress/render/OCR, **HTML→PDF generation**, **Office→PDF conversion** (docx/xlsx/pptx → PDF), and extract-images/diff/bookmarks/attachments), CSV (convert/dedupe/select), image (13 ops: resize/convert/compress/rotate/metadata/crop/blur/sharpen/grayscale/exif-strip/dominant-color/composite/contact-sheet), codes (qr/barcode generate + qr-decode + color-convert), utils (hash/hmac/base64/uuid/jwt-decode/jwt-verify), generate (og-image/mock-data/invoice/chart/qr-logo/favicon/placeholder/identicon), archive (zip/unzip/gzip), **data** — the ETL transform layer: filter, sort, group-by, join, union, profile, cast, pivot, schema-infer, validate, diff, derive, fillna, dropna, rename, slice, explode, sample, **Excel in/out** (from-xlsx/to-xlsx), and **sql** (full SQL over your file on a sandboxed DuckDB) — and **text** (case/slugify/diff/count/regex-extract/template/markdown-to-html/html-to-text/sanitize-html). Your agent sends a file and an x402 payment, gets back a result, and never created an account. Pay per call with x402 or top up with a card via Stripe — one Relaystation balance funds cputools and every other Relaystation product. No subscription, no minimum. These are the tools you could self-host — they're built on open-source [pdf-lib](https://pdf-lib.js.org/), [pdf.js](https://mozilla.github.io/pdf.js/), [qpdf](https://qpdf.sourceforge.io/), [poppler](https://poppler.freedesktop.org/), [Tesseract](https://tesseract-ocr.github.io/), [Papaparse](https://www.papaparse.com/), [sharp](https://sharp.pixelplumbing.com/), [jose](https://github.com/panva/jose), [satori](https://github.com/vercel/satori), [faker](https://fakerjs.dev/), [fflate](https://github.com/101arrowz/fflate), [DuckDB](https://duckdb.org/), and [LibreOffice](https://www.libreoffice.org/) (via [Gotenberg](https://gotenberg.dev/)), plus Node's built-in `crypto`. We just run them so you don't have to: no Lambda layer to wrangle, no native binary to compile into your image, no cold-start tax on your own infrastructure. One HTTPS call, a result back, done. cputools is now a **transform platform**, not just a catalog: the data category and the pipeline runtime compose into ETL chains in a single call. Document rendering is live at both ends — HTML→PDF (Chromium) and Office→PDF (LibreOffice) — and media tenants are the next frontier. ## The lodestone The whole product is one HTTPS call: 1. Your agent `POST`s to `https://api.relaystation.ai/v1//` with the tool's payload. 2. The payload carries the file inline (base64, ≤ 4 MB) — or a storage reference (`{"inputKey":"..."}`) for larger files. 3. An `X-Payment` header carries the x402 (EIP-3009) authorization; an `Idempotency-Key` header makes the call safe to retry. 4. cputools runs the transform and returns a JSON envelope: the result inline (base64) when small, or `{ outputKey, outputUrl }` (a presigned GET) when large. No account, no signup step, no deposit-then-withdraw. The wallet that signs the payment is the identity, so your transaction history is there if you come back. Or use a `Bearer rs_live_*` API key if you'd rather fund from a prepaid balance. An `outputKey` chains: pass it as the next op's `inputKey` and run multi-step transforms without re-uploading — the full I/O model (inline / scratch / baton) is the canonical [Passing & receiving files](/docs/receiving-outputs) page. ## The tools, one endpoint | Category | Ops | Billing | Reference | |---|---|---|---| | **PDF** | merge, split, rotate, pages, watermark, form, extract-text, metadata, encrypt, decrypt, compress, render, OCR, from-html, from-office, images, diff, bookmarks, attachments (19) | per page (from-html/from-office + metadata/bookmarks/attachments flat) | [PDF tools →](/docs/pdf-tools) | | **CSV** | convert, dedupe, select (3) | per MB | [CSV tools →](/docs/csv-tools) | | **Image** | resize, convert, compress, rotate, metadata, crop, blur, sharpen, grayscale, exif-strip, dominant-color, composite, contact-sheet (13) | per megapixel (metadata/dominant-color flat) | [Image tools →](/docs/image-tools) | | **Codes** | QR, barcode, qr-decode, color-convert (4) | flat | [QR & barcode →](/docs/qr-tools) | | **Utils** | hash, hmac, base64, uuid, jwt-decode, jwt-verify (6) | per MB (uuid/jwt flat) | [Utils tools →](/docs/utils-tools) | | **Generate** | og-image, mock-data, invoice, chart, qr-logo, favicon, placeholder, identicon (8) | flat | [Generate tools →](/docs/generate-tools) | | **Archive** | zip, unzip, gzip (3) | per MB (input) | [Archive tools →](/docs/archive-tools) | | **Data** | filter, sort, groupby, join, union, profile, cast, pivot, schema-infer, validate, diff, derive, fillna, dropna, rename, slice, explode, **Excel** from-xlsx/to-xlsx (19); **sql** (sandboxed DuckDB) | per MB (sql tier higher) | [Data tools →](/docs/data-tools) · [SQL →](/docs/data-sql) | | **Pipeline** | run — compose a chain of transforms in one call | sum of the steps | [Pipeline →](/docs/pipeline) | Send a file inline (base64, ≤ 4 MB) or by storage reference for larger files (≤ 50 MB); get the result back the same way. A small per-MB egress surcharge applies only to storage-ref outputs (the >4 MB path). The heavy ops (encrypt/decrypt/compress/render/OCR, data **sql**, **HTML→PDF**, and **Office→PDF**) run vendored qpdf, poppler, Tesseract, DuckDB, headless Chromium, and LibreOffice on dedicated engines — same request shape, same per-call billing. ## Pricing From $0.0002 per call. No subscription, no minimum, no commitment. Pay per call with x402, or top up with a card via Stripe — one Relaystation balance funds cputools and every other Relaystation product. [See full pricing →](/pricing) ## Built on a substrate The whole suite rides one chassis: payment, idempotency, the inline-vs-storage-ref I/O convention, per-page/per-MB/per-megapixel metering, and the compute-worker pattern were paid for once. Adding a new tool on top is small and bounded — a new operation in an existing category is a few dozen lines of glue; a whole new category is a single in-process module plus its pricing rows. The utils, generate, and archive categories (eleven ops) landed as ~530 and ~440 lines of route code with the shared substrate left byte-identical; the data category added zero dependencies; the `data/sql` engine was DuckDB dropped into the same worker tier as a single vendored binary (no Docker), and the pipeline runtime reused the existing transforms verbatim. That's why the catalog went from 8 PDF ops to 86 across nine categories — plus a composable runtime, HTML→PDF generation, and Office→PDF conversion (LibreOffice via Gotenberg, the first HTTP-upstream engine) — without a rewrite, and why the next tier — media — is an engine-drop away rather than a re-architecture. ## Why pay-per-call wins here Every PDF-API incumbent except one gates behind a monthly minimum or a credit catalog you have to pre-commit to. cputools is the one an agent can call once, pay a fraction of a cent, and leave — and we undercut even the closest pay-per-call peer by roughly 10×. Compute for in-process PDF ops is nearly free, so the price is a positioning number, not a cost-recovery exercise: dramatically under the incumbents, but still real money, never a subsidized loss-leader. The only real variable cost is egress on large outputs, which is why a small per-MB surcharge rides the storage-ref path and nothing else. ## Next [PDF tools](/docs/pdf-tools) · [Office tools](/docs/office) · [CSV tools](/docs/csv-tools) · [Image tools](/docs/image-tools) · [QR & barcode](/docs/qr-tools) · [Utils tools](/docs/utils-tools) · [Generate tools](/docs/generate-tools) · [Archive tools](/docs/archive-tools) · [Data tools](/docs/data-tools) · [Data — SQL](/docs/data-sql) · [Pipeline](/docs/pipeline) · [MCP tools](/docs/cputools-mcp-tools) · [Pricing](/pricing) · [API reference](/api-reference) · [x402 wire format](https://relaystation.ai/docs/x402) · [Authentication](https://relaystation.ai/docs/authentication) ## /docs/pdf-tools # PDF tools Twenty-one PDF operations, one endpoint, one uniform request/response shape. Every op is a `POST https://api.relaystation.ai/v1/pdf/`. The pure-PDF ops (merge … metadata, images, diff, bookmarks, attachments) run in-process on pdf-lib / pdf.js; the heavier ops run on dedicated engines — encrypt, compress, repair, render, OCR, ocr-searchable, and verify-signatures on vendored **qpdf**, **poppler** (including `pdfsig`), and **Tesseract**; **from-html (HTML→PDF) on a vendored headless Chromium**; and **from-office (Office→PDF) on LibreOffice (Gotenberg)** — same request shape, same per-call billing. `from-html` **generates** a PDF from scratch (an invoice, a report, a certificate); `from-office` **converts** an existing office document (docx, xlsx, pptx, …) into one. ## Inputs and outputs — the uniform shape Every op takes its file (or files) as an **input source** and returns a uniform **output envelope**. You never stream a raw file; the body is always JSON. **Input source** — one of two shapes per file field: ```json { "inline": "" } // for files ≤ 4 MB { "inputKey": "" } // for larger files ``` For files above the 4 MB inline ceiling, mint a one-time presigned upload first: ```bash curl -X POST https://api.relaystation.ai/v1/cputools/upload-url \ -H 'Authorization: Bearer rs_live_' \ -H 'Content-Type: application/json' \ -d '{"ext":"pdf","contentType":"application/pdf"}' # → { "url": "...", "fields": { ... }, "inputKey": "...", "expiresAt": "...", "maxBytes": 52428800 } ``` Then upload with a **multipart/form-data `POST` to `url`** — include every entry from `fields` as a form field, then a `file` field carrying the bytes (the standard S3 presigned-POST form). Finally pass `{"inputKey":"..."}` as the file to the op route. The presigned POST is customer-scoped, short-lived, and size-capped at `maxBytes` (S3 rejects an oversized body at upload time). An x402 lodestone wallet can mint one too — minting is free; only the op that consumes the bytes bills. **Output envelope** — always JSON, under an `output` key: ```json { "output": { "inline": "", // present when the result ≤ 4 MB "outputKey": "...", // present when the result > 4 MB "outputUrl": "https://...", // presigned GET for the scratch key "sizeBytes": 12345, "contentType": "application/pdf", "filename": "merged.pdf" } } ``` The inline-vs-reference threshold is operator-tunable (`cputools.io.max_inline_bytes`, default 4 MB). Results at or under it come back inline; larger results are written to a scratch key and returned as a presigned `outputUrl`. The full inline / scratch / baton model — and recipes for downloading or chaining outputs — is in [Passing & receiving files](/docs/receiving-outputs). ## Billing Per-page ops bill the page count × the per-page rate, **minimum one page**. `metadata` is a flat per-call charge. The per-op rates are operator-tunable (`cputools.price.pdf..per_page_micros`); the launch defaults: | Op | Billed on | Rate | |---|---|---| | `merge` | output pages | $0.001 / page | | `split` | source pages | $0.001 / page | | `rotate` | result pages | $0.001 / page | | `pages` | result pages | $0.001 / page | | `watermark` | pages stamped | $0.001 / page | | `form` | pages | $0.001 / page | | `extract-text` | source pages | $0.002 / page | | `metadata` | per call | $0.001 flat | | `encrypt` | pages | $0.001 / page | | `compress` | pages | $0.001 / page | | `repair` | pages | $0.001 / page | | `render` | output pages | $0.001 / page | | `ocr` | source pages | $0.003 / page | | `ocr-searchable` | source pages | $0.004 / page | | `verify-signatures` | per call | $0.001 flat | | `from-html` | per call | $0.003 flat | | `from-office` | per call | $0.005 flat | | `images` | pages scanned | $0.001 / page | | `diff` | combined pages of both inputs | $0.001 / page | | `bookmarks` | per call | $0.001 flat | | `attachments` | per call | $0.001 flat | These are launch defaults; the live `402` challenge is authoritative (an unauthenticated `POST` returns the exact price). Every billable call needs an `Idempotency-Key` header — omit it and the chassis generates one per call. ## merge Combine 2–50 PDFs into one, in the order given. ```json POST /v1/pdf/merge { "files": [ {"inline":""}, {"inputKey":"..."} ], "filename": "combined.pdf" } ``` ## split Split one PDF by 1-based ranges (`"1-3,5,8-10"`) into separate documents, or `burst: true` to explode every page into its own single-page PDF. Omit `ranges` (or pass `"all"`) for the whole document. ```json POST /v1/pdf/split { "file": {"inline":""}, "ranges": "1-3,7-9" } ``` Split produces multiple files, so it returns a **manifest** (not the single output envelope): each entry is a storage reference you can re-submit as an `inputKey` to another op. ```json { "files": [ {"index": 0, "outputKey": "...", "outputUrl": "https://...", "pages": 3, "sizeBytes": 12345} ] } ``` ## rotate Rotate pages by 90 / 180 / 270 degrees. `pages` is an optional 1-based range string; omit it to rotate every page. ```json POST /v1/pdf/rotate { "file": {"inline":""}, "degrees": "90", "pages": "1,3-5" } ``` ## pages Edit page structure. One of three actions: - `delete` — drop a range of pages: `{"action":"delete","file":{...},"pages":"2,4-6"}` - `reorder` — supply the new 1-based page order: `{"action":"reorder","file":{...},"order":[3,1,2]}` - `insert` — splice another PDF in at a 1-based position (`at: 1` inserts before the first page; `at: N+1` appends): `{"action":"insert","file":{...},"insert":{...},"at":2}` ## watermark Stamp text on every page (or a `pages` range). Tune `opacity` (0–1), `size` (pt, ≤ 400), `color` (`#RRGGBB`), and `position` (`center` | `top-left` | `top-right` | `bottom-left` | `bottom-right`). ```json POST /v1/pdf/watermark { "file": {"inline":""}, "text": "CONFIDENTIAL", "opacity": 0.2, "position": "center" } ``` ## form Fill or flatten an AcroForm. Pass `fields` as a map of field name → string or boolean (checkbox); set `flatten: true` to bake the values in so the form is no longer editable. ```json POST /v1/pdf/form { "file": {"inline":""}, "fields": {"name":"Ada Lovelace","subscribe":true}, "flatten": true } ``` ## extract-text Pull text + structure out of a PDF (engine: pdf.js / unpdf). Returns per-page text plus the concatenated whole. `pages` is an optional 1-based range string. ```json POST /v1/pdf/extract-text { "file": {"inline":""}, "pages": "1-5" } ``` Response (not an output envelope — this op returns parsed text directly): ```json { "totalPages": 12, "pages": [ {"page": 1, "text": "..."} ], "text": "..." } ``` ## metadata Read document metadata, or set any of `title`, `author`, `subject`, `keywords`, `creator`, `producer`. Read by omitting `set`; write by passing it. Flat per-call price. A read returns `{ "metadata": { ... } }`; a write returns the standard output envelope with the updated PDF. ```json POST /v1/pdf/metadata { "file": {"inline":""}, "set": {"title":"Q3 Report","author":"finance-bot"} } ``` ## encrypt Password-protect a PDF (qpdf, 256-bit AES). Supply `userPassword` (the open password), `ownerPassword` (permissions password), or both — at least one. Neither may begin with `-`. Supplying only an `ownerPassword` leaves the open password empty: anyone can open the document, but permissions stay owner-gated (the standard PDF model). Eight optional **permission booleans** — the canonical PDF permission set — restrict what an opened document allows: `print`, `printHighRes`, `copy` (text/graphics extraction), `modify`, `annotate`, `fillForms` (fill in form fields), `extract` (extract for accessibility), and `assemble` (insert/delete/rotate pages). An **absent** boolean means the qpdf default (allowed); pass `false` to restrict. Permissions are enforced by PDF viewers against the owner password. Two notes on the set: `print` and `printHighRes` fold into one print level — `print:false` blocks printing entirely, `print:true` with `printHighRes:false` allows only low-resolution printing, and `print:true` alone allows full-quality printing. And `copy` is the live text/graphics extraction control; `extract` is the separate *accessibility*-extraction bit, which PDF 2.0 (the 256-bit encryption used here) deprecated and always grants — so `extract:false` may be a no-op on modern viewers. Use `copy:false` to actually block extraction. ```json POST /v1/pdf/encrypt { "file": {"inline":""}, "userPassword": "hunter2", "ownerPassword": "admin-only", "print": true, "printHighRes": false, "copy": false, "modify": false, "annotate": false, "fillForms": true, "assemble": false } ``` Passwords ride only in the request body to the IAM-gated worker — they're never logged. ## compress Recompress a PDF's object and content streams (qpdf). Billed **per page, not by savings** — the compression ratio depends entirely on the document (an already-optimized PDF may not shrink). Set `linearize: true` for web "fast view" (byte-range streaming). ```json POST /v1/pdf/compress { "file": {"inline":""}, "linearize": true } ``` Looking for a "linearize" / web-optimize op? It's this flag — `linearize: true` on compress IS the linearization path (there is no separate `pdf/linearize` op). ## repair Repair a damaged PDF (qpdf). Two steps in one call: `qpdf --check` **diagnoses** the document (a findings report — broken xref, stream-length mismatches, structural warnings), then a full **rewrite** re-serializes it, reconstructing the cross-reference table and normalizing structure. Billed per page (compress's rate). Returns the repaired PDF **always as a storage ref** plus the diagnosis: ```json POST /v1/pdf/repair { "file": {"inline":""} } ``` ```json { "output": { "outputKey": "...", "outputUrl": "https://...", "sizeBytes": 51234, "contentType": "application/pdf", "filename": "repaired.pdf" }, "repair": { "exitCode": 2, "findings": ["xref not found", "Attempting to reconstruct cross-reference table", "..."] } } ``` `repair.exitCode` is qpdf's `--check` verdict: `0` clean, `2` errors found, `3` warnings — all three still produce a rewritten output. A PDF too damaged to parse at all returns `422 PDF_PARSE_FAILED` (charge reversed). ## render Rasterize PDF pages to PNG or JPEG (poppler `pdftoppm`). `pages` is an optional 1-based range (default: all); `dpi` ≤ 300 (default 150); `format` is `png` or `jpeg`. Non-embedded standard-14 fonts (Helvetica, Times, …) render correctly via a vendored DejaVu Sans substitution. **Capped at 40 pages and 300 DPI** — over either returns `422` pre-charge. Billed per **output** page. ```json POST /v1/pdf/render { "file": {"inline":""}, "pages": "1-5", "dpi": 200, "format": "png" } ``` Render produces multiple images, so it returns a **manifest** (like split), one presigned-GET image ref per page: ```json { "images": [ {"index": 0, "page": 1, "outputKey": "...", "outputUrl": "https://..."} ] } ``` ## ocr Extract text from a scanned/image PDF (poppler `pdftoppm` → Tesseract LSTM). `pages` is an optional 1-based range (default: all). **Synchronous and capped at 5 pages** — the call must clear a ~30-second window, so larger asynchronous OCR jobs are coming. Over the cap returns `422 OCR_TOO_MANY_PAGES_SYNC` (which advertises the cap) before any charge. Billed per **source** page. `lang` selects the OCR language from the **deployed 10-language roster**: `eng` (default), `spa`, `fra`, `deu`, `ita`, `por`, `nld`, `pol`, `rus`, `chi_sim`. The live roster is the operator-tunable `cputools.ocr.langs`; an off-roster `lang` returns a **free** `422 UNSUPPORTED_LANG` that names the roster. The same `lang` parameter (and roster) applies to `ocr-searchable` and [`image/ocr`](/docs/image-tools). ```json POST /v1/pdf/ocr { "file": {"inline":""}, "pages": "1-3", "lang": "spa" } ``` Response (parsed text, not an output envelope): ```json { "pages": [ {"page": 1, "text": "..."} ], "text": "..." } ``` ## ocr-searchable Turn a scanned PDF into a **searchable PDF**: per page, the worker rasterizes (`pdftoppm`), Tesseract emits a page PDF carrying the original page image plus an **invisible text layer**, and qpdf merges the pages. The result looks identical but is selectable, copyable, and indexable. Same `pages` / `lang` parameters and the same 5-page synchronous cap as `ocr`; billed per **source** page at a premium over plain OCR (it runs OCR *plus* PDF assembly). ```json POST /v1/pdf/ocr-searchable { "file": {"inline":""}, "pages": "1-3", "lang": "eng" } ``` The output is **always a storage ref** — `{ output: { outputKey, outputUrl, sizeBytes, contentType, filename } }`, never inline (the render convention; OCR'd page images are large). Fetch the PDF from the presigned `outputUrl`, or chain `outputKey` straight into another op. ## from-html **Generate a PDF from HTML** — render caller-supplied HTML/CSS to a PDF on a headless Chromium worker. This is how you produce invoices, reports, contracts, and certificates from a template + data: build the HTML, get back a print-ready PDF. Flat per-call price. ```json POST /v1/pdf/from-html { "html": "

Invoice #1042

…", "options": { "format": "A4", "margin": {"top":"2cm","bottom":"2cm"}, "printBackground": true, "headerTemplate": "
Invoice #1042
", "footerTemplate": "
Page of
", "pageRanges": "1-3" } } ``` `options` (all optional): `format` (`A4` | `Letter` | `Legal` | `A3` | `A5` | `Tabloid` | `Ledger`), `landscape` (boolean), `margin` (`top` / `right` / `bottom` / `left`, each a size string in `px`, `cm`, `mm`, or `in`), `printBackground` (boolean), `scale` (0.1–2). Plus print furniture and output controls: `headerTemplate` / `footerTemplate` (HTML for the running header/footer — use the `date`, `title`, `url`, `pageNumber`, and `totalPages` classes to inject values; leave room with `margin`), `displayHeaderFooter` (boolean — auto-enabled when you supply a template, so you rarely set it yourself; set `false` to suppress), `pageRanges` (a subset to print, e.g. `"1-5, 8"`; default all pages), and `tagged` (emit a tagged/accessible PDF — **default `true`**). The HTML itself is capped at 5 MB (`cputools.render.max_html_bytes`, operator-tunable; over → free `422 HTML_TOO_LARGE`). Returns the standard output envelope (a PDF). **Sandboxed by design.** The renderer is locked **read-only with no network and no JavaScript**: every external resource request — ``, ``, `" } ``` ## /docs/codes-tools # Codes tools — decode & color Two in-process readers that complement the [QR/barcode generators](/docs/cputools-qr-tools). Pure JavaScript ([`jsqr`](https://github.com/cozmo/jsQR) over a [`sharp`](https://sharp.pixelplumbing.com/) decode; color math is plain arithmetic). Each is a `POST https://api.relaystation.ai/v1/codes/` returning a small **JSON object**. ## Billing Both are **flat** at `cputools.price.codes..flat_micros` (launch default $0.0002 / call — a placeholder the operator tunes). The live `402` challenge is authoritative. | Op | Billed on | Rate | |---|---|---| | `qr-decode` | per call | $0.0002 flat | | `color-convert` | per call | $0.0002 flat | ## qr-decode Read a QR code from an uploaded image. Body: `{ file: }` (inline base64 ≤ 4 MB or an `inputKey` from `/v1/cputools/upload-url`). A non-decodable image is a **paid** `{ found: false }`; a non-image input is a free `422`. Returns `{ found, data?, version? }`. ```json POST /v1/codes/qr-decode { "file": { "inline": "" } } ``` ## color-convert Convert a color between `hex`, `rgb`, and `hsl` (including alpha forms — `#rrggbbaa`, `rgba()`, `hsla()`). Body: `{ color, to }`. Unrecognized input → `422 COLOR_INVALID`. Returns `{ input, to, result, rgb }`. ```json POST /v1/codes/color-convert { "color": "#ff8800", "to": "hsl" } ``` > **Not yet:** 1D **barcode decoding** is deferred — there's no clean pure-JS Node decoder (the common libraries need a browser DOM or a WASM binary). Barcode **generation** ships today at `/v1/barcode`. ## /docs/data-sql # Data — SQL `POST https://api.relaystation.ai/v1/data/sql` runs **full SQL over your uploaded file** — a real [DuckDB](https://duckdb.org/) engine, not a partial dialect. Upload a CSV/JSON, send a `SELECT`, get the result back. The power of a database with the simplicity of one stateless call. ## Sandboxed by design The engine is the selling point — and so is the cage around it. Your SQL runs against your file and **nothing else**: the engine is locked **read-only with external access disabled** before your query runs, so it cannot read other files, reach the network (no `httpfs` / SSRF), write files, install or load extensions, or attach other databases. Your query sees exactly one table — `input`, loaded from your upload — and the full DuckDB SQL surface over it (joins, window functions, CTEs, aggregates). Powerful *and* safe. ## Inputs and outputs Body: `{ file, sql, from?, to? }`. The `file` is an **input source** — `{ "inline": "" }` (≤ 4 MB) or `{ "inputKey": "..." }` (≤ 50 MB, from `POST /v1/cputools/upload-url`). `from` is the input format (`csv` default, `json`, `ndjson`, `parquet`); `to` is the result format (`csv` default, `json`, `parquet`). Your file is loaded as the table **`input`** — write `... FROM input`. The result comes back in the uniform output envelope. ### Parquet, in and out DuckDB reads [**Parquet**](https://parquet.apache.org/) directly — pass `from: "parquet"` to query a columnar file with the same `SELECT ... FROM input`. And `to: "parquet"` emits a **binary Parquet** result: `csv`/`json` results return as inline text, but Parquet is binary, so it rides the storage-ref output envelope (`{ output: { outputKey, outputUrl, … } }`) — see [Passing & receiving files](/docs/receiving-outputs). The Parquet file is produced by a **safe two-step** that never touches the read-only user-SQL sandbox, so the sandbox guarantees above hold unchanged. A handy shape: `{ from: "csv", to: "parquet", sql: "SELECT * FROM input" }` is a one-call CSV→Parquet conversion (and `from: "parquet", to: "csv"` the reverse). Gzipped inputs are handled transparently — a gzip-compressed `csv`, `json`, or `ndjson` upload is detected by its magic bytes and decompressed before the query, no extra flag needed. ## Billing Per-MB of input, **minimum 1 MiB**, at `cputools.price.data.sql.per_mb_micros` (launch default **$0.0005 / MB** — a higher tier than the in-process ops, since the query runs on the vendored DuckDB worker). The live `402` challenge is authoritative. ## Query ```json POST /v1/data/sql { "file": {"inline":""}, "sql": "SELECT country, count(*) AS n, sum(spend) AS total FROM input GROUP BY country ORDER BY total DESC", "to": "json" } ``` ## Sample ```bash curl -X POST https://api.relaystation.ai/v1/data/sql \ -H 'X-Payment: ' \ -H 'Idempotency-Key: top-countries-20260609' \ -H 'Content-Type: application/json' \ -d '{"file":{"inline":"Y291bnRyeSxzcGVuZApVUywxMApVUyw1CkRFLDcK"},"sql":"SELECT country, sum(spend) AS total FROM input GROUP BY country ORDER BY total DESC","to":"json"}' ``` ## Caps Operator-tunable, enforced per call: a memory limit (`cputools.data.sql.memory_limit_mb`), a wall-clock timeout (`cputools.data.sql.timeout_ms`, default ~25 s — a query that runs long is killed), an output-size cap (`cputools.data.sql.max_output_bytes`, 128 MiB — a larger result returns `413`), a thread cap, and an SQL-length cap. These bound a runaway or oversized query; the engine sandbox (above) bounds what the query can reach. ## Errors - `402 PAYMENT_REQUIRED` — no valid payment. - `422 SQL_INVALID` — a SQL syntax/semantic error (the DuckDB message is surfaced; your data is never echoed). Includes any attempt to use a blocked capability (file/network/extension) — the sandbox denies it. - `413 OUTPUT_TOO_LARGE` — the result exceeds the output cap (incl. a large `to: "parquet"` result). - A query that exceeds the time or memory limit is terminated and the charge refunded. ## Next [Data tools](/docs/data-tools) · [Pipeline](/docs/pipeline) · [Pricing](/pricing) · [API reference](/api-reference) ## /docs/pipeline # Pipeline `POST https://api.relaystation.ai/v1/pipeline` runs a **sequence of transforms in one call**. Send a file and a list of steps; the pipeline feeds each step's output into the next and returns the final result — no intermediate round-trips, no orchestration on your side. This is what turns the cputools catalog from a list of operations into a composable transform runtime. ## How it works ```json POST /v1/pipeline { "file": {"inline":""}, "steps": [ { "op": "data.filter", "args": { "where": {"column":"status","op":"eq","value":"active"} } }, { "op": "data.groupby", "args": { "by": ["country"], "aggregate": [{"fn":"count","as":"n"}] } }, { "op": "csv.convert", "args": { "to": "json" } } ], "to": "json" } ``` Each step is `{ "op": ".", "args": { ... } }` — the op's normal arguments, minus the `file` (the data is threaded for you). Steps run **in order**, left to right; the final `to` sets the output format. ## The composable op set The pipeline composes the in-process single-stream ops — tabular **and** binary: - **csv** — `csv.convert`, `csv.dedupe`, `csv.select` - **data** — `data.filter`, `data.sort`, `data.groupby`, `data.cast`, `data.pivot`, `data.derive`, `data.fillna`, `data.dropna`, `data.rename`, `data.slice`, `data.explode`, plus the **Excel bridges** `data.from-xlsx` / `data.to-xlsx` (and `data.profile`, `data.validate`, `data.schema-infer` as final, report-producing steps) - **image** — `image.resize`, `image.convert`, `image.compress`, `image.rotate` (chain image transforms in one call) - **utils** — `utils.base64` - **archive** — `archive.gzip` Steps thread bytes, so a chain stays within a data type — a tabular chain, an image chain, or an **Excel-in/Excel-out** chain (`from-xlsx → … → to-xlsx`). Multi-input ops (`data.join`, `data.union`, `data.diff`), the SQL engine (`data.sql`), and the worker-backed PDF ops aren't composable in a pipeline — use them as standalone calls. ## Billing One charge: **the sum of the steps**. Each step is priced at its normal per-op rate (read live from the same `cputools.price.*` tunables the standalone ops use), plus an orchestration fee (`cputools.price.pipeline.run.flat_micros`, default $0). A three-step pipeline on a 5 MB file is `Σ (step rate × 5 MiB) + fee`. An unauthenticated `POST` returns the exact summed price as a `402` challenge — quote before you pay. ## Atomic A pipeline is all-or-nothing: if any step fails (an unknown column, a malformed intermediate, an over-cap result), the whole call fails and the charge is **refunded**. You never pay for a partial run. ## Caps Operator-tunable: `cputools.pipeline.max_steps` (default 10) bounds the chain length; `cputools.pipeline.max_intermediate_bytes` (128 MiB) bounds the data threaded between steps — a step whose output crosses it returns `413` and refunds. ## Errors - `402 PAYMENT_REQUIRED` — no valid payment. - `422 OP_NOT_COMPOSABLE` — a step names an op that isn't in the composable set. - `422 TERMINAL_STEP_NOT_LAST` — a report op (`data.profile`) appears before the last step. - `422` (step-level) — a step's own validation error (e.g. `UNKNOWN_COLUMN`) surfaces from that step; the charge is refunded. - `413 INTERMEDIATE_TOO_LARGE` — a step's output exceeds the intermediate cap. ## Next [Data tools](/docs/data-tools) · [Data — SQL](/docs/data-sql) · [CSV tools](/docs/csv-tools) · [Pricing](/pricing) · [API reference](/api-reference) ## /docs/cputools-mcp-tools # MCP tools **One endpoint, the whole Relaystation toolkit.** Point your MCP client at ``` https://api.relaystation.ai/mcp ``` and your agent sees the entire cputools catalog — 88 tools across PDF, CSV, image, codes, utils, generate, archive, data, text, and pipeline — alongside Relaystation's other primitives (e-sign, ID verification, Courier messaging, chassis), all funded from **one balance** with **one API key** (the shared endpoint surfaces 129 tools in total). No separate endpoint per product; no per-tool signup. Same auth, same per-call prices as the HTTP API — the MCP tool call runs the identical billing path, so you pay exactly what the route charges. ## The cputools tools Each tool's manifest `description` states its billing unit, the launch price, and its caps, and notes that an unauthenticated probe returns a live `402` price quote — so your agent can choose and budget without leaving the client. Every file-taking tool accepts `{ inline }` base64 (≤ 4 MB) or `{ inputKey }` from `cputools_upload_url` (≤ 50 MB). **PDF** — `pdf_merge`, `pdf_split`, `pdf_rotate`, `pdf_pages`, `pdf_watermark`, `pdf_form` ($0.001/page); `pdf_extract_text` ($0.002/page); `pdf_metadata` ($0.001 flat); `pdf_encrypt`, `pdf_compress` ($0.001/page); `pdf_render` ($0.001/output page, ≤ 40 pages / ≤ 300 DPI); `pdf_ocr` ($0.003/page, ≤ 5 pages synchronous); `pdf_from_html` ($0.003 flat — HTML→PDF on a sandboxed headless Chromium: no network, no JS, inline assets only); `pdf_from_office` ($0.005 flat — Office→PDF via LibreOffice: docx/xlsx/pptx/odt/ods/odp/rtf/txt/csv, ≤ 30 MB, pay only for a delivered PDF); `pdf_images` ($0.001/page — extract embedded images, ≤ 200), `pdf_diff` ($0.001/page — text compare, billed on both); `pdf_bookmarks`, `pdf_attachments` ($0.001 flat — read outline / list-extract attachments). **CSV** — `csv_convert`, `csv_dedupe`, `csv_select` ($0.0002/MB, min 1 MiB). **Image** — `image_resize`, `image_convert`, `image_compress`, `image_rotate`, `image_crop`, `image_blur`, `image_sharpen`, `image_grayscale`, `image_exif_strip`, `image_composite`, `image_contact_sheet` ($0.0003/MP); `image_metadata`, `image_dominant_color` ($0.0005 flat). **Codes** — `qr_generate`, `barcode_generate`, `codes_qr_decode`, `codes_color_convert` ($0.0002 flat; qr/barcode data ≤ 4096 chars). **Utils** — `utils_hash`, `utils_hmac`, `utils_base64` ($0.0002/MB, min 1 MiB); `utils_uuid`, `utils_jwt_decode`, `utils_jwt_verify` ($0.0001 flat; uuid count ≤ 1000; jwt-verify is alg-pinned and never logs your key). **Generate** — `generate_og_image` ($0.0005 flat, one OG template); `generate_mock_data`, `generate_invoice`, `generate_chart`, `generate_qr_logo`, `generate_favicon`, `generate_placeholder`, `generate_identicon` ($0.0002 flat; mock-data rows ≤ 1000 / fields ≤ 50; invoice ≤ 100 line items; chart ≤ 50 points; identicon is deterministic per seed). **Archive** — `archive_zip`, `archive_unzip`, `archive_gzip` ($0.0002/MB of input, min 1 MiB; `archive_unzip` is zip-bomb-guarded — ≤ 128 MiB uncompressed, encrypted rejected, one level — and bills the compressed input). **Data** — `data_filter`, `data_sort`, `data_groupby`, `data_join`, `data_union`, `data_profile`, `data_cast`, `data_pivot`, `data_schema_infer`, `data_validate`, `data_diff`, `data_from_xlsx`, `data_to_xlsx`, `data_derive`, `data_fillna`, `data_dropna`, `data_rename`, `data_slice`, `data_explode`, `data_sample` ($0.0002/MB, min 1 MiB; `data_from_xlsx`/`data_to_xlsx` are Excel in/out, `data_to_xlsx` formula-injection-safe; `data_filter`/`data_derive` are structured no-eval; `data_validate`'s pattern is a bounded regex; `data_join`/`data_union`/`data_diff` are output-byte-capped; `data_sample` takes n|fraction with a reproducible seed); `data_sql` ($0.0005/MB — full SQL on a sandboxed **read-only** DuckDB: no filesystem, network, or extension access). **Text** — `text_case`, `text_slugify`, `text_diff`, `text_count`, `text_regex_extract`, `text_template`, `text_markdown_to_html`, `text_html_to_text`, `text_sanitize_html` ($0.0002/MB, min 1 MiB; `text_regex_extract` is ReDoS-guarded; `text_template` is no-eval; `text_markdown_to_html`/`text_sanitize_html` whitelist-sanitize the HTML — no script/`javascript:`/`data:`). **Pipeline** — `pipeline_run` (compose a chain of cputools transforms in one call; threaded step-to-step; billed as the **sum of the steps**; atomic — a failed step refunds the whole run). **Upload** — `cputools_upload_url` (free) mints a presigned POST for the > 4 MB path and returns an `inputKey` to pass to any file-taking tool. `tools/list` on the endpoint also surfaces Relaystation's chassis + Courier tools, the e-sign (`esigndoc_*`) and ID-verification (`idverify_*`) product tools, and the `relaystation_products` meta-tool (which lists every Relaystation product MCP). One key, one balance, the whole set. ## Auth A tool call carries your credential one of three ways, and inherits the same billing as the HTTP route (your balance is debited per call): - **API key** — `Authorization: Bearer rs_live_`, or `?key=rs_live_` on the URL (handy for clients without a header field). - **x402** — an `X-Payment` header per call (the no-account lodestone path). - **OAuth agent-login** — the client runs the OAuth consent dance on connect; the resulting token carries the `products:call` scope. `tools/list` is browsable as soon as any credential is present; billable tool calls debit the resolved customer. ## Install — Claude Code & Claude Desktop In your config (`~/.config/claude-code/mcp.json`, the workspace `.mcp.json`, or Claude Desktop's `claude_desktop_config.json`), add: ```json { "mcpServers": { "relaystation": { "url": "https://api.relaystation.ai/mcp", "headers": { "Authorization": "Bearer rs_live_" } } } } ``` Restart the client; the cputools roster appears alongside the rest of the Relaystation tools. Mint a key at [app.relaystation.ai](https://app.relaystation.ai) or via `POST /v1/account/keys`. ## Install — Cursor, Codex, Cline, Continue The same MCP server URL applies. Each client has its own config location (Cursor: Settings → MCP; Codex: workspace config; Cline / Continue: extension settings), but the shape is identical — URL `https://api.relaystation.ai/mcp` plus the `Authorization: Bearer rs_live_` header. ## Install — Cowork In Cowork: **Settings → Connectors → Add connector.** Paste the URL with your key as a query parameter (Cowork's connector flow has no separate header field): ``` https://api.relaystation.ai/mcp?key=rs_live_ ``` Enable "always allow" and the cputools tools surface in Cowork's picker; tool calls land authenticated and debit your balance. > **⚠️ Treat the whole URL as sensitive.** The `?key=` query string carries your `rs_live_*` credential, so anywhere the URL ends up — browser history, terminal scrollback, screenshots — is somewhere your key has been. If it leaks, revoke that key from the dashboard and mint a fresh one (your balance and cap are unaffected; only the leaked credential becomes invalid). Per-key daily spend caps bound the exposure. ## Chassis MCP concepts The MCP protocol, authentication, and discovery model are shared across every Relaystation product and documented on the apex docs: - [MCP](https://relaystation.ai/docs/mcp) — the Relaystation MCP service, manifest discovery, and the auth header shapes. - [Authentication](https://relaystation.ai/docs/authentication) — API key, wallet JWT, and x402 per-call paths. - [x402 wire format](https://relaystation.ai/docs/x402) — the per-call payment authorization. ## Next [Overview](/docs/cputools-overview) · [PDF tools](/docs/pdf-tools) · [CSV tools](/docs/csv-tools) · [Image tools](/docs/image-tools) · [QR & barcode](/docs/qr-tools) · [Utils tools](/docs/utils-tools) · [Generate tools](/docs/generate-tools) · [Archive tools](/docs/archive-tools) · [Data tools](/docs/data-tools) · [Data — SQL](/docs/data-sql) · [Pipeline](/docs/pipeline) · [Pricing](/pricing) · [API reference](/api-reference) ## /docs/office # Office tools Two office surfaces, one engine (LibreOffice via Gotenberg), output always **PDF**: - **`POST /v1/office/rescue`** — best-effort convert an *exotic or legacy* office format to PDF. The formats LibreOffice can import but you won't reach through the everyday [`from-office`](/docs/pdf-tools) route: Apple iWork, Visio, Publisher, CorelDRAW, WordPerfect, the legacy MS binaries, and flat ODF. - **`GET /v1/office/formats`** — the free, no-auth roster of what each office route accepts. The everyday office formats (`docx`, `xlsx`, `pptx`, `odt`, `ods`, `odp`, `rtf`, `txt`, `csv`) go through [`POST /v1/pdf/from-office`](/docs/pdf-tools). `office/rescue` is the premium best-effort surface for the long-tail formats around it. > **Fidelity is best-effort.** These are exotic, sometimes proprietary, sometimes very old formats. LibreOffice does its best to import them, and you get a clean PDF — but complex layout, fonts, or vector detail may not survive perfectly. The output is always a faithful-as-possible PDF, never a guarantee of pixel parity. And **office↔office conversion (e.g. docx↔odt) is not offered** — every office route outputs PDF. ## Rescue a legacy or exotic document ```bash curl -X POST https://api.relaystation.ai/v1/office/rescue \ -H 'Authorization: Bearer rs_live_' \ -H 'Idempotency-Key: rescue-visio-20260612' \ -H 'Content-Type: application/json' \ -d '{ "file": { "inline": "" }, "filename": "network-diagram.vsdx" }' ``` Or on the lodestone path — no account, a signed x402 payment instead of an API key: ```bash curl -X POST https://api.relaystation.ai/v1/office/rescue \ -H 'X-Payment: ' \ -H 'Idempotency-Key: rescue-visio-20260612' \ -H 'Content-Type: application/json' \ -d '{ "file": { "inline": "" }, "filename": "network-diagram.vsdx" }' ``` The `filename` is required — its extension is how the format is recognized. Takes the standard input source (`{ "inline": "" }` ≤ 4 MB, or `{ "inputKey": "..." }` from [`POST /v1/cputools/upload-url`](/docs/receiving-outputs) for larger files) and returns the standard [output envelope](/docs/receiving-outputs) — a PDF inline when small, a presigned URL when large. ## The rescue roster The default exotic/legacy roster (operator-tunable, so it can flex without a redeploy): | Family | Extensions | |---|---| | Apple iWork | `pages`, `numbers`, `key` | | Visio | `vsd`, `vsdx` | | MS Publisher | `pub` | | CorelDRAW | `cdr` | | Flat ODF | `fodt` | | Legacy MS Office binaries | `doc`, `xls`, `ppt` | | WordPerfect | `wpd` | `GET /v1/office/formats` is always authoritative. Free, read-only, no auth: ```bash curl https://api.relaystation.ai/v1/office/formats ``` It returns the rescue roster and the everyday from-office roster, each with its output (`pdf`), plus a note that office↔office conversion is not offered: ```json { "rescue": { "inputs": ["pages", "numbers", "key", "vsd", "vsdx", "pub", "cdr", "fodt", "doc", "xls", "ppt", "wpd"], "output": "pdf" }, "fromOffice": { "inputs": ["docx", "xlsx", "pptx", "odt", "ods", "odp", "rtf", "txt", "csv"], "output": "pdf" }, "note": "office↔office conversion (e.g. docx↔odt) is not offered; all office routes output PDF." } ``` An extension outside the roster returns a free `422 UNSUPPORTED_FORMAT` that points you at `GET /v1/office/formats`. ## Export controls (from-office and rescue) Both `POST /v1/pdf/from-office` and `POST /v1/office/rescue` accept an optional `options` object passed straight to LibreOffice's PDF export — absent fields take the engine default: | Field | Type | Notes | |---|---|---| | `pageRanges` | string | a page subset, e.g. `"1-5,8"` | | `quality` | integer 1–100 | JPEG quality for embedded images | | `losslessImageCompression` | boolean | use lossless image compression | | `reduceImageResolution` | boolean | gate for `maxImageResolution` — must be `true` for it to apply | | `maxImageResolution` | one of `75`, `150`, `300`, `600`, `1200` | DPI ceiling for embedded images (a closed ladder; other values are rejected) | | `landscape` | boolean | force landscape orientation | ```bash curl -X POST https://api.relaystation.ai/v1/pdf/from-office \ -H 'Authorization: Bearer rs_live_' \ -H 'Idempotency-Key: print-ready-20260612' \ -H 'Content-Type: application/json' \ -d '{ "file": { "inputKey": "" }, "filename": "q3-report.docx", "options": { "pageRanges": "1-10", "reduceImageResolution": true, "maxImageResolution": 150, "landscape": true } }' ``` ## Billing | Route | Unit | Price | |---|---|---| | `office/rescue` | per call | $0.008 flat | | `from-office` | per call | $0.005 flat | | `office/formats` | — | free | Rescue is priced a step above the everyday `from-office` route — it's heavier, best-effort work. Both are flat per convert (the output page count isn't known before the conversion runs). **Charge-on-attempt** — a convert that reaches the engine and succeeds stands; one that can't be delivered (a corrupt file, an upstream failure) returns `422 CONVERT_FAILED` and the charge is reversed: you pay only for a delivered PDF. Every billable call requires an `Idempotency-Key`; a same-key retry returns the cached result without re-charging. ## MCP tools Callable over MCP at `https://api.relaystation.ai/mcp`: `office_rescue` (billable) and `office_formats` (free, read-only), plus `pdf_from_office` (the everyday route). Same auth, same prices as the HTTP routes. ## Next [Quickstart](/docs/quickstart) · [Passing & receiving files](/docs/receiving-outputs) · [PDF tools](/docs/pdf-tools) · [Document conversion](/docs/doc-convert) · [API reference](/api-reference) ## /docs/receiving-outputs # Passing & receiving files Every file-taking or file-producing op on Relaystation uses the **same I/O model**. You pick how to pass (or receive) the bytes per call — there is no setup step and no required storage product. Compute is priced per op on the metered input **regardless of where the bytes came from**. ## Passing a file in Three ways to hand an op its input, picked per call: - **Inline** — up to **4 MiB** (decoded), pass `{ "inline": "" }` as the op's file field. Nothing is stored; nothing to clean up. - **Scratch** — for files over 4 MiB, or whenever you'd rather upload once and reuse: 1. `POST /v1/cputools/upload-url` (free) returns a presigned upload form and an `inputKey` like `scratch//`. 2. Upload your file (up to **50 MB**) with one multipart POST to that URL. 3. Pass `{ "inputKey": "..." }` as any op's file field — as many ops, as many times as you like, for **24 hours**. Scratch keys are namespaced to your customer identity: another customer's key — even if leaked — resolves to `404` for you, and yours for them. The same gate admits all auth paths (API key, wallet-JWT, or a signed x402 payment), so an account-less agent can use scratch too. - **A baton** — pass `{ "batonId": "bat_…" }` (optionally `{ "batonId": "bat_…", "entryRef": { "sequenceNumber": N } }` to select one append entry) to read a [baton](https://relaystation.ai/docs/batons) as the op's input. The op's **compute charge** applies exactly as for any other input; the baton's **storage/egress** draw down the **prepaid quota** you bought at create and appear as byte-denominated quota events, never as a second money charge. > **Baton input works on HTTP and MCP.** `{ "batonId": … }` is a first-class input source on both the HTTP API and the MCP cputools tools (and `deliver` lands an op's output into a baton on both). Ownership is enforced identically on either surface — a baton you don't own resolves to `404`. ## Getting results back Every binary-producing op returns the same envelope. Small results arrive **inline** (the gate is the base64-encoded size against a 4 MiB threshold — raw outputs up to ~3 MiB, since base64 inflates ~1.37×); larger ones land in your free 24-hour scratch and arrive as a key + a link: ```json { "output": { "inline": "", "sizeBytes": 51234, "contentType": "application/pdf", "filename": "converted.pdf" } } { "output": { "outputKey": "scratch//", "outputUrl": "https://…presigned, valid 1 hour…", "sizeBytes": 12023329, "contentType": "image/png" } } ``` Handle both shapes and you handle every op. Recipes per client: ### curl / shell — to disk ```bash RESP=$(curl -s -X POST https://api.relaystation.ai/v1/pdf/merge \ -H "Authorization: Bearer $KEY" -H "Idempotency-Key: $(uuidgen)" \ -d @payload.json) if echo "$RESP" | jq -e '.output.inline' > /dev/null; then echo "$RESP" | jq -r '.output.inline' | base64 -d > result.pdf else curl -s -o result.pdf "$(echo "$RESP" | jq -r '.output.outputUrl')" fi ``` The `outputUrl` is presigned — no auth header on that GET (and don't send one; it's a direct storage URL, not an API route). ### Code (fetch) — in an agent sandbox ```js const { output } = await (await fetch(url, opts)).json(); const bytes = output.inline ? Buffer.from(output.inline, 'base64') : Buffer.from(await (await fetch(output.outputUrl)).arrayBuffer()); ``` If your sandbox blocks outbound domains, allow the API host **and** the storage host the `outputUrl` points at (it is a different domain). ### Chat-based clients (MCP) — present the link In chat clients the tool result is JSON in the conversation; nobody wants 12 MB of base64 there. For large outputs, surface `outputUrl` to the human as a download link — it works in a browser for one hour. If the session outlives the link, re-fetch a fresh one (below) or chain the `outputKey` into a durable [baton](https://relaystation.ai/docs/batons) when the result must persist. MCP tool results for a stored binary output also carry a **`resource_link`** content block alongside the JSON — `{ "type": "resource_link", "uri": "", "name": "", "mimeType": "" }` (MCP spec 2025-06). A client that understands resource links can offer the download directly; one that doesn't simply ignores the extra block — the JSON (and `structuredContent`) still carry `outputKey` + `outputUrl`, so nothing is lost. ### Re-fetching a stored output The `outputUrl` an op returns expires in an hour. To get a **fresh** link for the same object without re-running the op, call: ```bash curl -s https://api.relaystation.ai/v1/outputs/scratch// \ -H "Authorization: Bearer $KEY" # → { "outputKey", "url": "", "sizeBytes", "contentType", "expiresAt" } ``` This works for as long as the scratch object lives (24 hours), then returns `404`. It is **free**, and ownership-gated: you can only re-fetch keys under your own `scratch//` namespace — anyone else's key (or an aged-out one) returns `404`, never a hint that it exists. The `url` it returns is the immediate download; `outputUrl` from the original response stays valid for its hour too. ## Chaining: outputKey is an inputKey An op's `outputKey` lives in the same namespace as your uploads, so you can feed it straight into the next op's `inputKey` — no download, no re-upload, no pipeline product: ```bash # 1. Upload once (12 MB scan) → inputKey curl -s -X POST https://api.relaystation.ai/v1/cputools/upload-url \ -H "Authorization: Bearer $KEY" -d '{"ext":"png"}' # → { "inputKey": "scratch//aaaa.png", "url": ..., "fields": {...} } (multipart-POST the file to url+fields) # 2. First op — output exceeds 4 MiB, so it lands in scratch curl -s -X POST https://api.relaystation.ai/v1/image/convert \ -H "Authorization: Bearer $KEY" -H "Idempotency-Key: $(uuidgen)" \ -d '{"file":{"inputKey":"scratch//aaaa.png"},"format":"png"}' # → { "output": { "outputKey": "scratch//bbbb", ... } } # 3. Chain — the outputKey IS the next inputKey curl -s -X POST https://api.relaystation.ai/v1/image/metadata \ -H "Authorization: Bearer $KEY" -H "Idempotency-Key: $(uuidgen)" \ -d '{"file":{"inputKey":"scratch//bbbb"}}' ``` Each step is its own pay-per-call op with its own receipt. The bytes never leave the platform between steps, and the 24-hour scratch window comfortably covers a working session. (For chaining the *tabular* ops — filter, sort, join, SQL — in a single call, see [`POST /v1/pipeline`](https://cputools.relaystation.ai/docs/pipeline), which threads bytes step-to-step and bills the sum.) ## Delivering an output into a baton Instead of receiving the bytes, you can have an op **deliver** its output straight into a [baton](https://relaystation.ai/docs/batons) — durable, shareable, witnessed: - `"deliver": { "batonId": "bat_…" }` lands the output in an **existing** baton. That's **one** money charge (the op's compute) plus quota events for the storage drawn down — never a second charge for the same byte. - `"deliver": { "new": { "preset": "drop", "tier": "nano" } }` creates a **fresh** baton (quoted exactly like [`POST /v1/baton`](https://relaystation.ai/docs/batons)) and lands the output in it. That's **two ledger lines** — the op's compute charge *and* the baton-create charge — both real, shown distinctly. The whole thing is one idempotent unit: a retried call with the same `Idempotency-Key` returns the original result *and* the original baton id, never a second baton or a second charge. ## Lifetimes How long each thing lives: - **`outputUrl`** (a presigned download link) — valid **1 hour** from the response. - **`outputKey` / `inputKey`** (the scratch object behind it) — lives **24 hours**, then auto-deletes. Reusable across as many ops as you like inside that window. - **Inline** bytes — never stored; they exist only for the one call. - **A baton** — lives for the duration you configured at create. When a result must outlive the day — be **durable**, **shareable** (handed to another agent or a human via a token), or **witnessed** (tamper-evident, provable) — that's the baton tier, a paid product with its own quoted price, lifecycle, and trust options. Batons are optional, always: no op requires one, and the [lodestone path](https://relaystation.ai/docs/lodestone) never gains a mandatory storage step. ## Shared: cputools-faq # cputools FAQ ### What is cputools? A pay-per-call utility API for the CPU work your agent shouldn't host itself. PDF tools are live today — merge, split, rotate, page edits, watermark, form fill/flatten, text extraction, and metadata. Your agent sends a file and a payment, gets a result back, and never created an account. The roster grows over time: QR & barcodes, OCR, render-to-image, and Office→PDF are next. ### Do I need an account? No. The lodestone path is one HTTPS call with an `X-Payment` header carrying an x402 (EIP-3009) authorization — no signup, no email, no deposit. The wallet that signs the payment is your identity, so your transaction history is there if you come back. If you'd rather fund from a prepaid balance, sign in via Google or GitHub and use a `Bearer rs_live_*` API key instead. ### How does pricing work — will I get a surprise bill? You won't. Most PDF ops are $0.001 per page (extract-text $0.002, OCR $0.003, metadata $0.001 flat); CSV ops are $0.0002 per MB of input; image ops are $0.0003 per megapixel (metadata $0.0005 flat); QR and barcode are $0.0002 flat. Per-unit ops bill a minimum of one (one page / one MiB / one megapixel). There's no subscription, no minimum, and no commitment — you pay only for the calls you make, debited from your Relaystation balance, and an unauthenticated `POST` returns the exact price for a call before you pay. When the balance hits zero, calls return a `402` response — no overdraft, no auto-charge. ### How do I send a file bigger than 4 MB? Inline base64 is capped at 4 MB (it travels in the JSON request body). For larger files, `POST /v1/cputools/upload-url` to mint a one-time, customer-scoped presigned upload; it returns `{ url, fields, inputKey, ... }`. Upload the bytes with a multipart/form-data `POST` to `url` (send every `fields` entry plus a `file` part), then pass `{"inputKey":"..."}` as the file instead of `{"inline":"..."}`. Results above the threshold come back the same way — as a presigned `outputUrl` instead of inline base64. ### What's the egress surcharge? A small per-MB charge ($0.0002/MB) that applies **only** when an output is delivered by storage reference — the large-file presigned-GET path. It covers the AWS egress cost on big outputs. Inline outputs (≤ 4 MB, returned through the API) don't incur it. It's the only place cputools meters by size rather than by page. ### Can I call cputools over MCP? Yes. The full cputools catalog — 24 tools (the 23 ops plus `cputools_upload_url`) — is callable over MCP at `https://api.relaystation.ai/mcp`, the same endpoint that carries Relaystation's Courier and chassis tools, all funded from one balance with one key. Drop the URL into Claude Code/Desktop, Cursor, Codex, Cline, Continue, or Cowork with a `Bearer rs_live_*` key. See the [MCP tools page](/docs/cputools-mcp-tools) for per-client config. ### What are the tools built on? Open-source [pdf-lib](https://pdf-lib.js.org/) (manipulation) and [pdf.js](https://mozilla.github.io/pdf.js/) (text extraction) — pure JavaScript, no native binaries. These are tools you could self-host; cputools just runs them so you don't have to maintain the infrastructure, the Lambda layers, or the cold-start tax. ### How does cputools relate to the rest of Relaystation? It's one product on the Relaystation chassis. One balance funds cputools, Courier, Baton, and every other Relaystation product — top up once via x402, Stripe, or a crypto wallet, and spend it anywhere. Same wallet-as-identity, same transparent ledger, same pay-per-call promise — no subscription, no minimum. [Relaystation docs →](https://relaystation.ai/docs) ### Is my file content private? cputools is stateless: a file goes in, a result comes out, and scratch objects for the large-file path are short-lived and customer-scoped. We don't retain your documents to train anything or scan their contents for upsell targeting — that's forbidden regardless of revenue payoff, the same privacy stance as the rest of the Relaystation family.