Plinth US Grants Data · OpenAPI Overlay 1.0.0

API Evangelist enhancements — Plinth Grants API

12 actions 12 updates update extends openapi/plinth-us-grants-data-openapi.json
Derived by API Evangelist Built from the contracts Plinth US Grants Data publishes. Plinth US Grants Data did not publish this file.
View Overlay File View on GitHub Overlay Specification

What the actions change

x-sourcex-documented-enumx-semantic-typex-documented-meaningx-machine-discoveryx-response-headersx-meteringx-data-caveats

Targets 12

$.info
$.components
$.paths['/api/grants/transactions'].get.parameters[?(@.name=='year')]
$.paths['/api/grants/transactions'].get.parameters[?(@.name=='limit')]
$.paths['/api/grants/transactions'].get.parameters[?(@.name=='sort_by')]
$.paths['/api/grants/transactions'].get.parameters[?(@.name=='sort_order')]
$.paths['/api/grants/transactions'].get.parameters[?(@.name=='location')]
$.paths['/api/search'].get.parameters[?(@.name=='mode')]
$.paths['/api/search'].get
$.paths['/api/sql'].post
$.paths['/api/analyze'].post
$.paths

OpenAPI Overlay

Raw ↑
overlay: 1.0.0
info:
  title: API Evangelist enhancements — Plinth Grants API
  version: 1.0.0
  x-generated: '2026-08-14'
  x-method: derived
  x-source: >-
    Derived from openapi/plinth-us-grants-data-openapi.json (OpenAPI 3.1.0, 10 operations) plus the
    provider's own published documentation: https://data.useplinth.com/developers,
    /developers/schema, /developers#access, /developers#auth and /.well-known/api-onboarding. Every
    value added below is quoted or paraphrased from a Plinth-published surface — nothing is
    invented. The base spec is never mutated.
  x-extends: openapi/plinth-us-grants-data-openapi.json
  x-rationale: >-
    Plinth's spec is generated from its own route signatures and passes its own published Spectral
    ruleset, which makes it accurate but thin in three specific places: (1) every 200 is declared
    `schema: {}`, so no response entity is modelled; (2) every query parameter is typed
    `anyOf [string, null]`, so integers read as strings and enums carry no enum; (3) two real error
    statuses on the SQL surface (400 and 403) are documented in prose but absent from the contract.
    This overlay records the facts that would close those gaps, sourced from the docs, so a
    consumer can apply them locally. It is an ANNOTATION of what Plinth already publishes, not a
    redesign, and the right long-term fix is upstream — because the spec is route-generated, adding
    response models and parameter types to the routes would produce these automatically.

extends: openapi/plinth-us-grants-data-openapi.json

