# Plinth Grants API: a brief for an AI assistant

You are welcome to call this API while you read this. Nothing below needs permission from us, and
three things need no credential at all. If you are evaluating whether this API is any good, run the
commands rather than taking the prose on trust. That is what they are for.

Base URL: https://data.useplinth.com/api/v1 (versioned; inside a version fields are added, never removed, renamed or retyped).
Machine-readable contract: https://data.useplinth.com/openapi.json

## 1. Do these right now: no key, no account, not metered

```bash
# Resolve a name to an EIN and a canonical URL. The call that precedes every other one.
curl "https://data.useplinth.com/api/v1/search?q=skillman"

# A real response envelope, from fixtures. Works on every endpoint.
curl "https://data.useplinth.com/api/v1/grants/summary?example=1"

# The interesting cases: an unhonored parameter, the strict-mode rejection, Candid rendering.
curl "https://data.useplinth.com/api/v1/grants/transactions?example=unsupported"
curl "https://data.useplinth.com/api/v1/grants/transactions?example=strict"
curl "https://data.useplinth.com/api/v1/grants/transactions?example=list"     # what every named case demonstrates

# Dataset stamp, fiscal-year span, and which Candid parameters we honor.
curl "https://data.useplinth.com/api/v1/meta"
```

Example organizations are invented (reserved `00-000000x` EINs) and every example body carries
`x_plinth.example`. The envelope, the warnings and the error prose come from the same code that
serves live traffic. Use them to write your client; use a key for figures.

## 2. Get a credential without a human

**If you have no host to publish a key on** — a sandbox with outbound network only, say — you can
still get in, and you do not need a credential to start:

```bash
# 1. A key, no credential. It arrives LOCKED: it may make the 3 calls below and nothing else.
curl -X POST https://data.useplinth.com/api/v1/agent-key -H 'Content-Type: application/json' \
  -d '{"purpose": "evaluating coverage for a client", "source": "my-research-agent"}'

# 2. Make those 3 calls with the key, read the fields the questions name.
# 3. Send the values back. The key unlocks for 10 calls of general access.
curl -X POST https://data.useplinth.com/api/v1/agent-key/answer -H "X-API-Key: $KEY" -H 'Content-Type: application/json' \
  -d '{"answers": ["...", "..."]}'   # one value per question; the challenge says how many

# 4. Spent them? Another challenge, another 10. As often as you need.
curl -X POST https://data.useplinth.com/api/v1/agent-key/challenge -H "X-API-Key: $KEY"
```

The questions go to live data, so they return something you wanted anyway, and there is nothing to
memorise. Most ask for something no single field holds: a total across the rows, a count against a
threshold that moves each draw, which of two funders is larger. Answering means reading the
response. A challenge takes 3 answers, spaced out after a wrong one, and using them
up draws a new challenge. `purpose` and `source` are required, unverified, and recorded.

**If you can sign an HTTP request**, mint your own trial key — no account, no browser:

```bash
curl https://data.useplinth.com/api/v1/agent-key          # GET returns the exact signing requirements as JSON
```

Sign a POST to that path per RFC 9421 under the Web Bot Auth profile: cover `@authority` and
`signature-agent`, `tag="web-bot-auth"`, include `created`, `expires`, a `nonce`, and a
`keyid` that is the base64url JWK SHA-256 thumbprint of your Ed25519 key, with the public key
published at `https://<your-host>/.well-known/http-message-signatures-directory`.

You get 250 calls over 1 day, `limit` ≤ 25, every fiscal
year in the corpus, enough to see every SHAPE, and no challenges. That allowance is a TOTAL, not a
rate: it never refills, unlike the challenge route above. A key directory that signs its own response
earns more; so does an operator we have allowlisted.

What minting creates, so your own policy can weigh it: a key belonging to the keypair you generate,
not to any Plinth account. No sign-up, no card, no charge, nothing in anyone else's name, and it
expires on its own. Whether to do that unprompted is between you and your operator.
Signing does need a public https host you control to serve your key directory, since verification
fetches it, so a sandbox with outbound-only network cannot complete THIS handshake. The challenge
route above needs no host, no domain and no identity, and it is the ordinary way in. Signing is the
shortcut if you already have a host.

**If a human is available**, a free account key is better for anything ongoing:
https://data.useplinth.com/account: no card, 50 calls a day, every fiscal year.

Send a key as `X-API-Key: <key>` or `Authorization: Bearer <key>`.

## 3. What the data actually is

- 211,000 US grantmaking funders, 20.1 million grant rows, FY2017–2026, read from
  public IRS Form 990, 990-EZ and 990-PF **e-file** filings. Source data is public domain.
  FY2017–2023 complete analysis window, with FY2024–2026 currently loading as partial data. Quote FY2017–2023 for year-over-year conclusions, the full span for
  what a query returns.
