AnjomanBeta
17 min readUpdated October 27, 2026

Engine Inference

The Engine is Telegraph's inference coordinator. You send it a question; it picks a miner, calls that miner, and returns the answer with routing and cost metadata. You never manage miner endpoints, API keys, or schemas yourself.

There are three ways to ask:

  • Auto-routed, plain language. You describe what you want and the Engine classifies it and selects the best miner.
  • Auto-routed, canonical. You name the intent and send typed params. The node validates them before anything is charged.
  • Direct. You already know which miner you want, so you skip routing and call it by ID, with either the miner's native request or the same canonical intent + params.

All of them are paid per call, via x402 or from your escrow. All of them run fresh, on-demand inference, not reads from a cache.

The Engine is not the Daemon. The Engine answers your questions on demand. The Daemon is a separate background service that generates its own questions on a schedule and stores them for the dashboard, see Daemon Signal Feed. If you want a live answer to your own question, you want the Engine.

Engine base URL (testnet): https://devnode.telegraphprotocol.com/engine

A note on naming: parts of the Telegraph API still use subnet in field names (subnet_id, subnet_name on the Daemon and WebSocket signal feeds) and tool names (the tg_engine_*_subnet MCP tools). This is legacy naming from when every provider was a Bittensor subnet. Today any provider integrated via YAML is a miner. Read subnet as "miner" wherever you see it.

Auto-Routed Ask

Send a natural-language query and let the Engine choose the miner:

POST /engine/v1/ask
Content-Type: application/json

{
  "query": "What is the current weather in London?"
}
FieldRequiredDescription
queryYes, unless you send intent + paramsNatural-language question. The router classifies it into an Intent and picks a miner.
intentWith paramsThe Intent the request is for. See Canonical ask.
paramsWith intentThe request's typed params, as the Intent's input schema defines them.
contextNoObject merged into the routed request body. Use it to pass structured hints the miner understands. For an Intent with an input schema, keys that match the schema's params also stand in for params.

The endpoint is gated: your first request returns a 402 Payment Required challenge. Complete the payment and retry exactly as described in Paying with x402, or pay from escrow with scheme: "escrow".

Response (200):

json
{
  "miner_id": "41",
  "miner_name": "fx-frankfurter",
  "endpoint": "/latest",
  "result": { "rate": 0.8712, "base": "USD", "quote": "EUR" },
  "result_shape": "intent",
  "format_version": 1,
  "field_status": { "rate": "ok", "base": "ok", "quote": "ok" },
  "input": { "base": "USD", "quote": "EUR" },
  "input_schema_version": 2,
  "cost_usd": 0.01,
  "duration_ms": 412,
  "timestamp": "2026-10-26T19:24:42Z",
  "reasoning": "Query asks for a live FX rate.",
  "intent": "FX_NOW_EUR",
  "signal_hash": "0x7a44569d..."
}
FieldMeaning
miner_id / miner_nameThe miner that served the request (numeric ID and slug). May be the fallback rather than the router's first pick.
endpointThe upstream path that was called.
resultThe answer. When the node can read it into the Intent's response format, result has the same keys, in the same order, whichever miner answered (result_shape is intent). Otherwise it is the miner's raw output (result_shape is raw). See The answer's shape.
result_shapeintent or raw.
shape_errorWhy result is raw. Only present when result_shape is raw.
format_versionThe version of the Intent's response format. Omitted when the Intent has no format.
field_statusThe status of each field in result, in the format's order. Only present when result_shape is intent.
inputThe request's params as validated: types normalised, dates in ISO form, defaults applied. Omitted when the Intent has no input schema.
input_schema_versionThe version of the input schema, present with input.
cost_usdWhat the call cost, in USD (a number).
duration_msExecution time in milliseconds. It excludes routing and shaping.
reasoningWhy the router chose this miner. Omitted when empty.
intentThe Intent the request was matched to.
signal_hashThe hash the raw miner answer was recorded under. Look it up at GET /engine/v1/signal/{hash}. Omitted when the ask failed.
warningsAdvisory notes, for example your body is larger than the miner declared it accepts. The request still ran. Omitted when empty.

Changed in V2. Before V2, result was always the miner's raw output and a separate parsed object held any extracted fields. parsed is gone; the same information is now result. If your code reads the miner's own JSON out of result, read the Intent's fields instead, or branch on result_shape == "raw".