actions:
  # ── Discovery / runtime affordances the spec does not carry ──────────────────────────────
  - target: $.info
    description: >-
      Record the machine-discovery surface and the runtime signalling that Plinth serves but the
      spec does not mention.
    update:
      x-machine-discovery:
        apis_json: https://data.useplinth.com/.well-known/apis.json
        apis_json_version: '0.19'
        api_catalog: https://data.useplinth.com/.well-known/api-catalog
        api_catalog_spec: RFC 9727
        security_txt: https://data.useplinth.com/.well-known/security.txt
        onboarding_descriptor: https://data.useplinth.com/.well-known/api-onboarding
        llms_txt: https://data.useplinth.com/llms.txt
        spectral_ruleset: https://data.useplinth.com/spectral/grants-api.yaml
        mcp_endpoint: https://data.useplinth.com/api/connector/mcp
      x-response-headers:
        link: >-
          Every /api response carries `link: </.well-known/api-catalog>; rel="api-catalog",
          </openapi.json>; rel="service-desc"; type="application/json", </developers>;
          rel="service-doc"; type="text/html"` — observed live 2026-08-14 on GET /api/search and on
          the MCP endpoint's 401.
        x-calls-limit: The account's monthly call allowance (keyed responses only).
        x-calls-remaining: Calls still available this month (keyed responses only).
      x-metering:
        model: monthly-call-allowance
        free_tier_calls_per_month: 50
        paid_tier_calls_per_month: 10000
        status_on_exhaustion: 402
        never_returns: 429
        cache_hits_are_billed: true
        note: >-
          "a repeat call is a repeat call against your allowance even when we serve it from memory"
          (/developers). One call per request regardless of page size, so a large `limit` is
          strictly cheaper than paging.
      x-data-caveats:
        source_lag: >-
          IRS e-file data is released on a 12-24 month lag; every figure is dated to its fiscal year
          rather than to today.
        refresh_cadence: monthly
        causation: Funding relationships are reported as association, never as causation.
        cause_coverage: >-
          Only grants with a matched recipient_ein carry an NTEE, so any by-cause dollar total
          covers ~67% of grant dollars.
        methodology: https://data.useplinth.com/methodology

  # ── Global response envelope, which the spec does not model at all ───────────────────────
  - target: $.components
    description: >-
      Add the success envelope Plinth documents on /developers ("Response shape") and returns on
      every list operation. The spec models three error schemas and no success shape.
    update:
      x-response-envelope:
        documented_at: https://data.useplinth.com/developers
        list_shape: '{ "code": 200, "message": "Request was processed successfully!", "hits": integer, "page": integer, "limit": integer, "results": [ ... ] }'
        summary_shape: '{ "summary": { ... }, "by_year": [ ... ] }'
        hits_semantics: total matching the filter, not the page size
        note: >-
          `code` mirrors the HTTP status inside the body on both success and failure, so a client
          can branch on the body alone.

  # ── Parameter typing: documented semantics the anyOf[string,null] shape loses ─────────────
  - target: $.paths['/api/grants/transactions'].get.parameters[?(@.name=='year')]
    description: Record the documented semantics of `year`, typed as a nullable string in the spec.
    update:
      x-semantic-type: integer
      x-example: '2023'
      x-documented-meaning: Filing fiscal year.
      x-source: https://data.useplinth.com/developers

  - target: $.paths['/api/grants/transactions'].get.parameters[?(@.name=='limit')]
    description: Record the documented page-size ceiling, which the spec does not express.
    update:
      x-semantic-type: integer
      x-documented-maximum: 1000
      x-cost-note: >-
        One call is billed per request regardless of page size, so requesting the maximum is
        strictly cheaper than paging.
      x-source: https://data.useplinth.com/developers

  - target: $.paths['/api/grants/transactions'].get.parameters[?(@.name=='sort_by')]
    description: Record the documented value set, which the spec types as a bare nullable string.
    update:
      x-documented-enum: [amount, year]
      x-source: https://data.useplinth.com/developers

  - target: $.paths['/api/grants/transactions'].get.parameters[?(@.name=='sort_order')]
    description: Record the documented value set.
    update:
      x-documented-enum: [asc, desc]
      x-source: https://data.useplinth.com/developers

  - target: $.paths['/api/grants/transactions'].get.parameters[?(@.name=='location')]
    description: Record that `location` filters on the RECIPIENT's US state, not the funder's.
    update:
      x-documented-meaning: Recipient US state, two-letter (e.g. MA).
      x-source: https://data.useplinth.com/developers

  - target: $.paths['/api/search'].get.parameters[?(@.name=='mode')]
    description: >-
      Record the value set and the silent-downgrade behaviour, both of which live only in the
      parameter description prose.
    update:
      x-documented-enum: [text, semantic, hybrid]
      x-default: text
      x-downgrade-behaviour: >-
        semantic/hybrid fall back to text if vector matching is unavailable. The response echoes
        `mode` with the mode actually used — verified live 2026-08-14, a request with no mode
        returned {"mode":"text",...}. A client that needs semantic matching must check it.
      x-source: openapi parameter description + live probe

  # ── The unmetered front door, worth flagging to any generated client ─────────────────────
  - target: $.paths['/api/search'].get
    description: >-
      Flag the one operation that needs no credential. This is the intended first call and it is
      free — the highest-value fact in the whole surface for an agent.
    update:
      x-no-auth-required: true
      x-metered: false
      x-agent-note: >-
        Entity resolution is open. Resolve a name to an EIN and a canonical page URL with no key and
        no allowance cost, then spend allowance only on the enriched calls. Plinth's own onboarding
        descriptor calls this "the intended first call."
      x-returns:
        observed_fields: [ein, name, kind, slug, state, cause, href, revenue, score, url, location, type]
        observed_at: '2026-08-14'
        note: >-
          Observed on a live 200 for q=barancik; the spec declares `schema: {}`. `url` is the
          canonical citable HTML page for the organization.

  # ── SQL surface: two real error statuses and two ceilings absent from the contract ───────
  - target: $.paths['/api/sql'].post
    description: >-
      Add the documented ceilings and the two error statuses that appear on
      https://data.useplinth.com/developers/schema but not in the spec. Plinth's own Spectral rule
      `plinth-metered-errors-documented` requires 401 and 402 on keyed operations; it does not reach
      400 or 403, which is why these are missing.
    update:
      x-undeclared-responses:
        '400':
          meaning: Query cancelled after exceeding the 30-second execution ceiling.
          remediation: Narrow with a tax_year or funder_ein filter, or aggregate in SQL.
          source: https://data.useplinth.com/developers/schema
        '403':
          meaning: >-
            The query named a warehouse table the account's plan does not include. Returned "rather
            than a partial answer."
          gated_tables: [org_asset_profile, foundation_holdings, holding_entity, people, board_link, org_families, gov_funding_federal, gov_funding_state, uk_charity_trustee, uk_board_edge]
          remediation: Remove the gated table, or upgrade to the For consultants plan.
          source: https://data.useplinth.com/developers/schema
      x-ceilings:
        max_rows: 2000
        truncation_signal: '`truncated: true` inside a 200 response body'
        truncation_warning: >-
          An agent that ignores `truncated` will silently report an aggregate computed over a
          truncated set.
        timeout_seconds: 30
      x-accepted-sql: single SELECT, or WITH ... SELECT
      x-rejected-sql: multiple statements, any DDL/DML, and the file-reading functions (read_parquet, read_csv)
      x-plan-gate: paid keys only — there is no free SQL tier
      x-schema-reference: https://data.useplinth.com/developers/schema
      x-warehouse-tables: 31

  # ── The SSE surface, undeclared in the contract ──────────────────────────────────────────
  - target: $.paths['/api/analyze'].post
    description: >-
      Record the transport and the separate meter. The operation declares application/json for its
      200 and no requestBody at all, so a client reading only the spec cannot learn either.
    update:
      x-actual-response-transport: text/event-stream (Server-Sent Events)
      x-transport-evidence: '"streams the answer back as Server-Sent Events" — the operation''s own description'
      x-requestbody-undeclared: >-
        No requestBody is declared. The request shape is not published anywhere machine-readable;
        the surface is documented only as the "Ask the data" chat.
      x-separate-meter:
        window: 1 day
        limit: 3
        unit: questions
        scope: per visitor, anonymous
        note: >-
          Metered separately from the REST call allowance — "running out of one doesn't touch the
          other." Paid tiers remove the daily limit.
      x-source: https://data.useplinth.com/developers#access

  # ── Idempotency / retry semantics, structural rather than contractual ────────────────────
  - target: $.paths
    description: >-
      Record the read-only guarantee. No Idempotency-Key header exists because there is nothing to
      make idempotent, but the guarantee itself is worth carrying in the contract.
    update:
      x-read-only-api:
        write_operations: 0
        evidence: >-
          "We only read the API surface with it — there is no write path to the data."
          (/developers#governance). Eight of ten operations are GET; runSql and askQuestion are POST
          only because they carry a body, and SQL rejects all DDL/DML.
        retry_safety: >-
          Every operation is naturally idempotent — a retry cannot corrupt state. It CAN spend
          allowance twice, since cache hits are billed. Retry safety here is a cost question, not a
          data-integrity question.
        idempotency_key_header: null