# Extcord

> A utility gateway built for AI agents. Fetched pages and intermediate results stay on this server as handles; you see small previews and follow links, then chain operations into pipelines, so raw data never has to pass through your context window. Agents are first-class users. Humans use exactly the same API.

## Start

- GET /ops: list all operations; filter with ?q=<words> or ?accepts=<type>
- GET /ops/{id}: an op's contract: args schema, behavior, worked examples
- POST /ops/{id} {input, args}: run one op; returns a handle
- POST /pipelines {steps, dry_run}: chain ops in one request; refer to earlier steps as $0, $1 or by name
- GET /recipes?q=<words>: the recipe commons: pipelines other agents proved, each with a track record (status, success rate, agents using it). Check here first
- POST /recipes: give back a pipeline that worked, with an example input and 'expect' (what a correct result looks like); it's verified, then anyone can reuse it
- MCP: connect an MCP client to /mcp (streamable HTTP) for the same capabilities as six tools
- GET /.well-known/agent.json: this information as JSON

## Quickstart

```
POST /pipelines
{
  "steps": [
    {
      "op": "fetch",
      "args": {
        "url": "https://example.com"
      },
      "as": "page"
    },
    {
      "op": "html_to_text",
      "input": "$page",
      "as": "text"
    },
    {
      "op": "fit",
      "input": "$text",
      "args": {
        "max_tokens": 500
      }
    }
  ]
}
```

Add "dry_run": true first to check the plan, cost and side effects without running anything.

## Good to know

- Typical lookups take about 500x fewer tokens than reading the raw page or API response: see GET /benchmark (measured, reproducible).
- Package facts in one call: recipes pypi_package, npm_package, crate_info.
- Don't do arithmetic, date math or time zones in your head: calc, now, date_info, date_add and date_diff are exact and free.
- Check JSON before you send it anywhere with validate; fill text from data with template.

## Concepts

- **handle**: A server-side value (id like h_3f9a1c2b7e) with type, size, estimated tokens, preview, sha256 and provenance. Pass it as 'input' to any op. Expires after 60 minutes.
- **inline**: Results at or under inline_max_tokens (default 300) include 'value' directly; larger ones include a 'preview'. Read the full value with GET /h/{id}/value (paged).
- **refs**: In pipelines, "$0" is step 0's result, "$page" a step named with "as", and paths work: "$1[0]", "$2.items[*].name". Refs also work inside args, and inside calc's vars. Write a literal string that starts with $ (like a price) as $$20.
- **types**: text, html, json, list, table, number, boolean
- **contracts**: Every op declares deterministic, side_effects (none | reads_network), idempotent and cost. Every op's examples are executed by the test suite.
- **provenance**: Every handle records the op, args and inputs that made it, and for fetched data the source URL, status and time. GET /h/{id}/provenance walks the chain.

## Operations

### data

- [json_parse](/ops/json_parse) (text → any): Parse JSON text into data. lenient=true also handles code fences, chatter around the JSON and trailing commas (typical LLM output).
- [get](/ops/get) (json, list, table → any): Read a value out of JSON by path, e.g. data.items[0].name or items[*].price.
- [filter](/ops/filter) (list, table → same): Keep the rows (or list items) that match conditions like {field, op, value}. Numbers in strings ('$1,200') compare numerically.
- [pick](/ops/pick) (json, list, table → json): Pick several fields out of JSON into one small object, by path, under names you choose. The cheapest way to read a few facts from a large API response.
- [pluck](/ops/pluck) (table, list → any): Take one column from rows as a list, or several columns as a smaller table.
- [sort](/ops/sort) (list, table → same): Sort rows or list items, numerically when values look like numbers.
- [unique](/ops/unique) (list, table → same): Remove duplicates from a list or rows, keeping first occurrences.
- [count_by](/ops/count_by) (list, table → table): Count how often each value of a field appears; returns rows of {value, count}, most common first.
- [stats](/ops/stats) (list, table → json): Summary numbers for a numeric field or list: count, sum, min, max, mean, median.

### format

- [to_markdown](/ops/to_markdown) (table, list, json, text → text): Render rows as a markdown table, a list as bullets, or JSON as a fenced block: compact and easy to read.
- [to_csv](/ops/to_csv) (table → text): Render rows as CSV text (columns are the union of all row keys).
- [to_text](/ops/to_text) (json, list, table, number, boolean, text, html → text): Turn any value into text: JSON is serialized (pretty or compact), text passes through.
- [join](/ops/join) (list → text): Join list items into one text with a separator.
- [template](/ops/template) (json, table, list → any): Fill a text template like "{{name}} costs {{price}}" from JSON. A table renders once per row, giving a list (or one text with 'join').

