Signal manifest

thirdfy-app.json v1. One open format that says what each data read returns, what it measures, how fresh it is, what it costs and how a strategy may use it.

Category: Market data · signals

Agents should not trade on data they cannot inspect. The signal manifest describes every data read on Thirdfy in one machine-readable format. It says what a read measures, which typed fields it returns, their units, how fresh the reading is, what one call costs, and which roles a strategy may give it.

The model: app, actions, signals

There is one format, thirdfy-app.json (schema version 1).

  • An app is one provider or data agent, such as CoinMarketCap or Allora.
  • An app has actions. These are ordinary catalog actions, such as get_cmc_fear_greed.
  • A read-only action has signals. A signal is one named measure with typed fields, a universe, a freshness and allowed roles.

One action can carry several signals. For example, get_cmc_derivatives_metrics provides cmc.market_funding, cmc.open_interest_change and cmc.liquidations.

Thirdfy publishes the manifest inside the action catalog. Consumers read it from there. They do not have to guess meaning from action names.

App fields

FieldTypeMeaning
schemaVersion1Format version.
appIdstringStable id, lowercase (^[a-z0-9][a-z0-9_-]{0,63}$). For a built-in provider it equals the provider id (cmc, nansen, allora).
kindenumdata (a data provider), data_agent (an agent that sells its output, such as Allora), venue or internal.
titlestringDisplay name.
summarystringAt most 200 characters. Shown before a client loads the detail.
categorystringMarketplace shelf: sentiment, derivatives, forecasts, flows, news.
icon, docsUrlURLOptional.
upstream.transportenumHow Thirdfy reaches the provider: native, mcp_remote, rest, x402 or webhook.
upstream.url, upstream.authstringWhere and how Thirdfy authenticates upstream, for example thirdfy_held_key. Provider keys stay with Thirdfy. The public catalog never shows url or auth.
redistribution.displayValuesbooleanA consumer may show the typed values to its users.
redistribution.displayRawbooleanA consumer may show the provider's raw reply.
redistribution.attributionstringThe credit line to show wherever the data appears. Optional.
pricing.modelenumper_read, subscription or none.
manifestVersionintegerBumped on every change.
actions[]arraySee below.

Action fields

FieldTypeMeaning
actionstringThe catalog action name in snake case (get_cmc_fear_greed). The kebab-case spelling resolves to the same action.
readOnlybooleanOnly a read-only action carries signals.
billingTierstringThe credit tier of one call.
creditsnumberCredits per call at that tier.
outputSchemaJSON SchemaJSON Schema 2020-12 of the output. Required for any app that is not built in.
signals[]arrayThe signals this read provides. It may be empty.

Signal fields

FieldTypeMeaning
idstring<appId>.<measure>, such as cmc.fear_greed. Stable once published: strategies and studies key on it.
titlestringDisplay name.
summarystringAt most 200 characters.
measuresstringWhat it measures: sentiment, crowding, price_forecast, regime, positioning, …
scopeenummarket (one reading for the whole market) or asset (one reading per asset).
universe"market", "any" or stringmarket for market scope. For asset scope: any asset the action accepts, or a list of tickers such as ["BTC"].
fieldsobject{ name: { path, type, unit?, range?, values?, description? } }. See Fields and paths.
asOf.pathstringWhere the observation time is. When the provider sends no time, this is freshness.fetchedAt.
cadenceSecnumberHow often the provider refreshes the reading. Optional.
freshnessSecnumberThe oldest a reading may be and still be used. Optional.
horizonHoursnumberThe horizon a forecast speaks to. Optional.
rolesstringThe roles a strategy may give it. See Roles.
rulesstringThe rule types that can read it: threshold_veto, regime_gate, event_blackout, forecast_gap, positioning.
attribution, docsUrlstringOptional.

Fields and paths

type is one of number, string, enum, boolean, datetime, array or json.

  • range is allowed only on numbers, for example [0, 100].
  • values is allowed only on enums, for example ["low", "medium", "high"].
  • json marks a raw block the provider owns. It is read as context and never compared.

A path points into the action output's data object. It is a dotted key path. [] steps into every item of an array.

  • A typed reading is always under typed: typed.index, typed.rows[].fundingRate.
  • Allora forecasts are typed natively, so their paths start at results[].
  • A read with no typed reading yet points at the raw block (payload, or result for a CoinMarketCap brief). Such a signal is context only.

Units live in field names and in unit. usd is US dollars. percent means 6.26 is 6.26%. percentage_points is a change in points. bps is basis points. percentile is 0 to 100. index is a unitless score. count is a whole number of items, such as traders. percent_per_interval and fraction_per_interval are funding rates per funding interval. Dates are ISO-8601 UTC.

Roles

RoleWhat it may do
directionPick the side of a trade (long or short).
filterVeto an entry. A filter never opens a trade on its own.
contextInform only. It never changes a decision on its own.

