docs / orangecheck (python)

orangecheck (Python)

Pip-installable Python SDK. Thin HTTP client over the hosted API. Mirrors the JS SDK's three-function surface. Python 3.9+.

pip install orangecheck

The 30-second integration

from orangecheck import check

r = check(addr="bc1q...", min_sats=100_000, min_days=30)

if r.ok:
    ...                                  # let them through
else:
    print("rejected:", r.reasons)

check() queries the hosted ochk.io API, which discovers the most recent attestation, verifies the Bitcoin signature, recomputes live chain state, and compares against your thresholds.

Load-bearing functions

from orangecheck import check, verify, discover, challenge_issue, challenge_verify

check(addr="bc1q...", min_sats=100_000, min_days=30)
check(id="a3f5b8c2...", min_sats=100_000)
check(identity="github:alice", min_sats=50_000)

verify(addr="bc1q...", msg=canonical_message, sig=signature)

discover(addr="bc1q...", limit=10)

ch = challenge_issue(addr="bc1q...", audience="https://example.com")
v  = challenge_verify(message=ch.message, signature=user_sig, expected_nonce=ch.nonce)

All return typed frozen @dataclass objects. All raise OrangeCheckError (or subclasses) on transport / server errors.

Django

As a view dependency

from django.http import HttpResponse, JsonResponse
from orangecheck import check

def gated_post(request):
    addr = request.session.get("btc_address")
    r = check(addr=addr, min_sats=100_000, min_days=30)
    if not r.ok:
        return JsonResponse({"error": "orangecheck", "reasons": r.reasons}, status=403)
    # ... proceed
    return JsonResponse({"ok": True})

As middleware

# orangecheck_middleware.py
from django.http import JsonResponse
from orangecheck import check

class OrangeCheckMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        if request.path.startswith("/protected/"):
            addr = request.session.get("btc_address")
            r = check(addr=addr, min_sats=100_000, min_days=30)
            if not r.ok:
                return JsonResponse({"error": r.reasons}, status=403)
        return self.get_response(request)

See Django guide for the full integration.

FastAPI

from fastapi import FastAPI, Depends, HTTPException
from orangecheck import AsyncClient

app = FastAPI()
oc  = AsyncClient()

async def require_stake(
    addr: str,
    min_sats: int = 100_000,
    min_days: int = 30,
):
    r = await oc.check(addr=addr, min_sats=min_sats, min_days=min_days)
    if not r.ok:
        raise HTTPException(status_code=403, detail={"reasons": list(r.reasons)})
    return r

@app.post("/post")
async def post_comment(gate = Depends(require_stake)):
    return {"ok": True, "sats": gate.sats}

Flask

from functools import wraps
from flask import Flask, request, jsonify
from orangecheck import check

app = Flask(__name__)

def require_orangecheck(min_sats=100_000, min_days=30):
    def decorator(fn):
        @wraps(fn)
        def wrapper(*args, **kwargs):
            addr = request.headers.get("X-OC-Address")
            r = check(addr=addr, min_sats=min_sats, min_days=min_days)
            if not r.ok:
                return jsonify(error="orangecheck", reasons=r.reasons), 403
            return fn(*args, **kwargs)
        return wrapper
    return decorator

@app.post("/post")
@require_orangecheck(min_sats=100_000, min_days=30)
def post_comment():
    return {"ok": True}

Async

Use AsyncClient:

import asyncio
from orangecheck import AsyncClient

async def main():
    async with AsyncClient() as oc:
        r = await oc.check(addr="bc1q...", min_sats=100_000)
        print(r.ok, r.sats, r.days)

asyncio.run(main())

Configuration

from orangecheck import Client

c = Client()   # default — hits https://ochk.io

# Self-hosted
c = Client(base_url="https://verifier.mycompany.com", timeout=5.0)

# Reuse an existing httpx.Client
import httpx
session = httpx.Client(proxies="http://proxy.example.com")
c = Client(session=session)

Types

from orangecheck import CheckResult, VerifyOutcome, DiscoverResult, Challenge

@dataclass(frozen=True)
class CheckResult:
    ok:               bool
    sats:             int
    days:             int
    score:            float
    attestation_id:   str | None
    address:          str | None
    identities:       tuple[IdentityBinding, ...]
    network:          Literal["mainnet", "testnet", "signet"] | None
    reasons:          tuple[str, ...]

Full type info ships with the package (py.typed marker; tested under mypy --strict).

Errors

  • OrangeCheckError — base class.
  • RateLimitError — hosted API returned 429.
  • VerificationErrorchallenge_verify failed.

check() treats 404 (no attestation) as CheckResult(ok=False, reasons=("not_found",)) rather than raising — gate on .ok, not try/except.

Shell smoke-test

python -m orangecheck check --addr bc1q... --min-sats 100000

Exits 0 on pass, 2 on fail. For richer output use the TypeScript oc CLI.

License

MIT.