Errors before payment

These are answered before any payment is taken, and nothing is charged:

StatusBodyMeaning
400{"error": "invalid request body"}The body is not one JSON object with a non-empty query, or an intent with params. A message field says which.
400{"error": "invalid_params", ...}params do not pass the Intent's input schema. See Canonical ask.
400{"error": "no_input_schema", ...}You sent params for an Intent that has no input schema. Send a free-form query.
429{"error": "too many requests, slow down"}One client asked about too many new questions in a minute (default 60). A question quoted in the last 3 minutes is never refused this way. Wait and retry.
503{"error": "extraction_unavailable", ...}The node needed its parser model to read or complete your params and it is off, down or slow. Retry, or send intent + params.
503{"error": "no_compatible_miner", ...}No live miner can take the params, or the Intent you named is served by no live miner right now. It is never served by another Intent's miner.
503{"error": "could not route this question now, try again later"}No miner can take the question now.

The answer's shape

Different miners for the same Intent answer in different formats. So that your code reads one shape, a node that runs the response parser reads the miner's answer into the Intent's response format and returns it as result. Every field of the format is read from the answer and the keys are the same whichever miner answered.

A format is an ordered list of fields, each with a name, a description, a type (number, integer, string, enum, bool, date, datetime, list, object) and, where relevant, a unit, a plausible range, the allowed values of an enum, or the texts that mean true and false. Read an Intent's format from GET /engine/v1/intents/{intent} (response.fields).

How a field is read: the parser model copies the value, and code rejects anything not present word for word in the answer, converts units (an inverse FX quote is turned round), checks ranges, and maps enum and bool values.

A field marked processed is not copied but worked out for your request: a total over your window, a maximum, whether a reading crosses your threshold. The model must cite the values it used, and code checks that at least one is written in the answer itself.

json
{
  "result": { "rate": 0.8712, "base": "USD", "quote": "EUR" },
  "result_shape": "intent",
  "format_version": 1,
  "field_status": { "rate": "ok", "base": "ok", "quote": "ok" }
}
field_status valueMeaning
okThe value was read from the answer.
missingThe answer has no such value. The field is null.
rejectedThe model's reply was unusable: not present word for word in the answer, the wrong type, out of range, or not an allowed value. The field is null.
unavailableThe parser did not answer in time or is down.

The keys are always all present, in the format's order.

When the node cannot produce the shape, result is the miner's raw output, result_shape is raw, and shape_error says why:

shape_errorMeaning
parser_offThe node does not run the parser.
no_formatThe Intent, and the family it inherits from, has no response format.
parser_unavailableThe parser did not answer for any field (down, timed out, or the answer was too long).
no_fields_foundThe parser answered, but no field could be read from the answer.
answer_unreadableThe miner's answer could not be rendered as text for the parser.

Shaping never changes the status code or what you pay: a raw fallback still returns 200 and still settles. What is stored and signed is always the miner's raw answer, and signal_hash covers it, so the shaped result is a convenience view of it.

Some miners declare an intent_mapping in their YAML. Then the node reads their answer through that mapping: each field is found at a declared path and numbers are computed in code (unit conversion, a maximum over a series, a formula) instead of by the model. The reply is the same shape either way.

Canonical ask: intent + params

Every Intent has one request schema: the params a request for it carries. It is the same whichever miner serves the Intent, and the same off-chain and on-chain. Read it first:

bash
curl https://devnode.telegraphprotocol.com/engine/v1/intents/WEATHER_CHECK
json
{
  "intent_id": "WEATHER_CHECK",
  "class": "deterministic",
  "request": {
    "schema_version": 3,
    "params": [
      { "name": "location", "type": "string", "required": true, "description": "Place the reading is for" },
      { "name": "days", "type": "integer", "min": 1, "max": 7, "default": 1, "description": "How many days" }
    ]
  },
  "response": { "format_version": 3, "fields": [ { "name": "temp_c", "type": "number", "unit": "degrees Celsius" } ] },
  "onchain": { "ready": true }
}

Then send it:

bash
curl -X POST https://devnode.telegraphprotocol.com/engine/v1/ask \
  -H "Content-Type: application/json" \
  -H "PAYMENT-SIGNATURE: <base64-payment-payload>" \
  -d '{"intent": "WEATHER_CHECK", "params": {"location": "Tokyo"}}'

