> ## Documentation Index
> Fetch the complete documentation index at: https://docs.allocait.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Check a proposed change against the rules

> States which of the user's own rules a proposed change puts outside
its range or leaves further outside, using prices you supply. It
reads and writes nothing, decides nothing and needs only `read`, so
a Core account's key can call it. A result inside every range does
not say a change is advisable. Allocait cannot approve, stop or
place an order.

`trades` is the whole change, 1 to 50 items, applied together and in
any order: several trades in one symbol net. `side` is the direction
of the change being asked about: `BUY` adds shares to the holding,
`SELL` removes shares. Send `shares` (fractions allowed) or
`notional` (dollars, at the price in `prices`), not both. A `SELL`
larger than the shares held is `INVALID_INPUT`, never clamped,
unless the portfolio has `allow_shorts` on: then a `SELL` can take
a holding below zero and a `BUY` can take a short above it, and
`shares_after` is signed (negative is a short). A holding in the
Options class never goes below zero, and a change worth
100000000000000 or more cannot be recorded, so it is refused. `CASH`
is not tradable. A symbol the portfolio does not hold needs
`asset_class` (a class name, rules text alias or id); a held symbol
keeps the class it is recorded under.

`prices` is required and must cover every non-cash holding and every
traded symbol, except a held option with a `mark` or one past its
expiry, which is priced from the user's own number or at 0 (ADR-147).
A hole is a `400` with `error_code: PRICES_INCOMPLETE`
and `missing_symbols`; Allocait never fills a symbol you left out
from its own market data (ADR-036). A held symbol may be priced 0 (a
worthless option); a price you pass for an unexpired option replaces
its mark for this call only. A traded symbol priced 0 or less is
`PRICES_INVALID`. Prices are used for this call only and never
stored.

A trade in a held option that has expired is not checked, because the
option counts 0 and a trade cannot change the book. It is
`INVALID_INPUT` and the message names the trade's index and symbol and
the write that closes it (`shares` 0 and `price` 0).

`scenario_id` defaults to the portfolio's active scenario. A
portfolio with none set needs `scenario_id` (a 400 that says so).

**Reading the result.** `rules` lists only the rules outside their
range before or after the change. Rules inside both are counted in
`rules_judged_count`. `change` describes a rule's distance from its
range, never the trade. `newly_or_further_outside_count` answers
"did this change add a breach or deepen one". A bot that must do
neither checks that it equals 0.

`within_rules` answers a different question: is the whole book
inside every rule after the change. It is true only when no rule is
outside its range after the change, `rules_judged_count` is above 0
and `cash.sufficient` is true. Treat `false` as "not confirmed",
never as a block Allocait enforces. Treat `true` as saying nothing
about any symbol no rule covers; `not_covered` lists those
symbols, traded or held.

