patternfetch API & MCP reference
Base URL https://patternfetch.com · REST + MCP · pay-per-call with x402 (USDC) or Stripe · machine-readable OpenAPI & llms.txt.
Quickstart
# 0) no-signup demo — a real brief, no key
curl -X POST https://patternfetch.com/v1/demo \
-H "Content-Type: application/json" \
-d '{"ticker":"BTC/USDT","timeframe":"4h"}'
# 1) free key ($3.00 starter credit)
curl -X POST https://patternfetch.com/v1/keys \
-H "Content-Type: application/json" -d '{"email":"you@example.com"}'
# 2) a brief
curl -X POST https://patternfetch.com/v1/brief \
-H "Authorization: Bearer pf_…" -H "Content-Type: application/json" \
-d '{"ticker":"BTC/USDT","timeframe":"4h"}'
Authentication
All paid endpoints take a bearer key: Authorization: Bearer pf_…. Get one free (below). Agents can also connect over MCP with OAuth — no key to paste. Keys are shown once; store them. Every response carries an X-Disclaimer header.
① No-signup demo POST /v1/demo returns a real brief (rate-limited, never billed). ② Free MCP discovery — agents can initialize and tools/list with no key; only tools/call needs auth. ③ Free key with $3.00 starter credit (300 briefs) from one call, no card. See pricing.
POST /v1/brief POST $0.01
The full token-compact market-state brief for a ticker + timeframe.
Request
{ "ticker": "AAPL", "timeframe": "1d", "limit": 200 }
{ "ticker": "BTC/USDT", "timeframe": "4h" }
| Field | Type | Notes |
|---|---|---|
| ticker | string | required · stock, ETF, or crypto ticker, e.g. AAPL, SPY, BTC/USDT |
| timeframe | string | required · one of 1m 5m 15m 30m 1h 4h 1d 1w |
| limit | int | optional · candles to analyze (default 200, min 20) |
Response
{
"header": { "sym":"BTC/USDT","tf":"4h","src":"binance","n":200,
"t0":1747094400000,"t1":1747958400000 },
"codec": { "rows":"60125.4,60480,59890.1,60310.7,1284;…",
"sax":"dcefdcbe", "precision":1 },
"analysis": {
"patterns":[{"name":"double_bottom","confidence":0.86,"at":158,"span":18,
"evidence":{"scope":"major crypto pairs","tf":"4h","band":"0.75-1.00","horizon":10,"n":521,"hitRate":0.505,"ci95":0.043,
"dist":{"winRate":0.505,"median":0.001,"min":-0.199,"max":0.211},
"definition":"realizable gross directional base rate over the 10 bars after the pattern is knowable (chart patterns: +2 confirming bars, no lookahead). No stop/fees. Not a prediction."}}],
"levels":{ "support":[{"price":59820.4,"strength":1}],
"resistance":[{"price":63450.8,"strength":1}] },
"regime":{ "trend":"up","strength":0.42,"volPct":2.13 },
"indicators":{ "rsi":{"v":58.34,"state":"neutral"},
"ema":{"v":61240.77,"state":"above_20_50"} },
"nl":"BTC/USDT: uptrend (moderate), +1.94% last 4h, RSI 58.34 (neutral), double_bottom (conf 0.86, n=18, hist 51% over 10b)."
},
"disclaimer":"… informational only, not advice …",
"methodologyUrl":"https://patternfetch.com/methodology"
}
codec.rows = compact candles o,h,l,c,v per row (no per-row timestamp; the series is bound by header.t0/t1). codec.sax = an a–g shape fingerprint. Pattern names: bullish_engulfing, bearish_engulfing, hammer, doji, double_top, double_bottom, head_and_shoulders. Each directional pattern may also carry an evidence block — the backtested historical base rate (hitRate, sample n, ci95, full dist) for that pattern+timeframe+confidence-band, so your agent can weight the geometric score instead of trusting it. See methodology for exactly how each field is computed.
How to read hitRate. A hit rate means nothing on its own; it only means something next to the pattern-free baseline for the same market, timeframe and horizon — the rate at which any window of that length closes in that direction with no pattern involved. US equities drift upward, so that baseline is not 50%: over 10 bars it is 57.8% up on the daily, 61.2% on the weekly, 54.0% on 4h and 52.7% on 1h (117 US stocks & ETFs, non-overlapping samples). In crypto there is no comparable drift, so the baseline genuinely does sit near 50% — 49.3% up on the daily, 49.7% on 4h, 50.5% on 1h across 10 major pairs — and a crypto pattern that lands there is matching its baseline, not beating it. The gap between hitRate and that baseline (the lift) is the only quantity worth acting on, and ci95 tells you whether the gap is separable from noise.
What the current corpus shows. Measured this way as of 2026-07-18, three of the 105 buckets we tested clear an uncorrected interval — and all three are crypto: bullish_engulfing 1h −1.76pp (±1.36, n=8,897), double_bottom 1d −12.25pp (±8.91, n=143) and double_top 1d +9.78pp (±8.42, n=149) — the last of these the only bucket of the 105 that lands above its baseline. That is fewer than the ~5.3 you would expect from chance alone at this threshold, and none of the three survives a Bonferroni correction across 105 comparisons (which needs z > 3.49; the largest observed is 2.69). On US stocks & ETFs it is 0 of 60. On the daily stock buckets: bullish_engulfing 58.2% vs a 57.8% baseline (+0.4pp, ±2.6pp, n=11,535), hammer 57.7% vs 57.8% (−0.1pp, ±2.7pp, n=5,600), bearish_engulfing 41.3% vs 42.0% (−0.7pp, ±2.6pp, n=12,842), head_and_shoulders 42.0% vs 42.0% (+0.0pp, ±3.1pp, n=2,795). Intervals are cluster-robust by calendar day: treating ~117 tickers that move together on one day as independent samples made earlier intervals 2.1×–2.7× too narrow on the high-volume daily buckets. We publish this because your agent should size a pattern by its lift, not by a number that looks impressive in isolation. The rest of the brief — regime, levels, interpreted indicators, the compact codec — is unaffected.
Assets & sources: one schema covers US stocks, ETFs and crypto — crypto from Binance (realtime), stocks & ETFs from Yahoo (split & dividend adjusted, delayed/EOD). header.src reports which source produced the series.
POST /v1/multi POST $0.025
One call, several timeframes side by side, plus a cross-timeframe alignment read — do the trends agree across horizons, or split? The signal people care about is usually the divergence between timeframes, not any single one. Impersonal, descriptive — the divergence is exposed as data, never a directive.
Request
{ "ticker": "BTC/USDT", "timeframes": ["1h","4h","1d"], "limit": 200 }
| Field | Type | Notes |
|---|---|---|
| ticker | string | required · stock, ETF, or crypto ticker, e.g. AAPL, SPY, BTC/USDT |
| timeframes | string[] | optional · default ["1h","4h","1d"] · max 4, deduped, shortest→longest |
| limit | int | optional · candles per timeframe (default 200, min 20) |
Response
{
"sym":"BTC/USDT",
"views":[ {"tf":"1h","trend":"down","strength":0.51,"nl":"BTC/USDT: downtrend …"},
{"tf":"4h","trend":"down","strength":1,"nl":"…"},
{"tf":"1d","trend":"down","strength":1,"nl":"…"} ],
"alignment":{ "trends":{"1h":"down","4h":"down","1d":"down"},
"aligned":true, "direction":"down", "divergence":null },
"nl":"BTC/USDT: trend aligned down across 1h, 4h, 1d.",
"briefs":[ { …a full /v1/brief object per timeframe (patterns, evidence, levels, regime, indicators)… } ],
"disclaimer":"… informational only, not advice …"
}
alignment.aligned is false only when one timeframe is up while another is down; ranging timeframes never force a conflict. When they split, divergence spells it out, e.g. "1h up / 4h up / 1d down". Each entry in briefs is a full brief (so every pattern still carries its backtested evidence). Bundles ~3 briefs cheaper than 3 separate calls. No-signup demo: POST /v1/demo-multi returns a real multi-timeframe view with no key (rate-limited, never billed).
POST /v1/delta POST $0.008 · $0.001 empty
Only what changed since your last brief for this ticker+timeframe (trend flips, new patterns, RSI-state changes). Ideal for polling agents — when nothing material moved it returns changed:false and is billed at the cheaper empty rate.
// request
{ "ticker":"BTC/USDT", "timeframe":"4h" }
// changed → includes the full current brief
{ "delta": { "changed":true, "sym":"BTC/USDT", "tf":"4h",
"notes":["trend range -> up","new pattern(s): double_bottom"] },
"brief": { /* same shape as /v1/brief */ } }
// nothing changed → no brief, billed $0.001
{ "delta": { "changed":false, "sym":"BTC/USDT", "tf":"4h", "notes":[] } }
POST /v1/candles POST $0.005
The compact candle codec only — no analysis. Cheapest way to get raw-ish OHLCV in a token-efficient form.
{ "header":{ "sym":"BTC/USDT","tf":"4h","src":"binance","n":200 },
"codec":{ "rows":"60125.4,60480,59890.1,60310.7,1284;…","sax":"dcefdcbe","precision":1 },
"disclaimer":"…" }
POST /v1/analogs POST $0.05
Historical windows whose shape resembles the current price action, and the full distribution of what followed over a fixed forward horizon (winners and losers). Not a prediction, not a strategy backtest.
// request
{ "ticker":"BTC/USDT", "timeframe":"4h", "window":32, "horizon":20 }
// response
{ "sym":"BTC/USDT","tf":"4h","window":32,
"distribution":{ "n":18,"horizon":20,"winRate":0.6111,
"median":0.0214,"mean":0.0307,"min":-0.0892,"max":0.1645 },
"matches":18,
"note":"Full outcome distribution incl. losers. Not a prediction.",
"disclaimer":"…" }
Returns (e.g. median) are fractions: 0.0214 = +2.14%. window and horizon default to 32 / 20, range [2,500].
POST /v1/scan POST $0.02
The screener — discovery, not lookup. Instead of naming one ticker, filter the whole universe and get back the tickers currently in a given regime or printing a chart/candlestick pattern, ranked by the measured base rate (with its cluster-robust 95% CI, sample size and the pattern-free baseline for the same market, timeframe and horizon) — so your agent narrows the universe in one call instead of being handed one ticker at a time. Precomputed daily over a curated universe (liquid US large-caps + core/sector ETFs + major crypto pairs), so it's fast and cheap. Every filter is optional.
Request
{ "assetClass": "all", "regime": "up", "minLift": 0.02, "limit": 20 } // only patterns beating their own baseline by 2pp+
{ "pattern": "double_bottom", "assetClass": "stock" }
{} // no filters -> top of the universe, ranked
| Field | Type | Notes |
|---|---|---|
| assetClass | string | optional · stock | crypto | all (default all) |
| regime | string | optional · up | down | range — match the ticker's current regime trend |
| pattern | string | optional · require the top pattern to be this one, e.g. double_bottom, double_top, head_and_shoulders, bullish_engulfing, bearish_engulfing, hammer |
| tf | string | optional · one of 1m 5m 15m 30m 1h 4h 1d 1w (the universe is currently precomputed at 1d) |
| minLift | number | optional · −1..1 · the drift-free filter — prefer this one. Drop tickers whose top pattern doesn't beat its own pattern-free baseline by at least this many rate points (0.02 = 2pp, 0 = at or above baseline). Rows with no baseline in the evidence table are always excluded — absence of a lift is not a lift of 0. |
| minBaseRate | number | optional · 0..1 · drop tickers whose top-pattern base rate is below this. Careful: a raw base rate isn't comparable across bullish and bearish rows, so this mostly returns bullish patterns in a rising market before any of them carries information. Kept for existing consumers; use minLift for an unbiased shortlist. |
| limit | int | optional · 1..500 · max rows (default 50) |
Which threshold should you filter on? minBaseRate asks “how often did this pattern resolve in its direction?”. minLift asks “how much of that was the pattern?”. On the daily US stock table a bullish pattern starts roughly 16pp ahead of a bearish one before either carries any information, so a base-rate floor is largely a drift filter wearing a signal's clothes. minLift is the one that survives that objection — and across the 105 buckets we tested, it is also the one that returns almost nothing, which is the honest result.
Response
{
"asOf": "2026-07-05", "tfs": ["1d"], "universe": 137, "count": 2,
"results": [
{ "sym":"AAPL","tf":"1d","assetClass":"stock","regime":"up","pattern":"double_bottom",
"baseRate":0.58,"ci95":0.03,"n":300,"scope":"US stocks & ETFs","confidence":0.72 },
{ "sym":"BTC/USDT","tf":"1d","assetClass":"crypto","regime":"down","pattern":"double_top",
"baseRate":0.55,"ci95":0.04,"n":150,"scope":"major crypto pairs","confidence":0.81 }
],
"note":"Precomputed current-state scan ranked by impersonal historical base rate. Discovery, not a prediction or a buy/sell signal.",
"disclaimer":"…"
}
Each row: sym, tf, assetClass, regime, pattern, baseRate (the ranking key — the same backtested directional hit rate the brief attaches to a pattern), ci95, n, scope (the asset-class corpus the rate was measured over), and geometric confidence. Rows are ranked by baseRate desc, then confidence, then tf, then sym; rows whose top pattern has no sufficiently-powered evidence bucket carry baseRate: null and rank last. Same discipline as the brief: baseRate is only interpretable against the pattern-free baseline for its scope, timeframe and horizon, which is why scope travels with every row — US stocks & ETFs close up 57.8% of the time over 10 daily bars with no pattern involved, crypto about 49.3%, so equal baseRate values across asset classes are not equal findings. Typical workflow: scan → then call /v1/brief on the shortlist. Invalid filter values return INVALID_PARAM (400). Over MCP this is the patternfetch_scan tool (same filters + ranked output). Full landing page: the screener →.
POST /v1/demo POST free
A real brief with no key — the same body and output as /v1/brief. IP rate-limited; never billed. Great for a first taste and for the live playground.
POST /v1/keys POST free
curl -X POST https://patternfetch.com/v1/keys \
-H "Content-Type: application/json" -d '{"email":"you@example.com"}'
// -> { "key":"pf_…", "disclaimer":"…" } (key shown once; $3.00 credit granted)
The key is shown once and we never store it — only a hash. Signing up again with the same address does not return your old key; it creates a separate empty account, because the starter credit is granted once per address. If you lose your key, use /v1/recover.
POST /v1/recover POST free
Lost your key? We cannot re-send it — only a hash of it is ever stored. This mails a single-use link to the address on the account; opening that link issues a new key and shows it once.
curl -X POST https://patternfetch.com/v1/recover \
-H "Content-Type: application/json" -d '{"email":"you@example.com"}'
// -> { "ok":true, "message":"If that address has a patternfetch account, a recovery link is on its way." }
Then open the link from the mail: GET /v1/recover?token=… — it renders your new key.
- Previous keys are deactivated. Opening the link revokes every earlier key on the account, so update anything still using the old one. Your credit balance is not affected.
- Nothing happens until you open the link. If you did not request it, ignore the mail — your existing key keeps working.
- Single-use, and it expires (7 days by default). The new key is displayed only on that page, so store it immediately.
- The response never tells you whether an address is registered — it is byte-identical either way, so this endpoint cannot be used to probe for accounts.
- Rate-limited per IP. On a deployment with no mail provider configured the endpoint returns 503 NOT_CONFIGURED rather than handing a key to an unverified caller. That is not a transient failure — retrying won't help until a mail provider is configured.
GET /v1/platforms · GET /v1/status GET free
/v1/platforms returns the capability matrix (assets, timeframes, endpoints, limits). /v1/status returns a small health object. /healthz is a bare liveness check. None require a key or are geoblocked.
MCP server
Streamable HTTP at POST https://patternfetch.com/mcp. Tools: patternfetch_brief, patternfetch_multi, patternfetch_delta, patternfetch_analogs, patternfetch_scan, patternfetch_capabilities. Discovery (initialize, tools/list) is free; a tools/call needs a key (via OAuth or a Bearer header) and is billed like the matching REST endpoint.
{
"mcpServers": {
"patternfetch": {
"url": "https://patternfetch.com/mcp",
"headers": { "Authorization": "Bearer pf_…" }
}
}
}
Claude Desktop / Cursor over a local bridge: npx mcp-remote https://patternfetch.com/mcp --header "Authorization: Bearer pf_…". One-liner: claude mcp add --transport http patternfetch https://patternfetch.com/mcp --header "Authorization: Bearer pf_…".
Use with OpenAI / function-calling agents
If your agent runs on the OpenAI (or any OpenAI-compatible) chat API, expose patternfetch as a tool / function: the model decides when to call it, your code fulfills the call with one POST. You do not point your base_url at patternfetch — it is a data API, not a chat model. Drop this schema into your tools=[…]:
{
"type": "function",
"function": {
"name": "get_market_brief",
"description": "Token-compact market-state brief for a stock, ETF, or crypto ticker: candles (codec), chart patterns with backtested base-rate evidence, support/resistance, regime and indicators. Descriptive, never a trade directive.",
"parameters": {
"type": "object",
"properties": {
"ticker": { "type": "string", "description": "stock, ETF, or crypto ticker, e.g. AAPL, SPY, BTC/USDT" },
"timeframe": { "type": "string", "enum": ["1m","5m","15m","30m","1h","4h","1d","1w"] },
"limit": { "type": "integer", "description": "candles to analyze (default 200)" }
},
"required": ["ticker","timeframe"]
}
}
}
Fulfill the call by forwarding the arguments to POST /v1/brief and handing the JSON back to the model:
import json, requests
from openai import OpenAI
client = OpenAI()
TOOLS = [ /* the schema above */ ]
def get_market_brief(ticker, timeframe, limit=200):
r = requests.post("https://patternfetch.com/v1/brief",
headers={"Authorization": "Bearer pf_…"},
json={"ticker": ticker, "timeframe": timeframe, "limit": limit})
return r.json()
msgs = [{"role": "user", "content": "Is BTC/USDT stretched on the 4h?"}]
resp = client.chat.completions.create(model="gpt-…", messages=msgs, tools=TOOLS)
call = resp.choices[0].message.tool_calls[0]
brief = get_market_brief(**json.loads(call.function.arguments))
msgs += [resp.choices[0].message,
{"role": "tool", "tool_call_id": call.id, "content": json.dumps(brief)}]
final = client.chat.completions.create(model="gpt-…", messages=msgs, tools=TOOLS)
For several timeframes in one call, add a second tool that posts to /v1/multi with a timeframes array. Native agents should prefer the MCP server (auto-discovery + OAuth, no key handling) — this function-calling path is for code that already speaks the OpenAI tool protocol.
OAuth — one-click connect (no key to paste)
patternfetch implements OAuth for MCP, so Smithery, Claude.ai, Cursor and Claude Desktop can connect with zero manual key handling: add the server URL, click Authorize once, and patternfetch mints a free-tier key for you behind the scenes.
| Discovery / endpoint | What it is |
|---|---|
| /.well-known/oauth-protected-resource | RFC 9728 — points clients at the auth server |
| /.well-known/oauth-authorization-server | RFC 8414 — endpoints + PKCE (S256) support |
| POST /oauth/register | RFC 7591 dynamic client registration |
| GET/POST /oauth/authorize | consent → mint free key → auth code |
| POST /oauth/token | PKCE exchange → access token (a pf key) |
In Claude.ai: Settings/Customize → Connectors → Add custom connector → paste https://patternfetch.com/mcp → Authorize. The unauthenticated /mcp 401 also carries a WWW-Authenticate header so compliant clients discover OAuth automatically.
Billing & top-up
Calls are metered in micros (millionths of USD). When credit runs out, paid calls return PAYMENT_REQUIRED. Top up two ways:
- x402 — POST /v1/topup with a USDC (Base) payment. No account, no human; an agent settles it on its own. Discoverable at /.well-known/x402.json.
- Stripe — POST /v1/checkout (with your key) returns a hosted checkout URL.
Errors
Errors are JSON: { "error":"CODE", "message":"…" } with a matching HTTP status.
| Code | HTTP | Meaning |
|---|---|---|
| INVALID_PARAM | 400 | bad/missing ticker, timeframe, email, or value out of range |
| UNAUTHORIZED | 401 | missing or invalid bearer key |
| PAYMENT_REQUIRED | 402 | insufficient credit — top up via x402 or Stripe |
| GEO_BLOCKED | 451 | crypto data not available in your region |
| RATE_LIMITED | 429 | too many requests (or demo limit reached) |
| INSUFFICIENT_DATA | 422 | fewer than 20 candles available for that pair/timeframe |
| NOT_FOUND | 404 | no such route |
| UPSTREAM_ERROR | 502 | market-data source error |
| NOT_CONFIGURED | 503 | the feature is not configured on this deployment (e.g. key recovery with no mail provider) — permanent until an operator changes it, so don't retry |
Rate limits & notes
- Per-key rate limit on paid endpoints; demo is limited per IP. Back off on 429.
- Assets: US stocks, ETFs and crypto — one schema. Sources: crypto via Binance (realtime); stocks & ETFs via Yahoo (split & dividend adjusted, delayed/EOD). Timeframes: 1m–1w (1m 5m 15m 30m 1h 4h 1d 1w).
- Request body cap: 64 KB. All responses include the X-Disclaimer header.