A signal lists the roles it supports. A strategy picks one of them. A consumer such as a hosted runtime may approve fewer roles than the manifest allows.

Typed reads

The main CoinMarketCap and Nansen reads return a typed reading next to the raw reply. These are the typed reads today:

  • get_cmc_fear_greed, get_cmc_global_metrics, get_cmc_derivatives_metrics, get_cmc_upcoming_events
  • get_cmc_market_regime, get_cmc_derivatives_crowding, get_cmc_perp_contract_analysis
  • get_nansen_perp_screener

Every other CoinMarketCap and Nansen read has freshness but no typed yet. Its raw reply is in payload, in result for a composed CoinMarketCap brief, or in answer for the Nansen research agent. Composed briefs name the credit line sourceAttribution. Allora forecasts are typed natively: each item of results[] has its own timestamp and freshness.

FieldMeaning
payloadThe provider's raw reply, unchanged. Kept for compatibility.
typedThe typed reading. Display strings become numbers ("+6.26%" becomes 6.26, "385.08 B" becomes 385080000000). It is null when the reply had a shape the parser did not recognize.
parseIssuesstring[]. One entry per field that could not be read, such as "typed.index: missing". Empty when every field was read.
freshness{ fetchedAt, ttlMs, cacheHit }. fetchedAt is when Thirdfy fetched the data upstream. A cache hit keeps the original time. ttlMs is how long Thirdfy caches it (0 means not cached).
attributionThe credit line to show, when the provider requires one.

A read never fails because of an unexpected reply. typed has null fields and parseIssues says why. Check parseIssues and the age of asOf before you act on a value.

Credits and billing tiers

Each call is billed in Thirdfy credits at the action's billing tier. The catalog shows billingTier and credits on every action.

TierCredits per callExamples
read_offchain2.5get_cmc_fear_greed, get_cmc_derivatives_metrics, get_nansen_perp_screener, get_allora_market_forecast
read_compose10Composed CoinMarketCap briefs: get_cmc_market_regime, get_cmc_derivatives_crowding, get_cmc_perp_contract_analysis, get_cmc_btc_etf_demand

A cache hit is still a billed call. One call can feed several signals, so read once and evaluate every signal on that reply. See Credits and balance.

Redistribution and attribution

The app's redistribution block says what a consumer may show its users.

  • displayValues: true: show the typed values, with the attribution line.
  • displayValues: false: use the values in decisions, but show only the outcome, such as "passed" or "vetoed". Nansen is false.
  • displayRaw: false: do not show the provider's raw reply.

Show attribution wherever the data appears, for example "Powered by CoinMarketCap".

Discovery

REST

curl "https://api.thirdfy.com/api/v1/agent/actions/catalog"
  • Top level: apps[], each with appId, kind, title, summary, category, docsUrl, transport, redistribution, pricing, manifestVersion, source, actions (the action names) and signalIds.
  • Each action: app { appId, manifestVersion }, signals[], billingTier, credits, dataCategory and outputSchema.
  • The keyed agent list (GET /api/v1/agent/actions) has the same per-action fields. It sends outputSchema only when you add include=outputSchema.

dataCategory groups data reads: market_data, signal, research and wallet.

MCP

Connect to https://mcp.thirdfy.com/mcp?toolsets=signals.

  • The toolset adds only the read-only data reads that carry signals: 13 tools today. Every tool has readOnlyHint: true and a title.
  • Each tool has an MCP outputSchema. A call returns typed, parseIssues, freshness and attribution as structuredContent, so the model does not receive the raw payload. Pass responseFormat: "detailed" to get the full reply.
  • The tool description has one line per signal: what it measures, fields with units, refresh and max age, roles, credits and attribution.
  • Tool _meta["com.thirdfy/app"] carries the app id, manifest version, data category, billing tier, credits and signals.
  • Combine it with other toolsets: ?toolsets=signals,perps.
  • Without a key the tools are listed but do not run. A call returns AGENT_KEY_REQUIRED. See MCP onboarding.

CLI

thirdfy-agent signals --json
thirdfy-agent signals --provider cmc --role filter --json
thirdfy-agent actions --data-category signal,research --json

signals lists every app and signal: fields with paths and units, freshness, roles, rules, the read that returns it, credits and attribution. run prints the typed reading of a data read first in human mode.

Complete example: CoinMarketCap fear and greed

The CoinMarketCap manifest, trimmed to one action. The catalog serves the same data in a flatter shape: apps[] has the app fields with transport in place of upstream (upstream.url and upstream.auth stay private), and each catalog action carries its own signals[].