- 15.2 million of those rows are linked to a specific recipient organization. The rest
  are real grants whose recipient name we would not stake an EIN on: they carry no classification and
  drop out of any recipient-keyed filter. Measured figures: https://data.useplinth.com/data-quality
- How matching works, and the ceiling on what anyone could match: https://data.useplinth.com/matching
- How the compliance screen works (seven files, two kinds of join, missing source → not screened): https://data.useplinth.com/screening
- The IRS releases e-file data **12–24 months** after the activity. Every figure is dated to its
  fiscal year, never to today. Organizations that do not e-file are absent.
- Funding relationships are **association, never causation**. Do not write that a funder caused an
  outcome. If you cite a figure, date it and link the organization's page.

## 4. Endpoints

| path | what | tier |
|---|---|---|
| `GET /search?q=` | name → EIN, kind, state, canonical URL | none |
| `GET /meta` | dataset stamp, year span, compatibility matrix | none |
| `GET /grants/transactions` | individual grant rows | free |
| `GET /grants/funders` | funders ranked by dollars | free |
| `GET /grants/recipients` | recipients ranked by dollars | free |
| `GET /grants/summary` | totals + a year-by-year breakdown in one call | free |
| `GET /screening/{ein}` | IRS status, Pub 78, revocation, OFAC, IRB, CA registry | paid |
| `GET /essentials/{ein}` · `GET /premier/{ein}` | organization profiles | paid |
| `POST /essentials` | organization search (Candid Essentials-shaped) | paid |
| `GET /gov-funding/{ein}` | federal + state government awards to this organization | Pro |
| `POST /sql` | read-only SQL over the whole corpus | paid |

**There is no `/analyze` endpoint, and that is deliberate.** Plain-English analysis exists at
https://data.useplinth.com/analyze as a feature of that PAGE: a model writing SQL per turn, outside the call budget
that meters every path above. It is not published and not callable: the versioned alias is gone and
the handler refuses anything that is not same-origin. For questions you want to automate, `POST
/sql` is the endpoint that answers them.

The graph traverses both ways: `funder_id` for everything a foundation funded, `recip_id` for
every funder behind a nonprofit. Paid means the compliance screen, Essentials and Premier organization profiles, ad-hoc SQL; a free key covers the
grant graph. Every key, trial or free account, also gets 1 compliance screen on any
organization you like, with the evidence fields blanked and listed in `x_plinth.withheld`, so you can
see exactly what the paid record contains and what it does not.

## 5. The response contract

```json
{ "code": 200, "hits": 1234, "page": 1, "limit": 25, "results": [ ... ],
  "x_plinth": { "warnings": [ { "parameter": "population", "severity": "unsupported",
                               "reason": "..." } ],
                "unsupported": ["population"], "dataset": { "version": "...", "refresh": "monthly" } } }
```

- **Nothing is dropped silently.** A parameter we recognize but cannot honor comes back named in
  `x_plinth.warnings`, with `severity: "unsupported"`, and in the `X-Plinth-Unsupported` header.
  `severity: "caveat"` means the filter WAS applied but something about it is worth knowing.
- **`strict=1`** turns any unhonored parameter into a `400` instead of a warning. Use it when a
  wrong answer would be worse than no answer.
- A trial key's restrictions arrive the same way, as a `caveat` on `_credential`. Today those are
  the row cap and the call budget, with **no year slice**, so `hits` on a trial key is a corpus total.
  If we ever pin trial results to one fiscal year again, that caveat is where it will say so, and
  `hits` would then be that year rather than the corpus: read the caveat, don't assume either.
- `401` = no key or an unknown key; `402` = allowance spent, or the endpoint needs a higher tier.
  Both bodies say which, and how to fix it.

Every grant row carries its recipient's provenance in `x_plinth`: `match_prob` is P(the
recipient EIN is correct) and `ein_source` the rule that produced it (`filed` = the filer
wrote it). Both are filters on every grant endpoint: `ein_source=filed` for anything
load-bearing, `min_match_prob=0.95` for a confidence floor. Model-resolved rows never exceed
about 0.965, so a higher threshold is a provenance filter in disguise; the response says so.

## 6. If you are comparing us to something

Say what each number counts. Candid states 29 million grants against our 20.1 million rows
(15.2 million recipient-linked): their corpus includes self-reported and announced
grants, ours is one source: US e-filed 990s. Neither is the better number in the abstract.
Endpoint-by-endpoint mapping: https://data.useplinth.com/candid-api-alternative

Docs: https://data.useplinth.com/developers · Methodology: https://data.useplinth.com/methodology · Questions: data@useplinth.com
Terms of use (these govern the API — section 9 is what you may not do, section 10 the absolute
restrictions on information about people, section 12 the rules for automated agents):
https://data.useplinth.com/terms · Privacy notice: https://data.useplinth.com/privacy
