Sell to Scientists — Research Intelligence Data API

Version 0.4.0

# Sell to Scientists Data API Programmatic access to **research intelligence** — 250M+ publications, researchers mapped by field, H-index, grants and industry ties, and verified institutional contacts. Built for sales, BD, and **AI agents** that target academic researchers. The same data behind the public [selltoscientists.com](https://selltoscientists.com) site and the [app](https://app.selltoscientists.com), available as JSON, CSV, and an **MCP server** your AI assistant can call directly. ## Base URL ``` https://app.selltoscientists.com ``` > The API is served from `https://app.selltoscientists.com` at the documented paths — use this host today. A dedicated `https://api.selltoscientists.com` machine-API host is planned for GA but is **not yet deployed** (it currently returns 404); do not target it until it is announced. ## 60-second quickstart 1. Create a free account at [app.selltoscientists.com](https://app.selltoscientists.com) and start a subscription (or trial). 2. Generate an API key under **Account → API keys** (`app.selltoscientists.com/account/keys`). It is shown **once** — store it. 3. Call the API: ```bash curl -s https://app.selltoscientists.com/api/v1/awards?q=cybersecurity&page_size=5 \ -H "x-api-key: sci_xxxxxxxx" ``` Without an active subscription every data call returns **`402 Payment Required`** with a `payment_link` (and, for agents, a `machine_payment` affordance) — see **Subscriptions & the paywall** below. ## Authentication Two credential types, both resolved to one entitlement check: | Type | Header | Who | Get one | |---|---|---|---| | **API key** | `x-api-key: sci_…` | Humans / your backend | Dashboard → Account → API keys | | **Machine client** | `Authorization: Bearer client:<client_id>` | Autonomous agents | Self-register via DCR (`POST /api/oidc/register`) | - API keys are `sci_` + 48 hex chars, shown once, stored only as a SHA-256 hash, and revocable from the dashboard. - Machine clients self-register with no human in the loop (RFC 7591). A freshly registered client has **zero data scopes** until it subscribes — registration returns a `payment_link`, and the client can also pay autonomously via `POST /api/v1/pay`. - A full OAuth 2.1 authorization-code + token flow (the `OAuth2` scheme below) is on the roadmap; today, present the issued `client_id` as `Authorization: Bearer client:<client_id>`. ## Subscriptions & the paywall Data is gated by a single flat subscription (read live pricing at `GET /api/v1/price`). Until the caller's subject (`user:<id>` for a key, `client:<id>` for an agent) is active, **every data endpoint returns `402`**: ```json { "error": "payment_required", "message": "An active subscription is required to access award data.", "payment_link": "https://app.selltoscientists.com/checkout", "plan": "data-monthly", "machine_payment": { "endpoint": "https://app.selltoscientists.com/api/v1/pay", "method": "POST", "accepts": ["payment_method_token", "customer"], "scopes_granted": ["data:read", "data:export"] }, "price": "$49/mo", "currency": "usd" } ``` The `402` also carries a header advertising both paths: ``` WWW-Authenticate: Bearer realm="data", charge="…/checkout", charge_automated="…/api/v1/pay" ``` - **Humans** open `payment_link` (hosted Stripe Checkout). - **Agents** `POST /api/v1/pay` with a Stripe payment-method token and are charged off-session — no browser. On success the agent's very next data call returns `200`. **Plans & caps:** a 2-day trial is capped at **20 rows** per export/page; a paid plan returns up to **5,000 rows**. v1 entitlement is all-or-nothing — an active subscription grants every data scope. ## Scopes `data:read` (read awards), `data:export` (CSV export), `data:bulk` (bulk access). In v1 an active subscription grants all three. ## Pagination List endpoints take `page` (default 1) and `page_size` (default 100; hard-capped by your plan — 20 trial / 5,000 paid). Responses echo `total`, `page`, and `pageSize`. ## Rate limits - **Data calls:** 120 requests/minute per credential (per API key or machine client; anonymous/`402` traffic is limited per source IP). - **Client registration:** 10 registrations/hour per IP. Over the limit → `429` with a `Retry-After` (seconds) header. ## Using the API from an AI agent (MCP) This API is a **Model Context Protocol (MCP) server**. Point any MCP-capable client (Claude Desktop, Claude Code, Cursor, your own agent) at the streamable-HTTP endpoint and it can search and export Singapore government contracts on its own — including self-registering and paying for access with no human in the loop. **Endpoint** ``` https://app.selltoscientists.com/api/mcp ``` ### Connect flow (DCR + OAuth) The server advertises its auth the standard way, so spec-compliant clients wire themselves up automatically: 1. **Discover.** The client reads `GET /.well-known/oauth-protected-resource` (RFC 9728) to learn the resource and its authorization server, then `GET /.well-known/oauth-authorization-server` (RFC 8414) for the issuer, endpoints, PKCE support, and scopes (`data:read`, `data:export`, `data:bulk`). 2. **Register.** With no pre-shared credential, the client self-registers via RFC 7591 Dynamic Client Registration: `POST /api/oidc/register` returns a `client_id` (zero data scopes) plus a `payment_link`. 3. **Authenticate.** Present the issued client as `Authorization: Bearer client:<client_id>` on every MCP call. (`tools/list` is open and needs no credential; `tools/call` resolves the machine principal.) The full OAuth 2.1 authorization-code + token exchange is on the roadmap; the `Bearer client:<id>` form works today. 4. **Subscribe.** Until the client's subject is entitled, every `tools/call` returns the same **`402`** body as REST (see *Subscriptions & the paywall*) — carrying both a human `payment_link` and the agent-native `machine_payment` affordance. The agent calls the `pay` tool (`POST /api/v1/pay`) with a Stripe payment-method token, is charged off-session, and its very next `tools/call` returns data. ### Tool catalog `tools/list` returns **26 tools** (as of 0.4.0), each mirroring a REST endpoint 1:1 — search, entity detail, faceted search, lists, and saved searches — plus the autonomous `pay` tool. The full catalog (name · scope · REST endpoint · `requires_user_key`) is the machine-readable **`x-mcp-tools`** extension on this document; treat that as the source of truth. Highlights: `whoami` (ungated self-introspection), `search` (faceted), `get_buyer`/`get_supplier_detail`/`get_sector`/`get_tender` (entity detail; `get_buyer_detail` is a back-compat alias of `get_buyer`), and the owner-scoped `*_list`/`*_saved_search` tools that require a user `x-api-key` (an OIDC client token gets a `requires_user_key` result, mirroring the REST `403`). ### Raw JSON-RPC (manual test) List the tools (open, no auth): ```bash curl -s https://app.selltoscientists.com/api/mcp \ -H "content-type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' ``` Call a tool (needs an entitled principal, else `402`): ```bash curl -s https://app.selltoscientists.com/api/mcp \ -H "content-type: application/json" \ -H "Authorization: Bearer client:<client_id>" \ -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"search_awards","arguments":{"q":"cybersecurity","year":2024}}}' ``` ### Example agent prompts Once the connector is attached, a user can simply ask: - *"Search Sell to Scientists for cybersecurity awards in 2024 over S$1M and summarise the top 5 suppliers."* - *"Use the Sell to Scientists tools to pull every award won by the Ministry of Education this year and export them as CSV."* - *"Which suppliers does the Singapore government buy construction services from? Use get_agency on the relevant buyer."* If the agent hits a `402`, the affordance is self-explanatory: it can present the `payment_link` to the user, or — when authorised to spend — call `pay` and continue without interruption. ## Data notes Award amounts are in their **local award currency** (Singapore awards in SGD); the subscription **price** is in USD. `description` is the tender description, truncated to 240 chars in list/JSON responses. ## Support [[email protected]](mailto:[email protected]) · status & changelog below. --- ## What's new in 0.4.0 — REST · MCP · CLI parity 0.4.0 closes the gap between the three machine surfaces so **every capability is reachable the same way from REST, the MCP server, and the official CLI** (`@data-atlas/cli` → the `selltoscientists` binary). Same auth (`x-api-key` or `Bearer client:<id>`), same paywall, same entitlement caps. ### New REST endpoints (all under `/api/v1`, same machine paywall as `/awards`) | Endpoint | Returns | |---|---| | `GET /api/v1/whoami` | Self-introspection: `{subject, type, active, plan, scopes, export_cap}`. **No 402** — a valid-but-unentitled key returns `200` with `active:false` so an agent can decide to pay *before* doing work. Missing credential → `401`. | | `GET /api/v1/search` | Faceted full-text (Typesense) search over one `scope` (`tenders\|buyers\|suppliers\|sectors`): `{hits, found, page, per_page, facets}`. Search-budget rate limit (300/min). | | `GET /api/v1/buyers` · `GET /api/v1/buyers/{slug}` | Top buyers by spend · buyer detail (summary, yearly trend, sector breakdown, top suppliers). | | `GET /api/v1/suppliers` · `GET /api/v1/suppliers/{slug}` | Top suppliers by awarded total · supplier detail (summary, yearly trend, sectors, top buyers). | | `GET /api/v1/sectors` · `GET /api/v1/sectors/{slug}` | Curated sector taxonomy · sector detail (summary, yearly trend, active partners via `?kind=buyers\|suppliers`). | | `GET /api/v1/tenders/{id}` | Single award by numeric `award_id` **or** public tender page id, with related awards. | | `GET/POST /api/v1/lists` · `GET/PATCH/DELETE /api/v1/lists/{id}` · `POST/DELETE /api/v1/lists/{id}/items` | Saved award **lists** CRUD. **Owner-scoped: requires an `x-api-key` (user) credential** — OIDC client tokens get `403` (a list belongs to a real user). | | `GET/POST /api/v1/saved-searches` · `GET/PATCH/DELETE /api/v1/saved-searches/{id}` | Saved **searches** CRUD. Same owner-scoped rule as lists. | Entity-detail and partner tables are capped to your plan's row cap on trial keys (`table_cap` echoes the applied cap). ### MCP tool catalog (24 tools — `x-mcp-tools` on this document is the machine-readable source of truth) Every REST capability above has a matching MCP tool. `get_buyer` is canonical; `get_buyer_detail` is a kept back-compat alias. Lists/saved-searches tools require a user `x-api-key` (an OIDC client gets a `forbidden`/`requires_user_key` result, mirroring the REST `403`). `whoami` is ungated. ### Official CLI — `selltoscientists` `npm i -g @data-atlas/cli` ships the `selltoscientists` binary — a thin wrapper over this exact REST surface, 1:1 per command, with `--format table|json|csv`. Authenticate once with `selltoscientists auth login` (stores an `x-api-key` at `~/.config/selltoscientists/config.json`, mode `0600`) or pass `SCI_API_KEY`. See the `x-cli` extension on this document for the full command map, and the [CLI page](https://selltoscientists.com/cli/) on the marketing site. ```bash selltoscientists auth login # verify + persist your x-api-key via /whoami selltoscientists whoami # {subject, active, plan, scopes, export_cap} selltoscientists search "cybersecurity" --scope tenders --year-min 2023 selltoscientists get buyer ministry-of-education selltoscientists export --supplier ACME -o awards.csv ```

