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.

Read this before you act on a number. Trust Check reports publicly observable signals about a domain. It is not a certification, a credit check, or a fraud determination. A low score most often means a business is new or has a thin public footprint — not that it is dishonest. Read the signals and their evidence before acting on the number.

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.com sign-in works on the same endpoints, which is how the dashboard calls them.
  • No auth at all. That is the x402 rail. See below.
Bearer-accepting routes are excluded from the sign-in redirect. A bad key returns 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.

FieldTypeNotes
urlstringA 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 null when the band is insufficient. It is never a number we made up to fill the field.
  • bandstrong (70–100), mixed (4069), weak (0–39) or insufficient.
  • signals[].statuspass, partial, fail, or unknown. Only unknown means “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:

SignalWeightRead from
domain_age25RDAP, from the registry of record
tls20The live TLS certificate, read off the socket
contact20The homepage: mailto/tel links, contact pages, socials
policies15Links from the homepage
address12schema.org PostalAddress markup, then the page text
reviews8Outbound 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.

The review signal measures a link, not a rating. It records whether the business publicly points at a third-party review profile — Trustpilot, G2, Yelp, the app stores. We do not read the reviews and we do not use their scores. A link is cheap to add, which is why it carries the smallest weight in the model.

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_…
ToolCostWhat it does
check_business5 crScore a domain. Returns the score, band, six signals with evidence, and flags.
get_checkfreeRe-read a check you already ran, by id.
list_checksfreeYour past checks, newest first, filterable by band.
get_scoring_modelfree · publicEvery weight and cutoff, so an agent can verify a score it was handed.
get_credit_balancefreeSpendable Ounie credits on the caller's wallet.
get_pricingfree · publicCredit price and the x402 endpoint.
whoamifreeThe authenticated key's owner and key id.
Credit safety is server-side by construction. An agent that runs out gets 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, not eip155:8453 — under x402 v1 the naming scheme selects the protocol version, and a CAIP-2 id there names a combination nothing serves.
  • extra states the USDC EIP-712 domain (USD Coin / 2 on mainnet). Sign against it; guessing reverts on chain after the money is committed.

Errors

StatusBodyMeaning
400target_requiredNo url or domain in the payload.
400invalid_request: url must be a public addressLoopback, private range, link-local, a single-label LAN name — or a public name that resolves into private space.
400invalid_request: …Not parseable as a domain or URL, a non-http scheme, or credentials in the URL.
401unauthorizedMissing or invalid key. Never a redirect.
402insufficient_creditsCarries required_credits, balance_credits and buy_credits_url.
402thin_resultx402 only. Nothing resolved; the payment was NOT settled.
404not_foundThat check id is not yours, or does not exist.
429too_many_runningMore than 5 checks in flight on one account.
502check_failedThe 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.
What this is not. Trust Check reports publicly observable signals about a domain. It is not a certification, a credit check, or a fraud determination. A low score most often means a business is new or has a thin public footprint — not that it is dishonest. Read the signals and their evidence before acting on the number. It is not Know Your Business, it is not a credit check, and it will not satisfy a compliance obligation. Do not publish these scores next to a company’s name: a weak band usually means new, and presenting it as anything else would be unfair to a business that has done nothing wrong.