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.VerificationError—challenge_verifyfailed.
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.