{
  "schemaVersion": 1,
  "appId": "cmc",
  "kind": "data",
  "title": "CoinMarketCap",
  "summary": "Market sentiment, derivatives crowding, market regime, macro events and indicator state.",
  "category": "sentiment",
  "docsUrl": "https://coinmarketcap.com/api/documentation/v1/",
  "upstream": { "transport": "native", "auth": "thirdfy_held_key" },
  "redistribution": {
    "displayValues": true,
    "displayRaw": false,
    "attribution": "Powered by CoinMarketCap"
  },
  "pricing": { "model": "per_read" },
  "manifestVersion": 2,
  "actions": [
    {
      "action": "get_cmc_fear_greed",
      "readOnly": true,
      "billingTier": "read_offchain",
      "credits": 2.5,
      "signals": [
        {
          "id": "cmc.fear_greed",
          "title": "Fear and Greed index",
          "summary": "Market-wide sentiment, 0 (extreme fear) to 100 (extreme greed).",
          "measures": "sentiment",
          "scope": "market",
          "universe": "market",
          "fields": {
            "index": { "path": "typed.index", "type": "number", "unit": "index", "range": [0, 100] },
            "label": { "path": "typed.label", "type": "enum" }
          },
          "asOf": { "path": "typed.asOf" },
          "cadenceSec": 900,
          "freshnessSec": 3600,
          "roles": ["filter", "context"],
          "rules": ["threshold_veto"],
          "attribution": "Powered by CoinMarketCap"
        }
      ]
    }
  ]
}

A call to get_cmc_fear_greed returns (example values, raw payload trimmed):

{
  "success": true,
  "data": {
    "payload": { "…": "raw CoinMarketCap reply" },
    "classification": "evidence_only",
    "attribution": "Powered by CoinMarketCap",
    "typed": { "index": 55, "label": "Neutral", "asOf": "2026-09-30T14:45:00.000Z" },
    "parseIssues": [],
    "freshness": { "fetchedAt": "2026-09-30T14:52:10.000Z", "ttlMs": 900000, "cacheHit": false }
  }
}

How a strategy reads it:

  1. Read typed.index (the index field path) and typed.asOf (the asOf path).
  2. If parseIssues is not empty or asOf is older than freshnessSec (1 hour), treat the signal as unavailable.
  3. Use it only in an allowed role. Here that is filter (for example, a threshold_veto that skips new longs at 80 or above) or context.
  4. Show "Powered by CoinMarketCap" wherever the value appears.

Bring your own provider

A provider that runs its own MCP server publishes one manifest. Thirdfy proxies the calls, holds the key, meters each read and checks every reply against outputSchema. The provider never sees who the end user is.

{
  "schemaVersion": 1,
  "appId": "acme-flows",
  "kind": "data",
  "title": "Acme Flows",
  "summary": "Hourly net exchange flows per asset.",
  "category": "flows",
  "upstream": { "transport": "mcp_remote", "url": "https://mcp.acme.example/mcp", "auth": "thirdfy_held_key" },
  "redistribution": { "displayValues": true, "displayRaw": false, "attribution": "Data by Acme" },
  "pricing": { "model": "per_read" },
  "manifestVersion": 1,
  "actions": [
    {
      "action": "get_acme_net_flows",
      "readOnly": true,
      "billingTier": "read_offchain",
      "credits": 2.5,
      "outputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "typed": {
            "type": "object",
            "required": ["asset", "netFlowUsd", "asOf"],
            "properties": {
              "asset": { "type": "string" },
              "netFlowUsd": { "type": ["number", "null"] },
              "asOf": { "type": ["string", "null"], "format": "date-time" }
            }
          }
        }
      },
      "signals": [
        {
          "id": "acme-flows.net_flow",
          "title": "Net exchange flow",
          "summary": "Net USD flow into exchanges over the last hour. Positive means inflow.",
          "measures": "exchange_flow",
          "scope": "asset",
          "universe": ["BTC", "ETH"],
          "fields": {
            "netFlowUsd": { "path": "typed.netFlowUsd", "type": "number", "unit": "usd" }
          },
          "asOf": { "path": "typed.asOf" },
          "cadenceSec": 3600,
          "freshnessSec": 7200,
          "roles": ["filter", "context"],
          "rules": ["threshold_veto"],
          "attribution": "Data by Acme"
        }
      ]
    }
  ]
}

Checks before a manifest goes live:

  • The manifest validates, and ids are unique.
  • A signal id starts with its app id.
  • range appears only on numbers, values only on enums.
  • A signal whose fields are all json is context only.
  • An app that is not built in declares outputSchema on every action.
  • Every path resolves on a sample reply that validates against outputSchema.
  • Credits match the billing tier.

A Thirdfy admin then certifies the version. A change is always a new manifestVersion, never an edit.

Current signals

AppSignalsPage
CoinMarketCap (cmc)13CoinMarketCap
Nansen (nansen)1Nansen
Allora (allora)1Allora

GMGN reads are in the catalog but have no manifest yet. See GMGN.