thirdfy-app.json reference

The manifest format for a protocol on Thirdfy. Reads over MCP or REST, writes as contract calls, and the rules every write must pass.

thirdfy-app.json describes one protocol. It lists the protocol's actions, and each read action describes what it returns. The machine-readable schema (JSON Schema 2020-12) is at https://api.thirdfy.com/schemas/thirdfy-app.v1.json.

App fields

FieldMeaning
schemaVersion1.
appIdYour stable id: lowercase letters, digits, _ and -, starting with a letter or digit, up to 64 characters.
kinddata, data_agent or venue. Use venue for a protocol with writes.
title, summarySummary is at most 200 characters.
categoryThe shelf in the directory, such as earn, spot, derivatives, sentiment or news.
icon, docsUrlOptional URLs.
upstream{ transport, url?, auth }. See below.
pricing{ model }: none for a free read.
redistribution{ displayValues, displayRaw, attribution? }: whether agents' users may be shown your values and your raw reply, and the credit line to show.
manifestVersionA positive integer. It must grow with every submission.
contractsWrites only: { "<chainId>": { "<alias>": "0x…" } }. The only addresses a write may call or approve.
actions[]Your actions.

Reads

A read gets data from your service. Two transports:

  • mcp_remote: your remote MCP server. upstream.url is an https URL. Each action names the MCP tool to call in invoke. invoke.arguments can add fixed arguments (literals only).
  • rest: your HTTP API. upstream.url is an https base URL. Each action gives invoke.method (GET or POST), a path under that base with whole-segment {param} placeholders, and query and body fields ({param} or a literal).

Each read action has:

FieldMeaning
actionSnake case, for example get_acme_pool_stats. Start with a read verb: get_, list_, fetch_, search_, show_ or check_.
readOnlytrue.
billingTier, creditsThe tier is one of the read_* tiers for a read (read_offchain for data your service serves) and write or write_heavy for a write. credits must equal the tier's default. Thirdfy sets that figure, and the submission check tells you the expected value, so the 0 in the examples is a placeholder.
descriptionWhat an agent reads to decide to call it. At most 500 characters. Reviewed by Thirdfy.
inputSchemaJSON Schema of the params. Only these params reach your service.
outputSchemaJSON Schema of the typed reading. Required.
invoke.typedMaps each typed field to a path in your reply. Without it the whole reply is the reading.
fixtureParamsRead-only params Thirdfy uses for its checks and its hourly health probe.
signals[]Optional. Named measures with typed fields, a freshness and the roles a strategy may give them (direction, filter, context).
ttlMsHow long Thirdfy may cache a reading (0 to 3,600,000).

A read does one fixed thing. Tools that run code or call whatever their input names (a generic execute, a free endpoint_path) cannot be listed.

Thirdfy sends no Thirdfy key to a manifest it has not certified, and keys it holds never appear in the catalog.

{
  "schemaVersion": 1,
  "appId": "acme",
  "kind": "data",
  "title": "Acme Pools",
  "summary": "Pool depth and fees from Acme.",
  "category": "market_data",
  "upstream": { "transport": "mcp_remote", "url": "https://mcp.acme.example/mcp", "auth": "none" },
  "pricing": { "model": "none" },
  "redistribution": { "displayValues": true, "displayRaw": false },
  "manifestVersion": 1,
  "actions": [
    {
      "action": "get_acme_pool_stats",
      "readOnly": true,
      "billingTier": "read_offchain",
      "credits": 0,
      "description": "Depth, fee tier and 24h volume for one Acme pool.",
      "inputSchema": { "type": "object", "properties": { "pool": { "type": "string" } }, "required": ["pool"] },
      "outputSchema": { "type": "object", "properties": { "tvlUsd": { "type": "number" } } },
      "invoke": { "tool": "pool_stats" },
      "fixtureParams": { "pool": "USDC-WETH" },
      "signals": []
    }
  ]
}

Writes

A write is an on-chain call to your contracts, declared as data (upstream.transport: contract_call, auth: none, no url). Thirdfy encodes the call from the action's write block, checks it, and sends it from the agent's wallet.