What a param can say

KeyOnMeaning
nameevery paramUnique, [A-Za-z_][A-Za-z0-9_]*
typeevery paramstring, integer, number, bool, enum, date (YYYY-MM-DD), datetime (RFC 3339 with a zone, returned in UTC)
requiredanyMust be present and non-empty. "" counts as missing. An optional param sent as "" is treated as absent.
descriptionanyWhat it means
allowedenumThe values. Matched case-insensitively, returned in the schema's spelling.
min / maxinteger, numberInclusive range
patternstringA regular expression the whole value must match
unitinteger, numberThe value's unit
defaultoptional paramsApplied when the param is absent
examplesanyFor people

Validation is deterministic. Coercion is only ever safe: "3" is read as the integer 3, "five" is a type error, 2.5 is never an integer, "true" and "false" are bools, "yes" is not. Params the schema does not have are refused (unknown), never silently dropped. Limits: the whole params object at most 16 KiB of JSON, a string at most 2000 characters, a list at most 100 items.

How the node completes your request

  1. intent + params that cover every required param: validated in code. No model is called.
  2. A free-form query only: the parser model reads exactly the schema's params out of the question. It never guesses; anything the question does not state is null, and a free-text value that is not in the question is discarded. Then the same validation runs.
  3. params that are valid but incomplete, plus a query: your supplied values are validated first (a wrong type or range is refused before any model is called), then the model is asked once for the missing params with your values as known facts. Your values are kept.
  4. A value in the query that differs from a supplied param is refused as a conflict, naming both values. The node never overrides what you sent silently.
  5. context: keys that match the schema stand in for params but never override explicit params. The whole context is still merged into the miner request as before.
  6. Defaults: only the schema's own defaults are applied. A required value is never invented.

Whether a free-form ask is enforced against the schema is set by the node operator (INPUT_SCHEMA_MAPPING: off, shadow or on). Explicit params are always validated.

When params are refused

The check runs before any miner is chosen, called or paid. Nothing is charged, no payment authorization is spent and nothing is recorded.

json
{
  "error": "invalid_params",
  "intent": "WEATHER_CHECK",
  "schema_version": 3,
  "problems": [
    {"param": "location", "code": "missing", "message": "location is required (string)"},
    {"param": "days", "code": "range", "message": "days: 9 is outside 1 to 7"},
    {"param": "colour", "code": "unknown", "message": "unknown param \"colour\"; this intent takes: location, days"},
    {"param": "date", "code": "conflict", "supplied": "2026-10-24", "stated": "2026-10-25",
     "message": "date: params say \"2026-10-24\" but the query says \"2026-10-25\""}
  ],
  "message": "location is required (string); days: 9 is outside 1 to 7; ..."
}
codeMeaning
missingA required param is absent or empty
typeWrong type
enumNot one of the allowed values
rangeOutside min/max
patternDoes not match the pattern
unknownThe schema has no such param
sizeThe whole object is over its size limit (param is "")
conflictparams or context disagree with the query (adds supplied and stated)

Direct Ask

When you already know which miner you want, call it by ID and skip routing. There are two request forms. Pick one; mixing them is refused with 400.

Native form: the miner's own request

You supply the miner's endpoint and payload yourself:

POST /engine/v1/ask/{minerId}
Content-Type: application/json

{
  "method": "POST",
  "endpoint": "/chat",
  "payload": {
    "messages": [{ "role": "user", "content": "Explain Bitcoin in one sentence." }]
  }
}
FieldRequiredDescription
methodYesOne of GET, POST, PUT, PATCH, DELETE.
endpointYesThe upstream path, for example /chat. Must match what the miner's YAML declares.
payloadYesSent as the request body, or as query parameters when method is GET.
acknowledge_warningsNoRun the request even when the node predicts it will fail. See Pre-request validation.

The reply is the miner's raw output. It carries result_shape: "raw" to say so, and has no reasoning or intent.

json
{
  "miner_id": "104",
  "miner_name": "litellm",
  "result": { "...miner's raw output..." },
  "result_shape": "raw",
  "cost_usd": 0.01,
  "duration_ms": 650,
  "timestamp": "2026-10-26T19:24:42Z",
  "signal_hash": "0x7a44569d..."
}