Servers

Data

Search and export awarded government tenders. Requires an active subscription (else 402).

GET /api/v1/awards

Search awarded government tenders

Paginated, filterable query over the full GeBIZ award set, ordered by awarded amount (desc). Requires an active subscription — otherwise returns `402` with a payment link. Row count per page is capped by your plan (20 trial / 5,000 paid). **Note:** `amount_max` is honored by the MCP `search_awards`/`export` tools; the REST endpoint currently filters on `amount_min` only (`amount_max` parity fix pending GA).

Parameters

  • q in query — Full-text match on tender description (ILIKE).
  • agency in query — Buyer agency — exact slug or partial display-name match.
  • supplier in query — Winning supplier — partial display-name match.
  • category in query — Category slug (exact).
  • year in query — Award year (YYYY).
  • amount_min in query — Minimum awarded amount (in the award's local currency).
  • amount_max in query — Maximum awarded amount. MCP tools only today (see note).
  • page in query — 1-based page number.
  • page_size in query — Rows per page (capped by plan).

GET /api/v1/awards.csv

Export awarded tenders as CSV

Same filters as `searchAwards`, returned as `text/csv` (attachment `selltoscientists-awards.csv`). Columns: `tender_no,supplier,agency,category,awarded_amt,award_date,description`. Row count capped by plan (20 trial / 5,000 paid). Requires an active subscription; un-entitled callers are redirected to `/checkout`.

Parameters

  • q in query
  • agency in query
  • supplier in query
  • category in query
  • year in query
  • amount_min in query

GET /api/v1/whoami

Self-introspection — auth, entitlement & scopes

Resolve the *calling* credential's identity, entitlement and granted scopes. **Deliberately different from the data paywall:** a valid-but-unentitled credential returns `200` with `active:false` (so an agent/CLI can decide to pay *before* doing work); a missing/invalid credential returns `401` (not `402`). Granted scopes are all data scopes when active, else empty (v1 is all-or-nothing). Shares the standard 120/min per-caller rate limit.

GET /api/v1/search

Faceted full-text search (one collection)

Faceted Typesense search over a single `scope` collection. Returns hits, total `found`, pagination, and `facets` (counts per facet value). Uses the **search budget** rate limit (300/min) shared with the public `/api/search` route. Behind the same machine paywall as `/awards` — no human-session fallback.

Parameters

  • scope in query — Collection: tenders | buyers | suppliers | sectors (default tenders).
  • q in query — Full-text query.
  • country in query — ISO country filter (multi-country instances).
  • sector in query — Sector slug (repeatable).
  • type in query — Record type filter (scope-dependent).
  • status in query — Status filter (scope-dependent).
  • year_min in query — Earliest award year.
  • year_max in query — Latest award year.
  • amount_min in query — Minimum amount.
  • amount_max in query — Maximum amount.
  • page in query — 1-based page (default 1).
  • per_page in query — Hits per page (≤ 50).

GET /api/v1/buyers

Top buyers by spend

Top buying agencies ranked by total awarded spend. `?limit` (default 50, max 500). Trial keys are row-capped (`table_cap`).

Parameters

  • limit in query — Rows to return (default 50, max 500).

GET /api/v1/buyers/{slug}

Buyer (agency) detail

Full buyer profile: header summary, yearly spend trend, sector breakdown, and a paginated top-suppliers table (`?sup_page`). `404` when the slug is unknown.

Parameters

  • slug in path (required) — Buyer slug, e.g. ministry-of-education.
  • sup_page in query — Top-suppliers page (1-based).

GET /api/v1/suppliers

Top suppliers by awarded total

Top suppliers ranked by total awarded value. `?limit` (default 50, max 500). Trial keys are row-capped.

Parameters

  • limit in query — Rows to return (default 50, max 500).

GET /api/v1/suppliers/{slug}

Supplier detail

Full supplier profile: header summary, yearly awarded trend, sector breakdown, and a paginated top-buyers table (`?buyer_page`). `404` when the slug is unknown.

Parameters

  • slug in path (required) — Supplier slug.
  • buyer_page in query — Top-buyers page (1-based).

GET /api/v1/sectors

Sector taxonomy

The curated sector taxonomy (slug + name). Small reference list — no row cap.

GET /api/v1/sectors/{slug}

Sector detail

Sector profile: header summary, yearly trend, and active partners. `?kind=buyers|suppliers` (default buyers) selects the partner table; `?page` paginates it. `404` when the slug is unknown.

Parameters

  • slug in path (required) — Sector slug.
  • kind in query — Partner table: buyers | suppliers (default buyers).
  • page in query — Partner table page (1-based).

GET /api/v1/tenders/{id}

Single award (tender) detail

One award resolved by **numeric `award_id` OR public tender page id**, plus related awards (same tender / same buyer). Related lists are pre-capped; no trial row cap applies. `404` when unknown.

Parameters

  • id in path (required) — Numeric award_id or public tender page id.

Billing

Live pricing and autonomous (agent) payment.

GET /api/v1/price

Live subscription price

Public. Returns the active Stripe subscription price (USD), resolved live by lookup key, with a graceful fallback if Stripe is unreachable. No auth required.

POST /api/v1/pay

Activate a subscription autonomously (agent payment)

Agent-native payment. Present a Stripe payment-method token (`pm_…`) or an existing `customer` (`cus_…`) you control; the subscription is created server-side, off-session — no hosted Checkout page. On success the caller's subject-keyed entitlement flips active and the next data call returns `200`. A decline returns `402` (retryable) echoing `decline_code` and the same `machine_payment` affordance. Requires a machine credential (API key or `Bearer client:`).

Agents & MCP

Self-registration (DCR) and the MCP-over-HTTP tool server for AI agents.

POST /api/oidc/register

Dynamic Client Registration (RFC 7591)

Autonomous, zero-scope client registration for agents. Returns client credentials with **no data scopes** plus a `payment_link` whose Checkout activates exactly this client. Rate-limited 10/hour/IP. After paying (hosted link or `POST /api/v1/pay`), present `Authorization: Bearer client:<client_id>` on data calls.

POST /api/mcp

MCP-over-HTTP (JSON-RPC 2.0)

Model Context Protocol server. Methods: `tools/list` (open catalog) and `tools/call` (gated by subscription). `tools/call` resolves the machine principal (`x-api-key` or `Bearer client:`) and returns `402` if missing or un-entitled. As of 0.4.0 the catalog is **26 tools** at full REST parity (search, entity detail, faceted search, lists, saved searches) — owner-scoped list/saved-search tools require a user `x-api-key`. See `x-mcp-tools` for the catalog.

Discovery

OAuth/MCP discovery metadata (RFC 9728 / RFC 8414).

GET /.well-known/oauth-protected-resource

Protected Resource Metadata (RFC 9728)

Tells MCP/OAuth clients which authorization server and scopes apply to this resource. Origin-relative.

GET /.well-known/oauth-authorization-server

Authorization Server Metadata (RFC 8414)

Issuer, registration/authorize/token/jwks endpoints, PKCE methods, and supported scopes. The token endpoint implements the authorization_code + refresh_token grants (PKCE mandatory); there is no client_credentials token grant today — autonomous agents use the `Authorization: Bearer client:<client_id>` machine shim returned by DCR.

Lists

Saved award lists CRUD. Owner-scoped — requires a user x-api-key (OIDC clients get 403).

GET /api/v1/lists

List saved award lists

Every saved award list the caller can see. **Owner-scoped:** requires an `x-api-key` (user) credential. OIDC client (`Bearer client:`) tokens get `403` — a saved list/search belongs to a real user account.

POST /api/v1/lists

Create a saved list

Create a personal saved award list. **Owner-scoped:** requires an `x-api-key` (user) credential. OIDC client (`Bearer client:`) tokens get `403` — a saved list/search belongs to a real user account.

GET /api/v1/lists/{id}

Get a saved list + items

One list with its items joined to award data (paginated `?page`/`?page_size`, ≤100). **Owner-scoped:** requires an `x-api-key` (user) credential. OIDC client (`Bearer client:`) tokens get `403` — a saved list/search belongs to a real user account.

Parameters

  • id in path (required) — List id.
  • page in query — 1-based page.
  • page_size in query — Items per page (≤100).

PATCH /api/v1/lists/{id}

Rename a saved list

Rename a list (creator or workspace owner). **Owner-scoped:** requires an `x-api-key` (user) credential. OIDC client (`Bearer client:`) tokens get `403` — a saved list/search belongs to a real user account.

Parameters

  • id in path (required) — List id.

DELETE /api/v1/lists/{id}

Delete a saved list

Delete a list and its items. **Owner-scoped:** requires an `x-api-key` (user) credential. OIDC client (`Bearer client:`) tokens get `403` — a saved list/search belongs to a real user account.

Parameters

  • id in path (required) — List id.

POST /api/v1/lists/{id}/items

Add a tender to a list

Add a tender by `tender_no` **or** `award_id`. Idempotent. **Owner-scoped:** requires an `x-api-key` (user) credential. OIDC client (`Bearer client:`) tokens get `403` — a saved list/search belongs to a real user account.

Parameters

  • id in path (required) — List id.

DELETE /api/v1/lists/{id}/items

Remove a tender from a list

Remove a tender by `tender_no`. **Owner-scoped:** requires an `x-api-key` (user) credential. OIDC client (`Bearer client:`) tokens get `403` — a saved list/search belongs to a real user account.

Parameters

  • id in path (required) — List id.

Saved Searches

Saved search CRUD. Owner-scoped — requires a user x-api-key (OIDC clients get 403).

GET /api/v1/saved-searches

List saved searches

Every saved search the caller can see (personal ∪ workspaces). **Owner-scoped:** requires an `x-api-key` (user) credential. OIDC client (`Bearer client:`) tokens get `403` — a saved list/search belongs to a real user account.

POST /api/v1/saved-searches

Save a search

Persist a search under a name. `kind:"atlas"` normalizes to the unified-search shape; a legacy Explorer filter set is stored as-is. **Owner-scoped:** requires an `x-api-key` (user) credential. OIDC client (`Bearer client:`) tokens get `403` — a saved list/search belongs to a real user account.

GET /api/v1/saved-searches/{id}

Get a saved search

One saved search by id (visible to caller, else `404`). **Owner-scoped:** requires an `x-api-key` (user) credential. OIDC client (`Bearer client:`) tokens get `403` — a saved list/search belongs to a real user account.

Parameters

  • id in path (required) — Saved-search id.

PATCH /api/v1/saved-searches/{id}

Rename a saved search

Rename a saved search (creator or workspace owner). **Owner-scoped:** requires an `x-api-key` (user) credential. OIDC client (`Bearer client:`) tokens get `403` — a saved list/search belongs to a real user account.

Parameters

  • id in path (required) — Saved-search id.

DELETE /api/v1/saved-searches/{id}

Delete a saved search

Delete a saved search. **Owner-scoped:** requires an `x-api-key` (user) credential. OIDC client (`Bearer client:`) tokens get `403` — a saved list/search belongs to a real user account.

Parameters

  • id in path (required) — Saved-search id.