Live deals, A to Z Claim deals

For software

Machine interface

FirmFax serves its prop firm dataset over the Model Context Protocol, so an assistant can read the record for a firm directly. Every figure states whether we hold evidence for it, and a figure we cannot source is a state on the wire rather than a blank.

Last updated 2026-09-10

71

firms served

6

tools exposed

0

keys, accounts or signups

The endpoint

URL https://firmfax.com/api/mcp
Method POST only. A GET is answered 405: there is no page there.
Transport JSON-RPC 2.0 over HTTP. Protocol version 2025-06-18.
Server name firmfax version 1.0.0
Authentication None. There is no key to request and no account to create.

A JSON-RPC error is carried inside the envelope with a 200, because a caller inside another product sees a tool result rather than a status line, and an HTTP error there reads as a network fault rather than as an answer.

Tools

get_firm

One prop trading firm. Every figure states whether we hold evidence for it, and a firm whose score range spans more than one band carries no letter grade at all.

{
  "type": "object",
  "properties": {
    "slug": {
      "type": "string",
      "description": "The firm slug, e.g. \"topstep\"."
    }
  },
  "required": [
    "slug"
  ]
}

get_rule_changes

The dated record of what changed, newest first. Every entry states its KIND, and most of this tape is FirmFax correcting its own data rather than a firm changing its rules. Read counts before describing industry activity: a total taken across kinds overstates it.

{
  "type": "object",
  "properties": {
    "slug": {
      "type": "string",
      "description": "Limit to one firm. Omit for every firm."
    },
    "since": {
      "type": "string",
      "description": "ISO date, e.g. \"2026-08-01\". Entries dated on or after it."
    },
    "kind": {
      "type": "string",
      "enum": [
        "firm",
        "record",
        "withdrawn"
      ],
      "description": "firm = the firm changed a rule. record = FirmFax corrected its own data. withdrawn = we published an entry and then retracted it."
    },
    "limit": {
      "type": "number",
      "description": "How many entries to return, newest first. Default 50."
    }
  }
}

get_evidence

The stored reading behind a figure: the verbatim quote, the page it came from, and the date we read it. Answers "we hold none" explicitly rather than returning an empty list. `outcome` is load-bearing: a `contradicted`, `contradiction` or `absent` row records what we read, not a figure we publish. Use get_firm for the published figure.

{
  "type": "object",
  "properties": {
    "slug": {
      "type": "string",
      "description": "The firm slug, e.g. \"topstep\"."
    },
    "field": {
      "type": "string",
      "description": "One field path, e.g. \"rules.dailyLossLimit\". Omit for every field we hold for this firm."
    }
  },
  "required": [
    "slug"
  ]
}

find_firms

Firms matching every criterion given. A firm whose record does not carry a criterion is never counted as a match and never silently dropped: it is returned separately under unknown, with the field that could not be tested. Absence is common here, so read counts before treating the matched list as the answer.

{
  "type": "object",
  "properties": {
    "assetClass": {
      "type": "string",
      "enum": [
        "forex",
        "futures",
        "crypto"
      ],
      "description": "Firms recorded as offering this. 21 of 71 records name no asset class and are returned as unknown."
    },
    "maxActivationFee": {
      "type": "number",
      "description": "Activation fee at or below this. 42 of 71 record no fee."
    },
    "maxResetFee": {
      "type": "number",
      "description": "Reset fee at or below this. 56 of 71 record no fee."
    },
    "maxConsistencyPct": {
      "type": "number",
      "description": "Consistency rule at or below this. 28 of 71 record none."
    },
    "maxDailyLossLimit": {
      "type": "number",
      "description": "Daily loss limit at or below this, in the unit dailyLossLimitUnit names. 31 of 71 record none."
    },
    "dailyLossLimitUnit": {
      "type": "string",
      "enum": [
        "percent",
        "usd"
      ],
      "description": "The unit maxDailyLossLimit is in. Daily loss limits are recorded in percent at some firms and in dollars at others, and the two are never compared: a firm recorded in the other unit is returned under unknown with its unit named."
    },
    "maxMinTradingDays": {
      "type": "number",
      "description": "Minimum trading days at or below this, counted as minTradingDaysUnit names. 16 of 71 record none."
    },
    "minTradingDaysUnit": {
      "type": "string",
      "enum": [
        "calendar_days",
        "trading_days",
        "winning_days"
      ],
      "description": "What maxMinTradingDays counts: calendar_days, trading_days or winning_days. A minimum is compared only with minimums counted the same way; a firm counting another way, or stating no unit, is returned under unknown with what it counts named."
    },
    "minTradingDaysMinProfit": {
      "type": "number",
      "description": "With winning_days only: the profit a day must reach to count. A firm whose winning days carry a different profit bar is returned under unknown."
    },
    "maxMinDaysToPayout": {
      "type": "number",
      "description": "Minimum days to first payout at or below this, counted as minDaysToPayoutUnit names. 26 of 71 record none."
    },
    "minDaysToPayoutUnit": {
      "type": "string",
      "enum": [
        "calendar_days",
        "trading_days",
        "winning_days"
      ],
      "description": "What maxMinDaysToPayout counts: calendar_days, trading_days or winning_days. A wait is compared only with waits counted the same way; a firm counting another way, or stating no unit, is returned under unknown with what it counts named."
    },
    "minDaysToPayoutMinProfit": {
      "type": "number",
      "description": "With winning_days only: the profit a day must reach to count. A firm whose winning days carry a different profit bar is returned under unknown."
    }
  }
}