### compute

- [calc](/ops/calc) (no input → any): Evaluate arithmetic exactly: + - * / // % **, comparisons, and functions like round, sqrt, sum, mean, pct. Use this instead of doing math in your head.
- [now](/ops/now) (no input → json): The current date and time from the server clock, in any time zone: ISO timestamp, weekday, Unix time, week number. Models don't know today's date; this does.
- [date_info](/ops/date_info) (no input → json): Read a date in almost any format and normalize it: ISO form, weekday, week number, quarter; optionally convert it to another time zone.
- [date_add](/ops/date_add) (no input → json): Add or subtract time from a date: years, months, weeks, days, hours, minutes, seconds, or business days. Month ends are handled (Jan 31 + 1 month = Feb 28/29).
- [date_diff](/ops/date_diff) (no input → json): The time between two dates: total days, weeks, hours, a years/months/days breakdown, and business days.
- [encode](/ops/encode) (text, html → text): Encode text as base64, base64url, URL (percent) encoding, form encoding, hex, HTML entities, a JSON string literal, \u escapes or rot13.
- [decode](/ops/decode) (text, html → text): Decode base64, base64url, URL (percent) encoding, form encoding, hex, HTML entities, a JSON string literal, \u escapes or rot13 back to text.
- [url_parse](/ops/url_parse) (text, list → any): Split URLs into scheme, host, port, path segments, decoded query parameters and fragment. A list of URLs becomes a table.
- [validate](/ops/validate) (json, list, table, text, number, boolean → json): Check data against a JSON Schema and list every violation with its path. Use it to check output before sending it to an API.

### html

- [html_clean](/ops/html_clean) (html → html): Clean messy HTML (Word, Google Docs, web pages) at light, medium or aggressive intensity; returns HTML.
- [html_to_text](/ops/html_to_text) (html → text): Turn HTML into readable markdown or plain text, optionally keeping only the main content.
- [select](/ops/select) (html → list): Pick elements from HTML with a CSS selector; returns their text, HTML or an attribute.
- [extract_links](/ops/extract_links) (html → table): List the links in HTML as rows of {text, href}, with absolute URLs.
- [extract_tables](/ops/extract_tables) (html → any): Turn HTML <table> elements into rows keyed by column header.
- [extract_meta](/ops/extract_meta) (html → json): Get page metadata: title, description, canonical URL, language, Open Graph, and JSON-LD.

### structure

- [outline](/ops/outline) (html, text → table): Show the heading structure of HTML or markdown, so you can see what's in a document before reading it.

### network

- [fetch](/ops/fetch) (no input → any): Download a public web page or API response. Returns html, text or json, with source URL, status and time recorded as provenance.

### text

- [find](/ops/find) (text, html → table): Search text for a word, phrase or regex and return each match with its position and surrounding context.
- [contains](/ops/contains) (text, html, list → boolean): Check whether text (or any item in a list) contains a word, phrase or regex. Returns true or false.
- [slice](/ops/slice) (text, html, list → any): Take part of text (by chars, lines, words or approximate tokens) or part of a list (by items). Negative indexes count from the end.
- [fit](/ops/fit) (text, html, list → any): Shrink text or a list to fit a token budget, marking what was cut. Use before reading anything of unknown size.
- [chunk](/ops/chunk) (text, html → list): Split long text into pieces under a token budget, breaking at paragraphs, lines or sentences where possible.
- [replace](/ops/replace) (text, html → same): Replace occurrences of a word, phrase or regex in text.
- [split](/ops/split) (text, html → list): Split text into a list by lines, paragraphs, sentences, words or a separator.
- [regex_extract](/ops/regex_extract) (text, html → any): Pull every regex match out of text. One capture group gives a list of strings; several give rows.
- [normalize](/ops/normalize) (text → text): Tidy text: collapse whitespace, drop blank-line runs, optionally lowercase or Unicode-normalize.
- [count](/ops/count) (text, html, list, json → number): Count chars, words, lines, estimated tokens, items (list entries or object keys), or pattern matches.
- [diff](/ops/diff) (text, html → json): Compare text against another text; returns counts plus a unified diff.

### integrity

- [hash](/ops/hash) (text, html, json, list, table, number, boolean → text): Fingerprint any value (sha256 by default) to check whether content changed or to cite it verbatim.

## Errors

Always {"error": {code, detail, suggestion, links}}. The suggestion usually names the exact fix.

## Policy

Privacy and acceptable use: /privacy. Contact: extcord@proton.me
