{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://brokerfit.io/data/schema.json",
  "title": "BrokerFit open dataset",
  "description": "Field definitions for the files published at https://brokerfit.io/methodology/open-data. Every row carries the day its source was read (verified_at) and the page it was read on (source_url); a value without both is absent from the files rather than published as an empty cell. An empty cell therefore never means zero — it means the source does not state it, and each field below says what its absence means. Dates are ISO 8601 (YYYY-MM-DD) and belong to the row, not to the file: each file is dated by the newest row in it, and one file's date says nothing about another's.",
  "license": "https://creativecommons.org/licenses/by/4.0/",
  "$defs": {
    "registry-entities": {
      "title": "registry-entities",
      "description": "One row of https://brokerfit.io/data/registry-entities.csv and one element of the \"records\" array of https://brokerfit.io/data/registry-entities.json. Read by: Manual reading of each licence's card on the regulator's public register.",
      "type": "object",
      "properties": {
        "broker_slug": {
          "type": "string",
          "description": "Broker identifier, stable across every file and equal to the last path segment of its review URL."
        },
        "broker_name": {
          "type": "string",
          "description": "Broker name as this site prints it in English."
        },
        "entity": {
          "type": "string",
          "description": "Legal entity as the register prints its name."
        },
        "regulator": {
          "type": "string",
          "description": "Regulator whose register holds the card."
        },
        "jurisdiction": {
          "type": "string",
          "description": "Country or territory the licence is issued in."
        },
        "reference": {
          "type": [
            "string",
            "null"
          ],
          "description": "Licence or registration number as the register prints it. Empty means the register prints none; `note` then says so."
        },
        "status": {
          "type": "string",
          "description": "Our normalisation of the register wording: authorised, authorised-with-requirements, run-off, applied-to-cancel, no-longer-authorised, licence-transferred."
        },
        "register_status": {
          "type": "string",
          "description": "The register card's own status wording on `verified_at`, copied verbatim."
        },
        "registry_url": {
          "type": "string",
          "description": "The register card itself."
        },
        "verified_at": {
          "type": "string",
          "description": "ISO date (YYYY-MM-DD) the source page was opened and read. Present on every row: a row without it is not published."
        },
        "note": {
          "type": [
            "string",
            "null"
          ],
          "description": "English caveat where the status alone would mislead. Empty means the register's wording needs none."
        }
      },
      "required": [
        "broker_slug",
        "broker_name",
        "entity",
        "regulator",
        "jurisdiction",
        "status",
        "register_status",
        "registry_url",
        "verified_at"
      ],
      "additionalProperties": false
    },
    "fees": {
      "title": "fees",
      "description": "One row of https://brokerfit.io/data/fees.csv and one element of the \"records\" array of https://brokerfit.io/data/fees.json. Read by: Manual reading of each broker's own pricing, deposit, withdrawal and interest pages.",
      "type": "object",
      "properties": {
        "broker_slug": {
          "type": "string",
          "description": "Broker identifier, stable across every file and equal to the last path segment of its review URL."
        },
        "broker_name": {
          "type": "string",
          "description": "Broker name as this site prints it in English."
        },
        "fee_type": {
          "type": "string",
          "description": "What the number prices: stock_commission, deposit, withdrawal, fx_conversion, cash_interest."
        },
        "unit": {
          "type": "string",
          "description": "What the number means. per-share, per-trade, pct-of-value, zero, min-per-trade, per-order (a fixed charge added to every order on top of the rate, not a minimum), max-pct-of-value, free-below-monthly-volume, flat, pct-of-amount, free-up-to, free-per-month, pct-per-year, min-balance, max-balance."
        },
        "amount": {
          "type": "number",
          "description": "The number itself, in the semantics of `unit` and denominated in `currency` where the unit is money. Percentages are written as percentages (0.2 means 0.2%), except pct-of-value, which is a fraction of trade value as the broker states it."
        },
        "currency": {
          "type": [
            "string",
            "null"
          ],
          "description": "Currency of `amount`. Empty where the unit carries no currency (a count, a percentage, a fraction)."
        },
        "condition": {
          "type": [
            "string",
            "null"
          ],
          "description": "Scope and eligibility in English, including what the reading did not establish. Empty means the source states the value without qualifying it."
        },
        "entity": {
          "type": [
            "string",
            "null"
          ],
          "description": "Legal entity the row belongs to, as the broker names it. Empty means the source does not name one — the row then belongs to the brand, and a reader with a different entity may face different terms."
        },
        "verified_at": {
          "type": "string",
          "description": "ISO date (YYYY-MM-DD) the source page was opened and read. Present on every row: a row without it is not published."
        },
        "source_url": {
          "type": "string",
          "description": "The page the row was read on. Present on every row: a row without it is not published."
        }
      },
      "required": [
        "broker_slug",
        "broker_name",
        "fee_type",
        "unit",
        "amount",
        "verified_at",
        "source_url"
      ],
      "additionalProperties": false
    },
    "min-deposits": {
      "title": "min-deposits",
      "description": "One row of https://brokerfit.io/data/min-deposits.csv and one element of the \"records\" array of https://brokerfit.io/data/min-deposits.json. Read by: Manual reading of each broker's own account-opening and funding pages.",
      "type": "object",
      "properties": {
        "broker_slug": {
          "type": "string",
          "description": "Broker identifier, stable across every file and equal to the last path segment of its review URL."
        },
        "broker_name": {
          "type": "string",
          "description": "Broker name as this site prints it in English."
        },
        "row_kind": {
          "type": "string",
          "description": "headline for the broker-wide threshold, tier for one account, plan or jurisdiction that differs from it. A headline is the lowest of the broker's tiers."
        },
        "scope": {
          "type": [
            "string",
            "null"
          ],
          "description": "Which account, plan or jurisdiction the tier belongs to. Empty on a headline row."
        },
        "amount": {
          "type": "number",
          "description": "Lowest amount that opens a funded account within this scope, in `currency`."
        },
        "currency": {
          "type": "string",
          "description": "Currency of `amount`. Always present: a threshold without its currency is not data."
        },
        "quote": {
          "type": [
            "string",
            "null"
          ],
          "description": "Verbatim sentence stating the threshold. Empty means the page states the figure in a table rather than a sentence."
        },
        "zero_caveat": {
          "type": [
            "string",
            "null"
          ],
          "description": "What actually stops a one-unit deposit where `amount` is 0. Empty on every non-zero row and on tier rows."
        },
        "min_withdrawal_amount": {
          "type": [
            "number",
            "null"
          ],
          "description": "Separate floor on taking money out, on headline rows. Empty means the broker publishes none."
        },
        "min_withdrawal_currency": {
          "type": [
            "string",
            "null"
          ],
          "description": "Currency of `min_withdrawal_amount`. Empty when that is empty."
        },
        "entity": {
          "type": [
            "string",
            "null"
          ],
          "description": "Legal entity the row belongs to, as the broker names it. Empty means the source does not name one — the row then belongs to the brand, and a reader with a different entity may face different terms."
        },
        "condition": {
          "type": [
            "string",
            "null"
          ],
          "description": "Scope and eligibility in English, including what the reading did not establish. Empty means the source states the value without qualifying it."
        },
        "verified_at": {
          "type": "string",
          "description": "ISO date (YYYY-MM-DD) the source page was opened and read. Present on every row: a row without it is not published."
        },
        "source_url": {
          "type": "string",
          "description": "The page the row was read on. Present on every row: a row without it is not published."
        }
      },
      "required": [
        "broker_slug",
        "broker_name",
        "row_kind",
        "amount",
        "currency",
        "verified_at",
        "source_url"
      ],
      "additionalProperties": false
    },
    "country-access": {
      "title": "country-access",
      "description": "One row of https://brokerfit.io/data/country-access.csv and one element of the \"records\" array of https://brokerfit.io/data/country-access.json. Read by: Manual reading of each broker's own country list — sign-up form, help-centre country article or terms of business.",
      "type": "object",
      "properties": {
        "broker_slug": {
          "type": "string",
          "description": "Broker identifier, stable across every file and equal to the last path segment of its review URL."
        },
        "broker_name": {
          "type": "string",
          "description": "Broker name as this site prints it in English."
        },
        "country_code": {
          "type": "string",
          "description": "ISO 3166-1 alpha-2 country code of the client residence."
        },
        "status": {
          "type": "string",
          "description": "available, restricted or unavailable, as the broker's own country list settles it. Pairs the broker does not settle are not in this file at all."
        },
        "verified_at": {
          "type": "string",
          "description": "ISO date (YYYY-MM-DD) the source page was opened and read. Present on every row: a row without it is not published."
        },
        "source_url": {
          "type": "string",
          "description": "The page the row was read on. Present on every row: a row without it is not published."
        }
      },
      "required": [
        "broker_slug",
        "broker_name",
        "country_code",
        "status",
        "verified_at",
        "source_url"
      ],
      "additionalProperties": false
    },
    "custody": {
      "title": "custody",
      "description": "One row of https://brokerfit.io/data/custody.csv and one element of the \"records\" array of https://brokerfit.io/data/custody.json. Read by: Manual reading of each broker's client agreement or terms of business.",
      "type": "object",
      "properties": {
        "broker_slug": {
          "type": "string",
          "description": "Broker identifier, stable across every file and equal to the last path segment of its review URL."
        },
        "broker_name": {
          "type": "string",
          "description": "Broker name as this site prints it in English."
        },
        "real_shares": {
          "type": "boolean",
          "description": "Whether the broker delivers a real security to the client at all."
        },
        "holding_model": {
          "type": "string",
          "description": "How that security is registered: omnibus-pooled, or none where no security is delivered."
        },
        "holding_quote": {
          "type": [
            "string",
            "null"
          ],
          "description": "Verbatim sentence on registration and title. Empty means the document does not say it in as many words."
        },
        "offers_share_cfd": {
          "type": [
            "boolean",
            "null"
          ],
          "description": "Whether the broker also sells contracts for difference on equities. Empty means the documents answer neither way — which is not the same statement as \"they do not\"."
        },
        "cfd_quote": {
          "type": [
            "string",
            "null"
          ],
          "description": "Verbatim sentence on what a contract for difference is at this broker. Empty where no equity CFD is offered or the document does not define one."
        },
        "insolvency_quote": {
          "type": [
            "string",
            "null"
          ],
          "description": "Verbatim sentence on what becomes of client securities if the broker fails. Empty means the document does not address it; an inferred reassurance is worth less than none."
        },
        "shortfall_quote": {
          "type": [
            "string",
            "null"
          ],
          "description": "Verbatim sentence on pooling, shortfall sharing or delays — the counterweight the same document states. Empty means the document states none."
        },
        "entity": {
          "type": [
            "string",
            "null"
          ],
          "description": "Legal entity the row belongs to, as the broker names it. Empty means the source does not name one — the row then belongs to the brand, and a reader with a different entity may face different terms."
        },
        "condition": {
          "type": [
            "string",
            "null"
          ],
          "description": "Scope and eligibility in English, including what the reading did not establish. Empty means the source states the value without qualifying it."
        },
        "verified_at": {
          "type": "string",
          "description": "ISO date (YYYY-MM-DD) the source page was opened and read. Present on every row: a row without it is not published."
        },
        "source_url": {
          "type": "string",
          "description": "The page the row was read on. Present on every row: a row without it is not published."
        }
      },
      "required": [
        "broker_slug",
        "broker_name",
        "real_shares",
        "holding_model",
        "verified_at",
        "source_url"
      ],
      "additionalProperties": false
    },
    "protection": {
      "title": "protection",
      "description": "One row of https://brokerfit.io/data/protection.csv and one element of the \"records\" array of https://brokerfit.io/data/protection.json. Read by: Manual reading of each broker's own margin, liquidation and negative-balance disclosures.",
      "type": "object",
      "properties": {
        "broker_slug": {
          "type": "string",
          "description": "Broker identifier, stable across every file and equal to the last path segment of its review URL."
        },
        "broker_name": {
          "type": "string",
          "description": "Broker name as this site prints it in English."
        },
        "offers_margin": {
          "type": "boolean",
          "description": "Whether any leveraged product is offered to retail clients."
        },
        "liquidation": {
          "type": [
            "string",
            "null"
          ],
          "description": "What the broker does when equity falls below the requirement: no-notice, notice-then-close, auto-close-esma. Empty where no leveraged product is offered."
        },
        "close_out_pct": {
          "type": [
            "number",
            "null"
          ],
          "description": "Equity level at which close-out begins, as a percentage of required margin. Empty where the broker publishes none."
        },
        "close_out_is_regulatory": {
          "type": [
            "boolean",
            "null"
          ],
          "description": "True where `close_out_pct` is the regulator's mandate rather than a house rule. Empty where there is no percentage."
        },
        "negative_balance_protection": {
          "type": "string",
          "description": "Who is covered: retail-only, all, or none. Never a boolean — retail-only disappears on reclassification to professional."
        },
        "nbp_condition": {
          "type": [
            "string",
            "null"
          ],
          "description": "Scope of the protection where it is narrower than the whole account. Empty means it applies account-wide."
        },
        "quote": {
          "type": [
            "string",
            "null"
          ],
          "description": "Verbatim sentence from the broker's own disclosure. Empty means the document states the policy without a quotable sentence."
        },
        "entity": {
          "type": [
            "string",
            "null"
          ],
          "description": "Legal entity the row belongs to, as the broker names it. Empty means the source does not name one — the row then belongs to the brand, and a reader with a different entity may face different terms."
        },
        "condition": {
          "type": [
            "string",
            "null"
          ],
          "description": "Scope and eligibility in English, including what the reading did not establish. Empty means the source states the value without qualifying it."
        },
        "verified_at": {
          "type": "string",
          "description": "ISO date (YYYY-MM-DD) the source page was opened and read. Present on every row: a row without it is not published."
        },
        "source_url": {
          "type": "string",
          "description": "The page the row was read on. Present on every row: a row without it is not published."
        }
      },
      "required": [
        "broker_slug",
        "broker_name",
        "offers_margin",
        "negative_balance_protection",
        "verified_at",
        "source_url"
      ],
      "additionalProperties": false
    },
    "platforms": {
      "title": "platforms",
      "description": "One row of https://brokerfit.io/data/platforms.csv and one element of the \"records\" array of https://brokerfit.io/data/platforms.json. Read by: Manual reading of each broker's own platform, API and demo-account pages.",
      "type": "object",
      "properties": {
        "broker_slug": {
          "type": "string",
          "description": "Broker identifier, stable across every file and equal to the last path segment of its review URL."
        },
        "broker_name": {
          "type": "string",
          "description": "Broker name as this site prints it in English."
        },
        "platforms": {
          "type": "string",
          "description": "Terminals the broker states it supports, as \"Name (kind)\" joined by \"; \". Kind is proprietary, metatrader or third-party."
        },
        "api": {
          "type": [
            "boolean",
            "null"
          ],
          "description": "Whether programmatic access is offered. Empty means the pages do not establish it either way."
        },
        "api_kinds": {
          "type": [
            "string",
            "null"
          ],
          "description": "Protocols as the broker names them, joined by \"; \". Empty where there is no API or the broker does not name one."
        },
        "order_types": {
          "type": [
            "string",
            "null"
          ],
          "description": "Order types as the broker lists them, joined by \"; \" — never normalised into our words, never extended. Empty where the broker publishes no list."
        },
        "demo": {
          "type": [
            "boolean",
            "null"
          ],
          "description": "Whether a demo account exists. Empty means the pages do not establish it."
        },
        "demo_expiry": {
          "type": [
            "string",
            "null"
          ],
          "description": "How the demo ends: fixed-term, inactivity, or none-stated where the broker says outright there is no limit. Empty means the broker does not address it, which is a different answer from none-stated."
        },
        "demo_expiry_days": {
          "type": [
            "number",
            "null"
          ],
          "description": "Days in the expiry window. Empty where the broker states no limit or does not say."
        },
        "entity": {
          "type": [
            "string",
            "null"
          ],
          "description": "Legal entity the row belongs to, as the broker names it. Empty means the source does not name one — the row then belongs to the brand, and a reader with a different entity may face different terms."
        },
        "condition": {
          "type": [
            "string",
            "null"
          ],
          "description": "Scope and eligibility in English, including what the reading did not establish. Empty means the source states the value without qualifying it."
        },
        "verified_at": {
          "type": "string",
          "description": "ISO date (YYYY-MM-DD) the source page was opened and read. Present on every row: a row without it is not published."
        },
        "source_url": {
          "type": "string",
          "description": "The page the row was read on. Present on every row: a row without it is not published."
        }
      },
      "required": [
        "broker_slug",
        "broker_name",
        "platforms",
        "verified_at",
        "source_url"
      ],
      "additionalProperties": false
    },
    "w8ben": {
      "title": "w8ben",
      "description": "One row of https://brokerfit.io/data/w8ben.csv and one element of the \"records\" array of https://brokerfit.io/data/w8ben.json. Read by: Manual reading of each broker's own documents on the W-8BEN form and US withholding.",
      "type": "object",
      "properties": {
        "broker_slug": {
          "type": "string",
          "description": "Broker identifier, stable across every file and equal to the last path segment of its review URL."
        },
        "broker_name": {
          "type": "string",
          "description": "Broker name as this site prints it in English."
        },
        "supported": {
          "type": "boolean",
          "description": "Whether the broker takes the W-8BEN form at all."
        },
        "filing": {
          "type": [
            "string",
            "null"
          ],
          "description": "How the form is submitted: in-app, paper or both. Empty where the form is not supported."
        },
        "filing_quote": {
          "type": [
            "string",
            "null"
          ],
          "description": "Verbatim sentence on how the form is submitted. Empty where the document has no such sentence."
        },
        "withholding_agent": {
          "type": [
            "boolean",
            "null"
          ],
          "description": "Whether the broker withholds US tax at source itself, by its own statement. Empty means its documents do not say, never inferred from its licence."
        },
        "withholding_agent_quote": {
          "type": [
            "string",
            "null"
          ],
          "description": "Verbatim sentence on the withholding role. Empty where the document has none."
        },
        "stated_rate_pct": {
          "type": [
            "number",
            "null"
          ],
          "description": "The US dividend withholding rate the broker's own documents name, as a percentage. Empty where the document gives no number — this file never substitutes one, and it publishes no rate for the reader's own country."
        },
        "stated_rate_quote": {
          "type": [
            "string",
            "null"
          ],
          "description": "Verbatim sentence carrying that rate. Empty where there is no stated rate."
        },
        "validity_quote": {
          "type": [
            "string",
            "null"
          ],
          "description": "Verbatim sentence on how long the form stays valid. Empty where the document does not say."
        },
        "entity": {
          "type": [
            "string",
            "null"
          ],
          "description": "Legal entity the row belongs to, as the broker names it. Empty means the source does not name one — the row then belongs to the brand, and a reader with a different entity may face different terms."
        },
        "condition": {
          "type": [
            "string",
            "null"
          ],
          "description": "Scope and eligibility in English, including what the reading did not establish. Empty means the source states the value without qualifying it."
        },
        "verified_at": {
          "type": "string",
          "description": "ISO date (YYYY-MM-DD) the source page was opened and read. Present on every row: a row without it is not published."
        },
        "source_url": {
          "type": "string",
          "description": "The page the row was read on. Present on every row: a row without it is not published."
        }
      },
      "required": [
        "broker_slug",
        "broker_name",
        "supported",
        "verified_at",
        "source_url"
      ],
      "additionalProperties": false
    }
  }
}