`exposure_after` says how much of the book gains if prices rise
once the whole change is applied: long, short, net and gross as
percents of `total_value`, at your prices (`shares x price x moves`;
a holding's Moves is the setting the user recorded, 1 unless set). It
is a reading beside the rules, not part of the verdict, and
`within_rules` does not read it. Options count at their premium, a
call as long and a put as short, and an expired option counts 0
(ADR-147).

Report `positions_updated_at` with the verdict. The book is what
Allocait has recorded. Allocait does not read the live account. The symbols in the
examples are fictional and their numbers are made up.




## OpenAPI

````yaml /openapi.yaml post /trades/check
openapi: 3.1.0
info:
  title: Allocait Agent API
  version: 1.0.0
  contact:
    name: Allocait support
    email: support@allocait.app
    url: https://docs.allocait.app/support
  license:
    name: Proprietary
    url: https://allocait.app/terms
  description: >
    Allocait checks your holdings against rules you wrote. It does not give
    investment advice or recommend trades.


    Use this API to read your rules and holdings, check them at prices you

    supply, and keep positions and cash current.


    New here? The [API quickstart](https://docs.allocait.app/api/quickstart)

    covers a key, a first read, a first check and error handling.


    **Currency.** Allocait supports US dollars only. Money values are in USD.


    **Authentication.** Send your API key in the `x-api-key` header.

    Create keys in the Allocait app under Settings > API keys. Every

    request is scoped to the key owner's own portfolios, and no request can

    reach another user's data.


    **Versioning.** The major version is in the path, and every path in

    this file is relative to a base that ends in `/v1`. A request to a path

    with no version, or to a version that does not exist, gets `404` with

    `error_code: NOT_FOUND` (a request with no valid key gets `401` first).
    There is no version header to send, and

    `info.version` above is the revision of this file, not the path

    version. The intent for `/v1`: changes are additive (a new route, a

    new response field, a new optional parameter, a new value in a

    response enum), so ignore fields you do not know and treat an

    `error_code` you do not know by its HTTP status. A breaking change

    ships as `/v2` beside `/v1`, and the docs announce the old version, with

    `Deprecation` and `Sunset` headers, before it is removed.


    **Prices.** Every price that leaves this API is one you supplied.

    Allocait's own stored quote is never returned, and nothing computed from

    it is either: not a value, a compliance verdict, a trade, or a monitor

    reading. The flow is two calls: `GET /portfolio/snapshot` to see what a

    portfolio holds and which symbols still need a price

    (`symbols_to_price`), then call again with a `prices` map for those

    symbols: `POST /portfolio/snapshot`, `/compliance/check`,

    `/rebalance/preview` or `/trades/check`. `/trades/check` also needs a

    price for every symbol in `trades`. One kind of number is the person's

    own and not a quote: the `mark` of an option, the premium per share they

    recorded. A held option with a mark needs no entry in `prices`, and

    an option past its expiry counts 0 whatever you pass (ADR-147).


    **Permissions.** A key carries `read`, `write`, or both. Every GET and

    every compute-only POST (`/compliance/check`, `/rebalance/preview`,

    `/portfolio/snapshot`, `/validate`, `/trades/check`, `/positions/size`,

    `/setup/preview`) needs `read`;

    every route marked as a write needs `write`. A key without the right

    one gets `403` with `This API key does not have read permission` or

    `Write permission required` (the second carries `error_code:

    READ_ONLY_KEY`, the code MCP gives it). When the account's agent

    access is read-only, no key can hold `write`, so that refusal says

    `This account's agent access is read-only.` instead, with the same

    code and status (ADR-232). This rule applies to

    every route; each operation's own `403` description lists only the

    causes specific to it. One route reads the body to decide: `POST

    /scenarios/{id}/rules` needs `read` when `dry_run` is `true` and

    `write` otherwise.


    **Plans.** An account on Core or Advanced with a paying or trialing

    subscription can use this API. Core reads: every GET and every

    compute-only POST works, and a key that carries `write` is capped to

    `read` at request time, so a write route answers `403` with `Write

    permission required`. Advanced reads and changes, and is the only plan

    with webhooks. A key issued on Advanced reads only after a downgrade

    and works again after an upgrade. An account with no active plan gets one
    `403` text on

    every route: `This Allocait account has no active plan, so agent access is
    off. The user manages their account in the Allocait app.`


    **Rate limits.** Every route shares one 60-requests-per-minute budget

    per account, shared with the MCP server and across all of the

    account's API keys. Every response that reaches a route carries

    `X-RateLimit-Limit` (always `60`) and `X-RateLimit-Remaining` (how many

    are left in the current window). A request past the cap gets `429` with

    those headers and `Retry-After: 60` instead of a route response. The

    same account also has a ceiling of 10,000 requests per UTC day; past it,

    requests get `429` with `Retry-After` set to the seconds until 00:00

    UTC, and no `X-RateLimit-*` headers (they would say requests remain).

    `401` and the no-plan `403` carry none of these headers. A per-route cap

    (`DAILY_LIMIT_EXCEEDED`, 20 support requests or feedback reports in 24

    hours) is also a `429`, with the `X-RateLimit-*` headers and no

    `Retry-After`, because waiting a minute does not clear it. See the `429`

    response on each operation.


    **List responses.** Every GET list route returns `{data, has_more}`,

    never a bare array. `data` is the page of rows and `has_more` is `true`

    when another page follows. The GET list routes that take `limit`

    (default 100, max 500) and `cursor` (a row offset, default 0) query

    parameters (`/portfolios`, `/positions`, `/watchlists`, `/scenarios`,

    `/asset-classes`, `/charts`, `/playbooks`, `/position-rules`,

    `/support-requests`, `/agent-feedback`, `/actions`, `/inputs`,

    `/inputs/{id}/entries`) page on them: call again with

    `cursor` set to the previous `cursor` (or 0) plus `limit` while

    `has_more` is `true`. `/monitors`, `/watchlists/{id}/items`,

    `/inputs/{id}/options` and `/webhooks` take no

    `limit`/`cursor` and always return their complete result, so `has_more`

    on those is always `false`. A list scoped to an id (a portfolio, an

    input, a watchlist, a scenario) answers `404` when that id is not

    yours, never an empty `data` that reads like a list with nothing in it.

    `/rules/restore-points` is a distinct shape

    (`{success, restore_points}`, already holding at most the caller's most

    recent 20) and does not use this envelope at all.


    **Method not allowed.** A real path called with a verb it does not

    support gets `405`, not the `404` an unknown path gets, with an `Allow`

    header listing the methods that path does support.


    **Request format.** Every POST/PATCH/DELETE body must be JSON. A request

    sent with a `Content-Type` that is present and not `application/json`

    gets `400` with `Content-Type must be application/json` before any

    other validation runs. A request with no `Content-Type` at all is still

    parsed as JSON, unchanged. A body that is present but does not parse as

    JSON gets its own `400` (`Request body is not valid JSON`), distinct

    from the "field is required" a genuinely empty body produces.


    **Field types and ids.** A field of the wrong type is a `400` that names

    it (`initial_cash must be a number`), never a default: a number sent as

    text does not become 0. On a create, `null` means "no value", the same as

    leaving the field out. An id in the query or body that is not a UUID is a

    `400` too (`portfolio_id must be a UUID`). In the path, an id of the right

    length with the wrong characters is a `400` (`The id in the path must be a

    UUID`), and any other shape matches no route and is a `404`. A mistake in

    the request should get a `400`, not a `500`.


    **Bulk write limits.** A bulk write caps these array fields at 500 items per

    request: `positions` (`POST /portfolios/{id}/positions/apply`), `rules`

    (`POST /scenarios/{id}/position-rules/apply`), `entries`

    (`POST /inputs/entries`), `entry_ids` (`DELETE /inputs/entries`),

    `item_ids` and `sort_orders` (`PATCH /watchlists/{id}/items`), and

    `action_ids` (`DELETE /actions`).

    Sending more gets `400` with `<field> must have at most 500 items`. The

    `rules_text` field on `POST /scenarios/{id}/rules` has its own two caps:

    the text itself at 102,400 bytes (100 KiB), checked before the file is

    parsed at all (`400` with `rules_text must be at most 102400 bytes`), and

    the file's `classes` at 500, checked right after parsing and before the

    RPC call (`400` with `classes must have at most 500 items`).


    **Not investment advice.** Every read here is a mechanical result of

    rules and data you supplied: a compliance check against your own

    declared bands, a rebalance preview toward your own declared targets.

    Nothing in this API opines on any security's merit, and it never says

    whether to buy, sell, or hold anything. Every write is confined to your

    own account and is always the absolute state you're asserting, never a

    delta and never an order. Allocait output states facts about

    the user's own rules. It is not advice or an approval. See the Terms of

    Service ("Not Investment Advice") for the full statement this API

    implements.


    **No stored answer about the person.** No field on this API records or

    returns how a person invests or which first-run lane they chose; the

    answer on the first screen is counted and not stored.


    **Two ranges on every rule.** A position rule carries an allocation band

    (`min_position_pct` / `max_position_pct`) on the asset class total, which

    is what `/compliance/check`, `/validate` and `/rebalance/preview`

    evaluate, and an optional position size range (`size_min_pct` /

    `size_max_pct`) for one holding, which `/compliance/check` and

    `/validate` report in `size_violations` and `/positions/size` sizes

    against. A rule may also carry a short size range

    (`short_size_min_pct` / `short_size_max_pct`) for one short holding,

    judged by its size without the minus sign. All are percentages of

    total portfolio value. A rule with a size range may have no allocation

    band: both band fields are then `null` and the class total is not

    judged, while each holding is. A band of 0 to 0 means none of the class.

    The snapshot

    also names the band `band_min_pct` / `band_max_pct` and the target

    `target_pct`, next to the older names, which stay.


    **Asset class names.** A body field that takes an asset class by name

    (`asset_class`) resolves it in one order: your own class with that

    name, then the system class, then the rules text alias (Stocks or ETFs →

    Equities, Bonds → Fixed Income, Forex → Currencies). Names compare

    case-insensitively after trimming, and an id is accepted too.


    **Duplicate names.** Every create route (`POST /portfolios`,

    `/scenarios`, `/asset-classes`, `/watchlists`, `/inputs`, `/playbooks`,

    `/monitors`, `/charts`) refuses a name that's already taken with `409`

    and `error_code: ALREADY_EXISTS`, carrying `existing_id`, so a retried

    create never makes a duplicate. Names compare case-insensitively after

    trimming: per portfolio for playbooks, monitors and charts, and per

    account for everything else. Names are at most 100 characters. Each

    account also has a ceiling on how many actions, custom asset classes,

    Inputs, recorded values, options, playbooks and charts it holds; a write

    past one is `TIER_LIMIT_EXCEEDED`, with `limit_type`, `current_count` and

    `max_allowed`.


    **Error codes and statuses.** Every refusal a route produces carries an

    `error_code` from one closed catalog shared with the MCP server:

    `INVALID_INPUT`, `NOT_FOUND`, `ALREADY_EXISTS`, `IN_USE`,

    `CONFIRMATION_REQUIRED`, `PRICES_INCOMPLETE`, `PRICES_INVALID`,

    `WOULD_CLOSE_POSITIONS`, `INSUFFICIENT_CASH`, `RULES_INVALID`,

    `TARGET_OUT_OF_BAND`, `TIER_LIMIT_EXCEEDED`, `FEATURE_NOT_AVAILABLE`,

    `IDEMPOTENCY_KEY_CONFLICT`, `DAILY_LIMIT_EXCEEDED`, `READ_ONLY_KEY`,

    `OAUTH_WRITE_NOT_GRANTED`, `TOOL_REMOVED` and `INTERNAL_ERROR`.

    `OAUTH_WRITE_NOT_GRANTED`, `WOULD_CLOSE_POSITIONS` and `TOOL_REMOVED`

    occur on MCP only. The status follows the code, on every route:

    `400` for `INVALID_INPUT`, `PRICES_INCOMPLETE`, `PRICES_INVALID`,

    `RULES_INVALID` and `TARGET_OUT_OF_BAND`; `403` for

    `TIER_LIMIT_EXCEEDED`, `FEATURE_NOT_AVAILABLE` and `READ_ONLY_KEY`;

    `404` for `NOT_FOUND`, which is also the answer for an id that is not

    yours, so an id cannot be probed; `409` for `ALREADY_EXISTS`, `IN_USE`,

    `CONFIRMATION_REQUIRED`, `INSUFFICIENT_CASH` and

    `IDEMPOTENCY_KEY_CONFLICT`; `429` for `DAILY_LIMIT_EXCEEDED`; `500` for

    `INTERNAL_ERROR`, which also carries a `request_id` to quote when you

    report it. `405` and `413` keep their HTTP statuses with

    `INVALID_INPUT`. Four answers are outside the catalog and carry no

    `error_code`: `401`, the no-plan `403`, the `403` for a key without

    `read`, and the `429` for the request budget (branch on the status for

    those). See the `Error` schema for the extra fields each code carries.
servers:
  - url: https://allocait.app/api/v1
    description: Hosted Allocait API
security:
  - ApiKeyAuth: []
tags:
  - name: Portfolios
    description: >-
      List, create, update and delete portfolios, and read one portfolio's full
      snapshot. The snapshot is the discovery call and lists the symbols you
      need to price.
  - name: Holdings and cash
    description: >-
      Read positions, set absolute share counts, size a position, and record
      deposits and withdrawals. Allocait records what you hold. It never places
      a trade.
  - name: Checks
    description: >-
      Compare a portfolio, or a change to it, with your rules at prices you
      supply. A check states how far a rule is outside its range. It never says
      what to trade.
  - name: Rules
    description: >-
      Scenarios, asset classes, allocation bands, position size ranges and the
      portable rules text, with restore points to undo an import.
  - name: Watchlists
    description: Named, ordered lists of symbols to follow.
  - name: Inputs
    description: >-
      Values you track by hand, such as a support level or a macro reading, with
      their dropdown options and recorded entries.
  - name: Playbooks
    description: >-
      Saved research views. A playbook resolves its symbols and shows the input
      values for any date you ask about, with each value's recorded date.
  - name: Monitors
    description: >-
      Condition monitors hold the levels or ranges you record for a symbol, read
      against the price you pass.
  - name: Charts
    description: Saved chart configurations for a portfolio.
  - name: Actions
    description: The record of position and cash changes, with an editable comment on each.
  - name: Webhooks
    description: Signed event notifications for compliance changes. Advanced plan.
  - name: Support and feedback
    description: >-
      File support requests and feedback about this API, and list your own. Each
      is capped at 20 per rolling 24 hours.
  - name: Preferences
    description: Which portfolio, watchlist, chart, playbook and monitor the app opens to.
  - name: Setup file
    description: >-
      Preview, apply and undo an ALLOCAIT.md setup file, and export the
      definitions of your watchlists, Inputs, playbooks, monitors and saved
      charts as one versioned JSON file. Definitions only, never a value, price
      or holding.
externalDocs:
  description: API quickstart
  url: https://docs.allocait.app/api/quickstart
paths:
  /trades/check:
    post:
      tags:
        - Checks
      summary: Check a proposed change against the rules
      description: >
        States which of the user's own rules a proposed change puts outside

        its range or leaves further outside, using prices you supply. It

        reads and writes nothing, decides nothing and needs only `read`, so

        a Core account's key can call it. A result inside every range does

        not say a change is advisable. Allocait cannot approve, stop or

        place an order.


        `trades` is the whole change, 1 to 50 items, applied together and in

        any order: several trades in one symbol net. `side` is the direction

        of the change being asked about: `BUY` adds shares to the holding,

        `SELL` removes shares. Send `shares` (fractions allowed) or

        `notional` (dollars, at the price in `prices`), not both. A `SELL`

        larger than the shares held is `INVALID_INPUT`, never clamped,

        unless the portfolio has `allow_shorts` on: then a `SELL` can take

        a holding below zero and a `BUY` can take a short above it, and

        `shares_after` is signed (negative is a short). A holding in the

        Options class never goes below zero, and a change worth

        100000000000000 or more cannot be recorded, so it is refused. `CASH`

        is not tradable. A symbol the portfolio does not hold needs

        `asset_class` (a class name, rules text alias or id); a held symbol

        keeps the class it is recorded under.


        `prices` is required and must cover every non-cash holding and every

        traded symbol, except a held option with a `mark` or one past its

        expiry, which is priced from the user's own number or at 0 (ADR-147).

        A hole is a `400` with `error_code: PRICES_INCOMPLETE`

        and `missing_symbols`; Allocait never fills a symbol you left out

        from its own market data (ADR-036). A held symbol may be priced 0 (a

        worthless option); a price you pass for an unexpired option replaces

        its mark for this call only. A traded symbol priced 0 or less is

        `PRICES_INVALID`. Prices are used for this call only and never

        stored.


        A trade in a held option that has expired is not checked, because the

        option counts 0 and a trade cannot change the book. It is

        `INVALID_INPUT` and the message names the trade's index and symbol and

        the write that closes it (`shares` 0 and `price` 0).


        `scenario_id` defaults to the portfolio's active scenario. A

        portfolio with none set needs `scenario_id` (a 400 that says so).


        **Reading the result.** `rules` lists only the rules outside their

        range before or after the change. Rules inside both are counted in

        `rules_judged_count`. `change` describes a rule's distance from its

        range, never the trade. `newly_or_further_outside_count` answers

        "did this change add a breach or deepen one". A bot that must do

        neither checks that it equals 0.


        `within_rules` answers a different question: is the whole book

        inside every rule after the change. It is true only when no rule is

        outside its range after the change, `rules_judged_count` is above 0

        and `cash.sufficient` is true. Treat `false` as "not confirmed",

        never as a block Allocait enforces. Treat `true` as saying nothing

        about any symbol no rule covers; `not_covered` lists those

        symbols, traded or held.


        `exposure_after` says how much of the book gains if prices rise

        once the whole change is applied: long, short, net and gross as

        percents of `total_value`, at your prices (`shares x price x moves`;

        a holding's Moves is the setting the user recorded, 1 unless set). It

        is a reading beside the rules, not part of the verdict, and

        `within_rules` does not read it. Options count at their premium, a

        call as long and a put as short, and an expired option counts 0

        (ADR-147).


        Report `positions_updated_at` with the verdict. The book is what

        Allocait has recorded. Allocait does not read the live account. The
        symbols in the

        examples are fictional and their numbers are made up.
      operationId: checkTrades
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CheckTradesRequest'
            example:
              portfolio_id: 6f0f6b32-0a52-4c0e-9b7e-2f8a4f1b9a10
              trades:
                - symbol: ALFA
                  side: BUY
                  shares: 40
                - symbol: GOLF
                  side: SELL
                  shares: 10
              prices:
                ALFA: 181.5
                BRAV: 98.1
                ECHO: 41.6
                GOLF: 62.3
      responses:
        '200':
          description: >-
            The rules the change puts outside their range, and the whole book
            after the change
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CheckTradesResult'
              example:
                portfolio_id: 6f0f6b32-0a52-4c0e-9b7e-2f8a4f1b9a10
                scenario_id: c1a7e0d4-3a52-4b1f-8f4b-0e6b2d9f7a21
                within_rules: false
                newly_or_further_outside_count: 2
                rules_judged_count: 9
                rules:
                  - kind: position_size
                    symbol: ALFA
                    asset_class_id: 3d5b0c1e-7a44-4c52-9a31-5c2f8e0b6d12
                    asset_class: Equities
                    min_pct: 3
                    max_pct: 12
                    before_pct: 10.89
                    after_pct: 13.79
                    before_status: COMPLIANT
                    after_status: ABOVE_MAX
                    gap_before_pts: 0
                    gap_after_pts: 1.79
                    change: newly_outside
                  - kind: class_band
                    symbol: null
                    asset_class_id: 8e2a4d7b-5c19-4f60-b3a8-1d9e6c0f4a35
                    asset_class: Commodities
                    min_pct: 3
                    max_pct: 11
                    before_pct: 2.59
                    after_pct: 2.34
                    before_status: BELOW_MIN
                    after_status: BELOW_MIN
                    gap_before_pts: 0.41
                    gap_after_pts: 0.66
                    change: further_outside
                not_covered:
                  - symbol: GOLF
                    asset_class: Commodities
                    reason: NO_SIZE_RANGE
                    traded: true
                trades:
                  - index: 0
                    symbol: ALFA
                    side: BUY
                    shares: 40
                    price: 181.5
                    notional: 7260
                    shares_before: 150
                    shares_after: 190
                  - index: 1
                    symbol: GOLF
                    side: SELL
                    shares: 10
                    price: 62.3
                    notional: 623
                    shares_before: 104
                    shares_after: 94
                cash:
                  before: 12000
                  after: 5363
                  sufficient: true
                total_value: 250000
                exposure_after:
                  total_value: 250000
                  long_pct: 97.85
                  short_pct: 0
                  net_pct: 97.85
                  gross_pct: 97.85
                  options_counted: 0
                  options_not_counted: 0
                positions_updated_at: '2026-10-09T18:22:04Z'
                basis: >-
                  Computed from this rule set's saved ranges, the holdings
                  Allocait has recorded, and the prices you supplied. Taxes,
                  fees, liquidity and accounts outside Allocait are not
                  considered.
                checked_at: '2026-10-11T14:03:11Z'
        '400':
          description: >-
            `INVALID_INPUT` (shape, side, both or neither of `shares` and
            `notional`, an amount of 0 or less, more than 50 trades, a sale
            larger than the shares held (with `allow_shorts` off), a sale that
            takes an Options holding below zero, a change worth 100000000000000
            or more, a symbol not held without `asset_class`, a portfolio worth
            nothing or less at these prices, a `portfolio_id` or `scenario_id`
            that is not a UUID; the message names the trade's index, from 0, and
            symbol), `PRICES_INCOMPLETE` (carries `missing_symbols`) or
            `PRICES_INVALID` (a traded symbol priced 0 or less, a price above
            1000000000, or a price that is not a number)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CheckTradesError'
              example:
                error: A price is needed for ECHO.
                error_code: PRICES_INCOMPLETE
                missing_symbols:
                  - ECHO
        '401':
          description: Invalid or missing API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: Key lacks `read` permission, or API access needs an active plan
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: >-
            Portfolio or scenario not found or not yours, or an `asset_class`
            that resolves to nothing (`NOT_FOUND`; carries `valid_values`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/ServerError'
components:
  schemas:
    CheckTradesRequest:
      type: object
      required:
        - portfolio_id
        - trades
        - prices
      properties:
        portfolio_id:
          type: string
          format: uuid
        scenario_id:
          type: string
          format: uuid
          description: Default is the portfolio's active scenario, and only that (ADR-232).
        trades:
          type: array
          minItems: 1
          maxItems: 50
          description: The whole change, checked together. Trades in one symbol net.
          items:
            type: object
            required:
              - symbol
              - side
            properties:
              symbol:
                type: string
                example: ALFA
              side:
                type: string
                enum:
                  - BUY
                  - SELL
                description: >-
                  The direction of the change being asked about. `BUY` adds
                  shares to the holding, `SELL` removes shares. On a portfolio
                  with `allow_shorts` on, a `SELL` can take a holding below zero
                  and a `BUY` can take a short above it.
              shares:
                type: number
                exclusiveMinimum: 0
                description: >-
                  Shares added or removed; fractions allowed. Send `shares` or
                  `notional`, not both.
                example: 40
              notional:
                type: number
                exclusiveMinimum: 0
                description: >-
                  Dollars added or removed, at the price in `prices`. Send
                  `shares` or `notional`, not both.
              asset_class:
                type: string
                description: >-
                  A class name, rules text alias or id. Required for a symbol
                  the portfolio does not hold; a held symbol keeps its recorded
                  class.
        prices:
          $ref: '#/components/schemas/PriceOverrides'
    CheckTradesResult:
      type: object
      required:
        - portfolio_id
        - scenario_id
        - within_rules
        - newly_or_further_outside_count
        - rules_judged_count
        - rules
        - not_covered
        - trades
        - cash
        - total_value
        - positions_updated_at
        - basis
        - checked_at
      properties:
        portfolio_id:
          type: string
          format: uuid
        scenario_id:
          type: string
          format: uuid
        within_rules:
          type: boolean
          description: |
            True only when no rule is outside its range after the change,
            `rules_judged_count` is above 0 and `cash.sufficient` is true. A
            breach that existed before keeps it false. `false` with
            `rules_judged_count: 0` means no rule was checked. Treat `false`
            as "not confirmed", never as a block Allocait enforces, and
            `true` as saying nothing about anything no rule covers.
        newly_or_further_outside_count:
          type: integer
          description: >-
            Rules with `change` `newly_outside` or `further_outside`. 0 means
            the change added no breach and deepened none.
        rules_judged_count:
          type: integer
          description: >-
            Class bands in the scenario plus holdings size-judged after the
            change (non-cash, above 0%, in a class with a range for the side the
            holding ends on: the position size range for a long, the short size
            range for a short).
        rules:
          type: array
          description: >-
            Only rules outside their range before or after the change. Rules
            inside both are counted in `rules_judged_count`, not listed.
          items:
            $ref: '#/components/schemas/CheckRuleRow'
        not_covered:
          type: array
          description: >-
            One entry per symbol no rule judges, so `within_rules: true` is
            never read as "a rule looked at this symbol". Traded symbols, and
            held symbols the change does not touch that have no class or that no
            rule judges (`traded: false`).
          items:
            type: object
            required:
              - symbol
              - asset_class
              - reason
              - traded
            properties:
              symbol:
                type: string
              asset_class:
                type:
                  - string
                  - 'null'
              traded:
                type: boolean
                description: >-
                  `false`: a holding the change does not touch, listed because
                  no rule in this scenario judges it (`NO_CLASS_RULE`).
              reason:
                type: string
                enum:
                  - NO_CLASS_RULE
                  - NO_SIZE_RANGE
                description: >-
                  `NO_CLASS_RULE`: no band and no size range for its side judges
                  the holding in this scenario (or the holding has no class).
                  `NO_SIZE_RANGE`: the class has a rule but no range for the
                  side the symbol ends on: no position size for a long, no short
                  size for a short. A symbol sold to zero from a short is named
                  only when the class has neither.
        trades:
          type: array
          description: One row per trade as sent.
          items:
            type: object
            required:
              - index
              - symbol
              - side
              - shares
              - price
              - notional
              - shares_before
              - shares_after
            properties:
              index:
                type: integer
                description: Position in the `trades` you sent, from 0.
              symbol:
                type: string
              side:
                type: string
                enum:
                  - BUY
                  - SELL
              shares:
                type: number
                example: 40
              price:
                type: number
                example: 181.5
              notional:
                type: number
                example: 7260
              shares_before:
                type: number
                example: 150
              shares_after:
                type: number
                description: >-
                  The symbol's shares once the whole change is applied, the same
                  in every row for that symbol. Signed, so a negative number is
                  a short.
                example: 190
        cash:
          type: object
          required:
            - before
            - after
            - sufficient
          properties:
            before:
              type: number
            after:
              type: number
            sufficient:
              type: boolean
              description: '`after` is 0 or more.'
        total_value:
          type: number
          description: >-
            Priced non-cash holdings plus cash, at the prices you passed. The
            same before and after.
        exposure_after:
          $ref: '#/components/schemas/ExposureAfter'
        positions_updated_at:
          type:
            - string
            - 'null'
          format: date-time
          description: >-
            When the newest holding record changed, null with no holdings.
            Report it with the verdict.
        basis:
          type: string
          description: >-
            What the result is computed from (the saved ranges, the recorded
            holdings, the prices you supplied). Always the same sentence.
        checked_at:
          type: string
          format: date-time
    CheckTradesError:
      type: object
      required:
        - error
        - error_code
      properties:
        error:
          type: string
        error_code:
          type: string
          enum:
            - INVALID_INPUT
            - NOT_FOUND
            - PRICES_INCOMPLETE
            - PRICES_INVALID
        missing_symbols:
          type: array
          items:
            type: string
          description: >-
            With `PRICES_INCOMPLETE`: held and traded symbols absent from
            `prices`, sorted.
        valid_values:
          type: array
          items:
            type: string
          description: 'With `NOT_FOUND` for an `asset_class`: the class names that resolve.'
    Error:
      type: object
      description: >
        Uniform error envelope returned by every non-2xx response. `error` is

        a human-readable message, safe to show the user. A refusal also

        carries `error_code`, a stable string from the catalog in the info

        block's Error codes section, to branch on; the HTTP status follows

        the code (ADR-134). Only `401`, the no-plan `403`, the `403` for a

        key without `read` and the `429` for the request budget have none.

        A `500` carries `request_id`. Extra fields by code:

        TIER_LIMIT_EXCEEDED carries `limit_type`, `current_count` and

        `max_allowed`, so a caller can branch on how many it has versus the

        cap without a second call. Its `error` text names no plan. It covers

        the portfolio and scenario limits and the account caps: 20 webhook
        endpoints, 50

        watchlists, 200 symbols on one watchlist and 50 custom symbols on one

        monitor. Don't retry it. RULES_INVALID (`POST

        /scenarios/{id}/rules`) carries `issues`. ALREADY_EXISTS carries

        `existing_id`, plus `duplicate_ids` where older duplicates exist.

        CONFIRMATION_REQUIRED carries `would_remove`; from `POST /setup/apply`
        it

        carries a fresh preview instead (`digest`, `file`, `items` and the rest
        of

        `SetupPreview`). INVALID_INPUT from the setup routes carries `issues`,
        and

        IN_USE from `POST /setup/receipts/{id}/undo` carries `later_receipt_id`.

        NOT_FOUND on a name, and

        INVALID_INPUT on a CHOICE value, carry `valid_values`. INVALID_INPUT on
        a

        monitor whose level input is not a number type carries `input_id`.

        IN_USE on an input whose type would change while a monitor uses it

        carries `monitors`. IN_USE on `PATCH /portfolios/{id}` that turns

        Allow shorts off while a short is held carries `short_count` and

        `symbols`. INVALID_INPUT on the holdings writes (`POST

        /positions/{symbol}`, `POST /portfolios/{id}/positions/apply`) that

        refuses a negative `shares`, a `moves` other than 1 on an Options

        holding, or a `mark` on a holding outside the Options class, carries

        `symbols`, the holdings it refused. (`symbols` also appears on
        `WOULD_CLOSE_POSITIONS`, MCP

        only, where it lists the holdings a full list left out.)
      required:
        - error
      example:
        error: Portfolio not found or access denied
        error_code: NOT_FOUND
      properties:
        error:
          type: string
          description: Human-readable error message.
        error_code:
          type: string
          enum:
            - INVALID_INPUT
            - NOT_FOUND
            - ALREADY_EXISTS
            - IN_USE
            - CONFIRMATION_REQUIRED
            - PRICES_INCOMPLETE
            - PRICES_INVALID
            - WOULD_CLOSE_POSITIONS
            - INSUFFICIENT_CASH
            - RULES_INVALID
            - TARGET_OUT_OF_BAND
            - TIER_LIMIT_EXCEEDED
            - FEATURE_NOT_AVAILABLE
            - IDEMPOTENCY_KEY_CONFLICT
            - DAILY_LIMIT_EXCEEDED
            - READ_ONLY_KEY
            - OAUTH_WRITE_NOT_GRANTED
            - TOOL_REMOVED
            - INTERNAL_ERROR
          description: >-
            Stable machine-readable code from the closed catalog. Present on
            every refusal a route produces; absent on `401`, the no-plan `403`,
            the `403` for a key without `read`, and the `429` for the request
            budget.
        request_id:
          type: string
          format: uuid
          description: >-
            Present on a `500` (`INTERNAL_ERROR`). The same id is in Allocait's
            logs next to the real cause; quote it when you report the failure.
        issues:
          type: array
          items:
            anyOf:
              - $ref: '#/components/schemas/RulesIssue'
              - $ref: '#/components/schemas/SetupIssue'
          description: >-
            Every error and warning found while reading the rules text, when
            error_code is RULES_INVALID. From the setup routes (`POST
            /setup/preview`, `POST /setup/apply`) it is `SetupIssue` rows, on
            INVALID_INPUT.
        later_receipt_id:
          type: string
          format: uuid
          description: >-
            The newer import of the same setup file that blocks this undo.
            Present on IN_USE from `POST /setup/receipts/{id}/undo`; undo that
            receipt first.
        limit_type:
          type: string
          description: >-
            Which cap was hit, e.g. "portfolios", "scenarios",
            "webhook_endpoints", "watchlists", "asset_classes", "input_entries"
            or "input_entries_per_input". Present only when error_code is
            TIER_LIMIT_EXCEEDED.
        current_count:
          type: number
          description: >-
            How many of limit_type the caller has right now. Present only when
            error_code is TIER_LIMIT_EXCEEDED.
        max_allowed:
          type: number
          description: >-
            The cap for limit_type on this account. Present only when error_code
            is TIER_LIMIT_EXCEEDED.
        existing_id:
          type: string
          format: uuid
          description: >-
            The record that already has this name. Present only when error_code
            is ALREADY_EXISTS; change it with that id instead of creating
            another.
        duplicate_ids:
          type: array
          items:
            type: string
            format: uuid
          description: >-
            Other records with the same name, created before duplicates were
            refused. Present only on ALREADY_EXISTS, and only when such records
            exist.
        would_remove:
          type: object
          additionalProperties:
            type: array
            items:
              type: string
          description: >-
            What the call would drop, keyed by list field, for example the
            `items` that a watchlist save would remove. Present only when
            error_code is CONFIRMATION_REQUIRED; resend with those members in
            `remove`.
        valid_values:
          type: array
          items:
            type: string
          description: >-
            The names or options the field accepts. Present on NOT_FOUND for a
            name, and on INVALID_INPUT for a CHOICE value.
        input_id:
          type: string
          format: uuid
          description: >-
            The level input that is not a NUMBER, DECIMAL or PERCENTAGE input.
            Present on INVALID_INPUT from a monitor create or update (`POST
            /monitors`, `PATCH /monitors/{id}`, MCP `save_monitor`); point the
            monitor at a number-type input instead.
        short_count:
          type: integer
          description: >-
            How many shorts the portfolio holds. Present on IN_USE from `PATCH
            /portfolios/{id}` (MCP `save_portfolio`) that turns Allow shorts off
            while a short is held; close them first.
        symbols:
          type: array
          items:
            type: string
          description: >-
            Holdings the call names. On IN_USE from `PATCH /portfolios/{id}`,
            the shorts still held (up to 20, by symbol). On INVALID_INPUT from
            the holdings writes, the holdings whose negative `shares` was
            refused (Allow shorts is off, or the holding is in Options), or
            whose `moves` other than 1 was refused (the holding is in Options).
            On WOULD_CLOSE_POSITIONS (MCP only), the holdings a full list left
            out.
        monitors:
          type: array
          description: >-
            The monitors that use this input as a level. Present on IN_USE from
            an input update (`PATCH /inputs/{id}`, MCP `save_input`) that would
            change `data_type` to TEXT or CHOICE; move those monitors to another
            input first.
          items:
            type: object
            properties:
              id:
                type: string
                format: uuid
              name:
                type: string
              portfolio_id:
                type: string
                format: uuid
    PriceOverrides:
      type: object
      description: |
        Symbol-to-price map. Keys are ticker symbols, matched
        case-insensitively (normalised to uppercase); values are numbers,
        0 or more. 0 is a real price, for example an option that expired
        worthless. On the agent surface, a symbol you leave out of this map is
        never priced from Allocait's own data (ADR-036); it comes back
        null, or the call is refused, depending on the endpoint. Two
        exceptions are the user's own numbers, not Allocait's: a held option
        with a `mark` is priced at its mark, so you may leave it out, and a
        price you pass for it replaces the mark for that call only and is
        never stored; an option past its expiry counts 0 whatever you pass
        (ADR-147).
      additionalProperties:
        type: number
        minimum: 0
      example:
        ALFA: 181.5
        ECHO: 98.1
        GOLF: 62.3
    CheckRuleRow:
      type: object
      description: One rule that is outside its range before or after the change.
      required:
        - kind
        - symbol
        - asset_class_id
        - asset_class
        - min_pct
        - max_pct
        - before_pct
        - after_pct
        - before_status
        - after_status
        - gap_before_pts
        - gap_after_pts
        - change
      properties:
        kind:
          type: string
          enum:
            - class_band
            - position_size
            - short_position_size
          description: >-
            `class_band` is a class's allocation band; `position_size` is one
            long holding against its class's size range; `short_position_size`
            is one short holding against its class's short size range. Both size
            kinds judge the size without a sign (ADR-144).
        symbol:
          type:
            - string
            - 'null'
          description: >-
            The holding for a `position_size` or `short_position_size` rule;
            null for a `class_band` rule.
        asset_class_id:
          type: string
          format: uuid
        asset_class:
          type: string
        min_pct:
          type: number
          description: The rule's lower end, in percent of total value.
        max_pct:
          type: number
          description: The rule's upper end.
        before_pct:
          type: number
          description: >-
            Percent of total value before the change. 0 for a holding not held
            yet.
        after_pct:
          type: number
          description: >-
            Percent of total value after the change. 0 for a holding taken to
            zero.
        before_status:
          type: string
          enum:
            - COMPLIANT
            - BELOW_MIN
            - ABOVE_MAX
        after_status:
          type: string
          enum:
            - COMPLIANT
            - BELOW_MIN
            - ABOVE_MAX
        gap_before_pts:
          type: number
          description: >-
            Distance outside the range, in percentage points. 0 when inside
            (tolerance 0.01 point).
        gap_after_pts:
          type: number
          description: >-
            Distance outside the range after the change, in percentage points. 0
            when inside.
        change:
          type: string
          enum:
            - newly_outside
            - further_outside
            - still_outside
            - back_inside
          description: |
            A rule's distance from its range, never a judgment of the trade.
            `newly_outside`: inside before, outside after (a size breach on a
            first buy is this). `further_outside`: outside both, and the gap
            grew by more than 0.01 point. `still_outside`: outside both and not
            further, including closer but still outside. `back_inside`: outside
            before, inside after (a size breach on a holding taken to zero is
            this). Sorted in that order, then `class_band` before
            `position_size` before `short_position_size`, then by name.
            A symbol that crosses zero is judged on the side it ends on, so
            a breach of the range it left is not reported as `back_inside`.
    ExposureAfter:
      type: object
      description: |
        The exposure reading once the whole change is applied, at the prices
        you supplied: the totals of `ExposureReading` without the by-class
        and per-holding lists. A fact about the book after the change, not a
        verdict, and `within_rules` does not read it. A symbol the change
        opens counts at Moves 1 in the class the trade named. Options count at
        their premium, as in `ExposureReading` (ADR-147).
      required:
        - total_value
        - long_pct
        - short_pct
        - net_pct
        - gross_pct
        - options_counted
        - options_not_counted
      properties:
        total_value:
          type: number
          description: The same as the check's `total_value`.
        long_pct:
          type:
            - number
            - 'null'
        short_pct:
          type:
            - number
            - 'null'
          description: Positive.
        net_pct:
          type:
            - number
            - 'null'
        gross_pct:
          type:
            - number
            - 'null'
        options_counted:
          type: integer
          description: Options inside the figures, as in `ExposureReading`.
        options_not_counted:
          type: integer
          description: >-
            Priced options left out because the symbol is not an option symbol,
            as in `ExposureReading`.
    RulesIssue:
      type: object
      description: |
        One problem or note found while reading rules text. A file
        carrying any `error`-level issue is never applied; `POST
        /scenarios/{id}/rules` 400s with `error_code: RULES_INVALID` and
        the full `issues` list instead. A file with only `warning`-level
        issues still applies; those ride along on the 200 response.
      required:
        - level
        - message
      properties:
        level:
          type: string
          enum:
            - error
            - warning
        message:
          type: string
        class:
          type: string
          description: >-
            The asset class name this issue is about, when it is about one class
            rather than the whole file.
    SetupIssue:
      type: object
      required:
        - level
        - where
        - message
      description: >-
        One thing the file's text got wrong (`error`) or one note about it
        (`warning`, such as a web address in a description).
      properties:
        level:
          type: string
          enum:
            - error
            - warning
        where:
          type:
            - string
            - 'null'
          description: Where in the file, or null for the file as a whole.
        line:
          type: integer
          description: The line of the file the issue is on, when it is known.
        message:
          type: string
  headers:
    RateLimitLimit:
      description: >-
        The per-minute request budget, always `60`. Sent on every response that
        reaches a route and on the per-minute `429`.
      schema:
        type: integer
        example: 60
    RateLimitRemaining:
      description: >-
        Requests left in the current one-minute window, after this one. The
        daily count is not reported until it is reached.
      schema:
        type: integer
        example: 41
    RetryAfter:
      description: >-
        Seconds to wait before retrying. Sent only on the request budget's own
        `429`s, never on a per-route cap (`DAILY_LIMIT_EXCEEDED`) or any other
        status.
      schema:
        type: integer
        example: 60
  responses:
    RateLimited:
      description: |
        Rate limit exceeded: maximum 60 requests per minute per account,
        or 10,000 per UTC day (see the info block's Rate limits section).
        The per-minute refusal carries `Retry-After: 60` and the
        `X-RateLimit-*` headers; the daily refusal carries `Retry-After`
        set to the seconds until 00:00 UTC and no `X-RateLimit-*` headers.
        Neither has an `error_code`. `POST /support-requests` and
        `POST /agent-feedback` also return `429` with `error_code:
        DAILY_LIMIT_EXCEEDED` past 20 filed per rolling 24 hours; that one
        carries the `X-RateLimit-*` headers and no `Retry-After`.
      headers:
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
        X-RateLimit-Limit:
          $ref: '#/components/headers/RateLimitLimit'
        X-RateLimit-Remaining:
          $ref: '#/components/headers/RateLimitRemaining'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    ServerError:
      description: |
        Unexpected server error. `error_code` is `INTERNAL_ERROR` and
        `request_id` names this failure: quote it in `POST /agent-feedback`.
        The body never carries a schema, column or constraint name.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: Internal error
            error_code: INTERNAL_ERROR
            request_id: 5b1e7c52-3f4a-4d0e-9a55-2d5cbe6f0a11
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: x-api-key
      description: >-
        Raw API key shown once when created in the Allocait app (Settings > API
        keys). Send it verbatim in this header; the server stores and compares
        only its SHA-256 hash. Allocait generates every key: `alc_` followed by
        64 lowercase hex characters. Keys issued before 2026-10-10 are 64 hex
        characters with no prefix and keep working. A key may carry an expiry
        (90 days or 1 year, or never). A malformed, unknown, revoked or expired
        key gets `401` with no further detail, and a malformed one is refused
        before any lookup. Keys cannot be created through this API.

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.