compare_firms

Two firms side by side. Reports each figure with its evidence state and whether the pair is comparable at all: a field neither firm records is not a tie, and a field only one records is not a win. No per-field winner is declared, because the direction of "better" for several of these is a judgement the published scoring model already makes; each firm's grade is returned alongside so the verdict comes from that model rather than from this tool.

{
  "type": "object",
  "properties": {
    "a": {
      "type": "string",
      "description": "First firm slug."
    },
    "b": {
      "type": "string",
      "description": "Second firm slug."
    }
  },
  "required": [
    "a",
    "b"
  ]
}

list_firms

Every firm slug and name. Carries no figures, so nothing here needs evidence.

{
  "type": "object",
  "properties": {}
}

Every figure is one of three states

Shape Means Notes
{ value, quote, sourceUrl, asOf } Evidenced A figure we can quote from a page the firm itself publishes. The quote and the URL travel with it, so a product presenting this can cite the firm rather than cite us.
{ status: 'unevidenced', unverifiedValue, checked } Held, nothing behind it A figure we hold with no published source read for it. Most of the dataset is here today. The number is under unverifiedValue and never under value, so reading .value across a response returns only evidenced figures and lifting an unsourced one has to be deliberate.
{ status: 'not_recorded', checked } Absent No value at all. Never null, never 0, never an omitted key. checked lists the pages that were read, so a caller can see the search was real.

01

What this is

FirmFax publishes its dataset over the Model Context Protocol as well as over this website. An assistant or other software can request the record for any of the 71 firms directly and present it inside a product that is not this one. The records are the same records: the same figures, the same scores, the same dated sources.

There is no API key, no account and no signup. The endpoint is a single POST route speaking JSON-RPC 2.0. It is read-only: nothing it exposes writes anything, and it holds no state about who called it.

02

Why the shapes are awkward

Every field is an object and there are no bare numbers anywhere. That is deliberate and it is the reason this interface exists in the form it does. A tool response is rendered inside a product we cannot see, by a template we do not control, so a null on the wire becomes "None" or "$0" in somebody else's interface and the absence disappears.

This site's first rule is that a missing value is not a claim. On a page that can be enforced by a renderer. Over a wire it has to be enforced by the shape, because there is no renderer of ours between the data and the reader. So an absent figure is a positive state with its own name, not a blank, and a figure with no source behind it carries its number under a key no shared template reaches by accident.

03

Where the grade is withheld

A letter grade is published only where the range behind the score sits inside one band. Where the range spans two, what is published in place of a letter is the range itself and the facts that would close it. Today 7 of 71 firms withhold a letter on that basis.

Abstention is the absence of the letter key, not a value in it. A caller reading grade.letter gets only firms where a letter is genuinely published: there is no null, no empty string and no "N/A" to mishandle. Where the letter is absent, range and closedBy are populated instead, so the abstention is actionable rather than merely blank.

04

Commission links in responses

Responses can carry commission-bearing links, on the same terms as the links on this website. 25 of the 71 firms pay us and they are the same 25 named on the disclosure page; the other 46 carry no offer object at all. No firm pays for its record to be served or for its link to be included in a response, and the scoring model cannot read commercial terms.

Where an offer is present it carries this sentence with it, in the same record: “FirmFax may earn a commission if you sign up through this link. Ranking and scoring cannot read commercial terms: the scoring model has no access to the affiliate data.” A link that arrives without it is not a shape this interface can produce. What that cannot do is put the disclosure in front of you: a product receiving this data decides how to render it and may shorten or omit that statement entirely. That is a property of publishing to machines rather than to readers.

Calling it

curl -X POST https://firmfax.com/api/mcp \
  -H 'content-type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

Clients that take a remote MCP server accept the URL above directly. Clients that expect a local command can reach it through any stdio-to-HTTP bridge; nothing here depends on which.

How the scores are computed is on the methodology page, and what is and is not paid for is on the disclosure. Both describe this interface on the same terms as the website, because it serves the same records.