API reference
Trust Check API
One endpoint that matters. Give it a domain, get a 0–100 legitimacy score built from six public signals, each carrying the evidence it was read from. The scoring is a pure function — no language model anywhere in it — and the model is published, so you can recompute any score this service hands you.
Quickstart
Three rails, one meter. Pick whichever fits: a Bearer key over REST, an MCP tool in your agent host, or keyless USDC over x402.
curl -X POST https://trustcheck.ounie.com/api/checks \
-H "Authorization: Bearer trs_live_…" \
-H "Content-Type: application/json" \
-d '{"url":"example-shop.com"}'Mint a key at /dashboard/api-keys. Every call draws the key owner’s shared Ounie credit wallet and can never overdraw.
Authentication
- This app’s key.
Authorization: Bearer trs_live_…. Up to five active keys per account; the raw token is shown once at mint and only its sha256 hash is stored. - The fleet master key. Your ounie.com developer key (
ounie_live_…) works here too, once you switch on Use across Ounie apps in your ounie.com API-key settings. One key, the whole fleet. - Query-string fallback. Hosts that cannot set headers — the Ounie AI Team’s manual MCP entry, for one — can pass
?api_key=trs_live_…instead. - Session cookie. The shared
.ounie.comsign-in works on the same endpoints, which is how the dashboard calls them. - No auth at all. That is the x402 rail. See below.
401, never a 302 to a login page — an agent should get an error it can read, not an HTML form.POST /api/checks
Runs one check synchronously and returns the finished reading. Costs 5 credits.
| Field | Type | Notes |
|---|---|---|
| url | string | A domain or a URL. example.com, https://example.com/ and www.example.com/shop all work. Max 300 characters. domain is accepted as an alias. |
That is the entire input. There are no options, because there is nothing to tune: the weights are fixed and published, and a score you could configure would not be comparable to anyone else’s.
The response
{
"ok": true,
"check": {
"id": "…",
"target": "example-shop.com",
"checked_url": "https://example-shop.com/",
"final_url": "https://example-shop.com/",
"score": 78,
"band": "strong",
"band_summary": "Most signals resolved and looked established.",
"resolved_signals": 5,
"resolved_weight": 92,
"thin": false,
"credits_spent": 5,
"signals": [
{
"name": "domain_age",
"label": "Domain age",
"status": "pass",
"value": 0.85,
"weight": 25,
"evidence": "example-shop.com was registered on 2019-03-04 via NameCo Ltd. — about 6 years ago."
},
{
"name": "reviews",
"label": "Review footprint",
"status": "unknown",
"value": null,
"weight": 8,
"evidence": "the site could not be read (the site answered HTTP 403)"
}
],
"flags": [
{ "code": "no_policy_pages", "severity": "info",
"detail": "No privacy, terms or returns pages were linked. …" }
],
"checked_at": "2026-08-23T12:00:00.000Z",
"disclaimer": "Trust Check reports publicly observable signals …"
}
}- score — 0–100, or
nullwhen the band isinsufficient. It is never a number we made up to fill the field. - band —
strong(70–100),mixed(40–69),weak(0–39) orinsufficient. - signals[].status —
pass,partial,fail, orunknown. Onlyunknownmeans “we could not observe this”, and it is the only one that leaves the weighting. - signals[].evidence — always populated, on every status. For an unresolved signal it states why it could not be read, so you can tell a blind spot from an absence.
- flags[] — notable observations that are not scored: a domain registered in the last 90 days, an expiring registration, broken HTTPS, an off-site redirect, no contact channel at all.
The scoring model
score = 100 × Σ(value × weight) ÷ Σ(weight), over resolved signals only, rounded half-up. The weights:
| Signal | Weight | Read from |
|---|---|---|
| domain_age | 25 | RDAP, from the registry of record |
| tls | 20 | The live TLS certificate, read off the socket |
| contact | 20 | The homepage: mailto/tel links, contact pages, socials |
| policies | 15 | Links from the homepage |
| address | 12 | schema.org PostalAddress markup, then the page text |
| reviews | 8 | Outbound links to third-party review platforms |
Fetch this at runtime rather than hard-coding it: GET /api/pricing returns the whole model, and the MCP tool get_scoring_model returns the same thing with no authentication.
Unresolved signals
This is the rule most likely to surprise you, and the one most worth understanding. A signal that cannot be observed leaves the denominator. It is never scored as zero.
Scoring an unobservable signal as zero would punish a business for our blind spot — plenty of ccTLDs publish no RDAP service at all — and it would quietly change what the number means every time an upstream went down. Instead the score is always “of the signals that resolved”, and resolved_signals tells you how many that was.
Below 3 resolved signals, or under 45 of the 100 weight, no number is published: band: "insufficient" and score: null. Such a check still bills — it did real work and the signals that resolved are in the response.
Thin checks & refunds
A check where not one of the six signals resolved has told you nothing, and is never billed. On the credit rail the whole reserve refunds pool-exact and the row is marked refunded; the response carries thin: true and credits_spent: 0.
On x402 there is no refund — an on-chain settlement is final — so the order inverts: the payment is verified (off-chain, free), the check runs, and only a non-thin result triggers settlement. A thin result comes back as a 402 with error: "thin_result" and nothing has moved.
Reading past checks
Free, forever. Reading work you already paid for never bills again.
GET /api/checks?limit=20&band=weak
GET /api/checks/<id>band filters on strong, mixed, weak or insufficient. Checks paid for over x402 have no owner and are not listed — there is no account to list them against.
MCP
Streamable HTTP at /api/mcp, legacy SSE at /api/sse. Works in Claude, Cursor, ChatGPT, the AI SDK and the Ounie AI Team.
{
"mcpServers": {
"trustcheck": {
"url": "https://trustcheck.ounie.com/api/mcp",
"headers": { "Authorization": "Bearer trs_live_…" }
}
}
}
# Hosts that cannot set headers (e.g. the Ounie AI Team):
https://trustcheck.ounie.com/api/mcp?api_key=trs_live_…| Tool | Cost | What it does |
|---|---|---|
| check_business | 5 cr | Score a domain. Returns the score, band, six signals with evidence, and flags. |
| get_check | free | Re-read a check you already ran, by id. |
| list_checks | free | Your past checks, newest first, filterable by band. |
| get_scoring_model | free · public | Every weight and cutoff, so an agent can verify a score it was handed. |
| get_credit_balance | free | Spendable Ounie credits on the caller's wallet. |
| get_pricing | free · public | Credit price and the x402 endpoint. |
| whoami | free | The authenticated key's owner and key id. |
insufficient_credits with the exact shortfall and a link to top up — it is refused, never overdrawn.x402 — keyless, pay per call
For agents with a wallet and no account. $0.06 per check in USDC on Base (60000 atomic units).
# 1. Ask. No payment header — you get the terms.
curl -X POST https://trustcheck.ounie.com/api/x402/check \
-H "Content-Type: application/json" \
-d '{"url":"example-shop.com"}'
# → HTTP 402
# { "x402Version": 1, "accepts": [{
# "scheme": "exact", "network": "base",
# "maxAmountRequired": "60000",
# "payTo": "0x…", "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
# "extra": { "name": "USD Coin", "version": "2" } }] }
# 2. Sign the EIP-3009 authorization and retry.
curl -X POST https://trustcheck.ounie.com/api/x402/check \
-H "X-Payment: <base64 signed payload>" \
-H "Content-Type: application/json" \
-d '{"url":"example-shop.com"}'- Bad input is refused before the quote. A malformed domain, or an address that cannot name a public site, returns
400— not a 402. You should never build and sign an authorization for a request that cannot succeed. - Verify happens before the work, settle after it. A garbage payment header cannot burn a page render, and an empty result cannot take your money.
- The network name is the bare
base, noteip155:8453— under x402 v1 the naming scheme selects the protocol version, and a CAIP-2 id there names a combination nothing serves. extrastates the USDC EIP-712 domain (USD Coin/2on mainnet). Sign against it; guessing reverts on chain after the money is committed.
Errors
| Status | Body | Meaning |
|---|---|---|
| 400 | target_required | No url or domain in the payload. |
| 400 | invalid_request: url must be a public address | Loopback, private range, link-local, a single-label LAN name — or a public name that resolves into private space. |
| 400 | invalid_request: … | Not parseable as a domain or URL, a non-http scheme, or credentials in the URL. |
| 401 | unauthorized | Missing or invalid key. Never a redirect. |
| 402 | insufficient_credits | Carries required_credits, balance_credits and buy_credits_url. |
| 402 | thin_result | x402 only. Nothing resolved; the payment was NOT settled. |
| 404 | not_found | That check id is not yours, or does not exist. |
| 429 | too_many_running | More than 5 checks in flight on one account. |
| 502 | check_failed | The run itself failed. Credits were already refunded; you were not charged. |
Limits & disclaimer
- 5 credits per check on the credit rails, $0.06 on x402. Buy credits at ounie.com.
- 5 concurrent checks per account; 5 active API keys.
- A typical check takes five to fifteen seconds. A site that needs a browser render takes longer.
- Only public http/https targets. Private addresses are refused at the gate, before anything is fetched.