Canonical form: intent + params on one miner

Send the same typed request you would send to /ask, but to a miner you chose:

bash
curl -X POST https://devnode.telegraphprotocol.com/engine/v1/ask/wx-openmeteo \
  -H "Content-Type: application/json" \
  -H "PAYMENT-SIGNATURE: <base64-payment-payload>" \
  -d '{"intent": "WEATHER_CHECK", "params": {"location": "Tokyo"}}'
FieldRequiredDescription
intentUnless the miner serves exactly one IntentThe Intent. The miner must serve it.
paramsUnless a query or context completes itThe Intent's typed params. Same rules as above.
queryUnless params are completeA free-form question that completes missing params.
contextNoAs on /ask.
acknowledge_warningsNoAs in the native form.

The node maps your params onto that one miner's own API and calls it. There is no fallback. The reply has the same structure as the routed ask: result in the Intent's shape, result_shape, field_status, input, intent, endpoint (the one the mapping chose), signal_hash. Two miners with different native APIs answer the same Intent in the same structure.

Errors specific to this form, all before payment:

StatusBodyMeaning
400intent_required (with intents: [...])The miner serves several Intents and you named none.
400invalid_params, no_input_schemaAs above.
404miner_not_foundThe miner is not registered.
429too many requests, slow downSame limit as /ask.
503no_compatible_minerThe miner does not serve the Intent, or none of its endpoints can take the required params.
500Upstream failureNo other miner is tried. The body includes miner_id and intent.

Replace {minerId} with a miner ID or slug from the discovery endpoint. Miner IDs are not stable across time, so always take them from discovery rather than from an example.

Rate limits are counted per miner across the whole node, so a warnings entry about a rate limit usually means switching to another miner for the same Intent works better than retrying the same one.

Pre-request validation

Before calling the miner, the node checks your request against the constraints that miner declared in its YAML: body and parameter sizes, value and count limits, and whether its call-rate allowance is already spent.

On the direct path this stops the request, because you named the miner and nothing else will be tried:

json
{
  "error": "request is predicted to fail",
  "warnings": ["miner \"zeus\" has reached its rate limit; try another miner for this intent, or try again later"],
  "proceed_anyway": {
    "field": "acknowledge_warnings",
    "value": true,
    "note": "resend with this field set to run the request regardless; you are charged only if it runs"
  }
}

That comes back as 422, and you are not charged. Your options:

  1. Pick another miner for the same Intent (usually right for rate limits).
  2. Fix the request, if the warning is about your payload.
  3. Resend with "acknowledge_warnings": true to run it anyway.

On the auto-routed path nothing is ever blocked. A fallback is already lined up behind the primary miner, so warnings are attached to the response instead.

The payment gate runs first

Payment is checked before your request is validated, except for the input checks above. An unregistered miner ID or a malformed native body can still come back as 402 rather than 404 or 400. You are not charged for it, since payment settles only on success, but you cannot probe an endpoint's shape for free. Check your miner ID and endpoint against /api/miners before you build the request.

Listing Available Miners

GET /api/miners

Returns a JSON array of every registered miner: numeric id, slug, name, endpoints (path, method, and schema), supported_intents, activation_status, and min_price_usdc. Filter server-side instead of fetching the whole catalog: ?intent=, ?status=, and &limit= are supported. See Discover Available Miners for the full field reference.

Available Intents

See Intents for how prefixes and sub-intents work, how each is scored, and how to propose one. To read the live set:

bash
curl https://devnode.telegraphprotocol.com/engine/v1/intents

Streaming over WebSocket

The same ask and ask_direct operations, including intent + params, are available over the Engine's WebSocket interface, which streams routing progress as events instead of returning a single response. See WebSocket Signals.

When to Use What

ScenarioUse
You aren't sure which rail you want at allChoosing an Inference Path
You want the best miner picked automatically from a plain questionPOST /engine/v1/ask with query
You want a predictable answer shape and a request validated before you payPOST /engine/v1/ask with intent + params
You know exactly which miner you needPOST /engine/v1/ask/{minerId} (native or canonical form)
You want to understand the payment flow itselfPaying with x402
You want a live stream of routing eventsWebSocket Signals
Your smart contract needs the result on-chainOn-Chain Jobs
You want to browse the Daemon's autonomously generated signalsDaemon Signal Feed
Was this helpful?