oc · docs
docs / quickstart

Quickstart

Goal: a real sybil-gate decision in one command.

You need curl. Not a wallet, not an account, not an API key — checking is a public read.

1. Ask the gate about an address

curl -s 'https://attest.ochk.io/api/check?addr=bc1pezn5yjxmz9nuahtqprvhfeya2nv4zdfk49u5amquhqptykx695rs60hqa2'
{
    "ok": true,
    "sats": 10000,
    "days": 307,
    "score": 103.46,
    "attestation_id": "bbde80f884ae849d2cc8d4fa19687e7ec862f8b7c85a3ee8cf93739548e23658",
    "address": "bc1pezn5yjxmz9nuahtqprvhfeya2nv4zdfk49u5amquhqptykx695rs60hqa2",
    "identities": [
        {
            "protocol": "nostr",
            "identifier": "npub1yfk7w33n60g05whqdmzluaftan390q89mqhht67wf4r8y90m3h9spyzpyn"
        }
    ],
    "network": "mainnet"
}

That is the whole product in one response. This address has proved control of an unspent 10,000-sat output that has sat still for 307 days, and has bound a Nostr identity to it. ok is the answer to "should I let this one in".

score is advisory — a convenience number, not a consensus value. Gate on sats and days, which are facts about the chain. See Scoring for why.

2. Set your own bar

ok is computed against thresholds you pass. Nothing is hardcoded.

# a floor this address clears
curl -s '.../api/check?addr=bc1pezn…&min_sats=5000&min_days=90'
{"ok": true,  "sats": 10000, "days": 307, "score": 103.46}

# a floor it does not
curl -s '.../api/check?addr=bc1pezn…&min_sats=100000000&min_days=3650'
{"ok": false, "sats": 10000, "days": 307, "score": 103.46}

Note that a failing check still returns the facts. You decide what to do with a 10,000-sat, 307-day identity — the endpoint does not moralise about it.

An address that has never attested is a different answer — and it comes back with a 404, not a 200:

curl -s 'https://attest.ochk.io/api/check?addr=bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kygt080'
{"ok": false, "reasons": ["not_found"]}

Read the status code, not just ok

There are four outcomes and the HTTP status separates two of them that ok alone does not:

meaningstatusbody
clears your bar200ok: true plus the facts
attested, below your bar200ok: false plus the facts
no attestation exists404ok: false, reasons: ["not_found"]
malformed request400error: "bad_request", issues[]

This matters more than it looks. The obvious client is wrong:

// WRONG — throws on a perfectly normal "this address never attested"
const res = await fetch(url);
if (!res.ok) throw new Error('check failed');
const { ok } = await res.json();
// Right — 404 is an answer, not a failure.
const res = await fetch(url);
if (res.status === 400) throw new Error('bad address');
if (res.status >= 500) throw new Error('verifier unavailable');
const body = await res.json(); // 200 and 404 both carry usable JSON
const admit = body.ok === true; // 404 lands here as false, correctly

"No proof exists" and "proof exists and is too small" are different facts about a person, and a gate usually wants to treat them differently — the first is a stranger, the second is someone who showed up with too little.

3. Look up by identity instead of address

If what you hold is a Nostr npub rather than a Bitcoin address, ask by identity. The value is protocol:identifier — a bare npub is rejected.

curl -s 'https://attest.ochk.io/api/check?identity=nostr:npub1yfk7w33n60g05whqdmzluaftan390q89mqhht67wf4r8y90m3h9spyzpyn'
{"ok": true, "sats": 10000, "days": 307, "address": "bc1pezn…"}

4. See everything an address has published

curl -s 'https://attest.ochk.io/api/discover?addr=bc1pezn…&limit=5'

Returns every attestation for that subject, each with its attestation_id, scheme (bip322), and bound identities. Use it when you want the history rather than a yes/no.

Then: gate your own thing

You now have the read path. The three next steps, in the order most people want them:

Verify without trusting us

Everything above is a convenience over a pure function. /api/check is attest.ochk.io reading a public index; if you would rather not trust that, don't.

npm i @orangecheck/sdk@^1.7.0

The version floor matters. In 1.3.0 and earlier, check() filtered relays on a multi-letter #address tag, which NIP-12 relays do not index — so it answered not_found for every real attestation. Before 1.7.0, it took the subject from relay tags rather than the signed message, and let one bond vouch for several identities on the same protocol.

import { check } from '@orangecheck/sdk';

const result = await check({
    addr: 'bc1pezn5yjxmz9nuahtqprvhfeya2nv4zdfk49u5amquhqptykx695rs60hqa2',
    minSats: 5000,
    minDays: 90,
});
// { ok: true, sats: 10000, days: 307, score: 103.46 }

Same shape as the HTTP response, and no call to attest.ochk.io — check() reads the attestations off Nostr relays and re-derives the answer locally from the envelope plus public chain data. Pass relays to point it at your own. That is the whole point of the protocol, and the reason the HTTP API is optional rather than load-bearing.

verify({ msg, addr, sig }) is the lower-level primitive if you hold a raw signed message rather than a published envelope, and createAttestation() builds and signs a new one.

To check an envelope you already hold without installing anything:

curl -s -X POST https://attest.ochk.io/api/verify \
  -H 'content-type: application/json' \
  -d '{"envelope": { … }, "policy": {"min_sats": 5000, "min_days": 90}}'

envelope is the full canonical AttestationEnvelope JSON as produced by createAttestation(). See Verification for the exact checks a verifier performs, and Self-host the verifier to run the whole stack yourself.

Get your own attestation

Signing needs a wallet that can do BIP-322 (UniSat, Xverse, Leather, Sparrow, Electrum, OKX, Phantom). Open attest.ochk.io, connect it, and sign one short canonical message — your Bitcoin never moves and no private key leaves your wallet. How it works is the whole protocol on one page.

The machine-readable contract

curl -s https://attest.ochk.io/api/openapi

OpenAPI 3.1, covering /api/check, /api/verify, /api/discover, /api/publish-attestation and /api/stats. Generate a client from it in any language. (/api/stats reports a rolling 10-minute window, so zeros there mean "quiet right now", not "empty".)