write fieldMeaning
routeHow the owner's wallet policy fences the call: erc4626_self, pinned_call or router_adapter. See below.
domainspot, bridge, perps, earn, prediction or internal.
chainsChain ids. Each needs an entry in contracts.
paramsWhat the agent supplies: { <name>: { type, role? } }. Types are fixed size: address, bool, bytes32, uintN, intN. An agent-supplied address needs a role: token, market or spend_token.
approvalsOptional ERC-20 approvals sent before the call. Exact amounts only.
call{ contract, function, args, pins, value?, maxValueWei? }.
opensPosition, exitA write that opens a position names the write of this app that closes it.
risk, maxAmountUsdRisk level, and the most one call may move. Thirdfy may lower maxAmountUsd when it lists you.

The three routes

RouteUse it forOwner signature
erc4626_selfA standard ERC-4626 vault: exactly deposit, withdraw or redeem, with receiver and owner pinned to the agent's wallet.Not needed.
pinned_callAny other function. Your contracts become a group in the owner's wallet policy, with the function and its pinned arguments fixed.The owner signs your protocol into the policy once.
router_adapterOpaque calldata through a Thirdfy router.Listed, but not executable until the router ships.

Pins: $self and $owner

args binds every input of the function to $param.<name>, $self (the agent's wallet), $owner (the owner's payout address) or a literal. pins turn into conditions of the wallet policy rule: a pinned path must equal $self (or $owner).

Every recipient-like input must be pinned. Names such as recipient, to, receiver, onBehalfOf, owner and beneficiary count, also inside longer names such as feeRecipient. A pinned input cannot also take agent input. Funds can only come back to the agent's wallet.

Approvals

approvals entries are { token, spender, amount }, where token is $param.<name> or an alias and spender is an alias from contracts. The amount is $param.<name> or a decimal. An approval is for the exact amount of the call and never unlimited: an amount of 2^128 or more is refused.

What a write may not do

The checks refuse a write when:

  • a function input is unbound, or an arg names no input;
  • a recipient-like address is not pinned;
  • an input is an array, bytes or string;
  • the function is a multicall wrapper (multicall, execute, aggregate, batch), because the wallet policy cannot see inside it;
  • a payable function has no maxValueWei, or a non-payable one sends value;
  • an approval is unlimited;
  • an address that is neither zero nor in contracts appears as a literal;
  • the name is not <appId>_<verb> (for example acme_deposit) or starts with a read verb;
  • the billing tier is not a write tier;
  • a write that opens a position has no exit, or the exit is not a write of this app.
{
  "action": "acme_vault_deposit",
  "readOnly": false,
  "billingTier": "write",
  "credits": 0,
  "signals": [],
  "description": "Deposit USDC into the Acme vault; shares go to the agent wallet.",
  "inputSchema": {
    "type": "object",
    "additionalProperties": false,
    "required": ["assets"],
    "properties": { "assets": { "type": "string", "pattern": "^[0-9]{1,78}$" } }
  },
  "fixtureParams": { "assets": "1000000" },
  "write": {
    "route": "erc4626_self",
    "domain": "earn",
    "chains": [8453],
    "params": { "assets": { "type": "uint256", "role": "spend_amount" } },
    "approvals": [{ "token": "usdc", "spender": "vault", "amount": "$param.assets" }],
    "call": {
      "contract": "vault",
      "function": "deposit(uint256 assets, address receiver)",
      "args": { "assets": "$param.assets", "receiver": "$self" },
      "pins": [{ "path": "receiver", "to": "self" }]
    },
    "opensPosition": true,
    "exit": "acme_vault_redeem",
    "risk": "low",
    "maxAmountUsd": 1000
  }
}

The app around it has kind: "venue", upstream: { "transport": "contract_call", "auth": "none" }, pricing: { "model": "none" } and contracts: { "8453": { "vault": "0x…", "usdc": "0x…" } }, where the aliases vault and usdc are the ones the action uses. The 0 in credits is a placeholder; see billingTier, credits above. The submission checks tell you if a field is missing.

fixtureParams

Every write needs fixtureParams: example params Thirdfy uses to build the call, simulate it and run the policy proof. Reads use them for the check call and the health probe. Use harmless values and read-only params.

exit

A write that opens a position (a deposit, a swap into a token, a new order) must name, in exit, the write of the same app that closes it. An agent that opens a position through you must always have a way out, and Thirdfy tests it.

After a check fails

The submission page lists each finding with its path in the manifest. Fix the manifest, raise manifestVersion and submit again.