orangecheck — Python SDK
Proof of Bitcoin stake for the open web. Python SDK for OrangeCheck.
A sybil-resistance primitive any Python app can consume in one call. Works with Django, Flask, FastAPI, Starlette, or a plain script.
pip install orangecheck
The 30-second integration
from orangecheck import check
result = check(addr="bc1q...", min_sats=100_000, min_days=30)
if result.ok:
# let them through
...
else:
print("rejected:", result.reasons)
That's it. check() queries the hosted ochk.io API, which discovers the most recent attestation for the address on Nostr, 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
# Gate
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 a raw attestation
verify(addr="bc1q...", msg=canonical_message, sig=signature)
# List attestations for a subject
discover(addr="bc1q...", limit=10)
# Signed-challenge auth (prove address control)
ch = challenge_issue(addr="bc1q...", audience="https://example.com")
verified = challenge_verify(message=ch.message, signature=user_sig, expected_nonce=ch.nonce)
All return typed dataclasses. All raise OrangeCheckError (or subclasses) on transport / server errors.
Django integration
# views.py
from django.http import HttpResponse, JsonResponse
from orangecheck import check
def gated_post(request):
addr = request.session.get("btc_address")
if not addr:
return HttpResponse(status=401)
result = check(addr=addr, min_sats=100_000, min_days=30)
if not result.ok:
return JsonResponse({"error": "orangecheck", "reasons": result.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)
FastAPI integration
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 integration
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
Every function has a sync counterpart on 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
# Default — hits https://ochk.io
c = Client()
# Self-hosted verifier, custom timeout
c = Client(base_url="https://verifier.mycompany.com", timeout=5.0)
# Reuse an existing httpx Client (connection pooling, auth, etc.)
import httpx
my_session = httpx.Client(proxy="http://proxy.example.com")
c = Client(session=my_session)
Types
Every response is a frozen dataclass with predictable fields:
@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 information ships with the package (py.typed marker, tested with mypy strict).
Errors
OrangeCheckError— base class for everything else.RateLimitError— the hosted API returned 429.VerificationError—challenge_verifyfailed (bad signature, expired, nonce mismatch, …).
check() treats 404 (no attestation found) as CheckResult(ok=False, reasons=("not_found",)) rather than raising — callers should gate on .ok, not on try/except.
Shell smoke-test
python -m orangecheck check --addr bc1q... --min-sats 100000
Exits 0 on pass, 2 on fail. Prefer the TypeScript oc CLI for richer output; this one is here mostly to verify the install.
License
MIT. The OrangeCheck protocol is CC-BY-4.0.
Metadata
Release files for orangecheck 2.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| orangecheck-2.0.0.tar.gz | 48.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| orangecheck-2.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 89.1 kB
Release files / orangecheck-2.0.0.tar.gz
| Download URL | orangecheck-2.0.0.tar.gz |
|---|---|
| Size | 48.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
d819c4ef365114777aeb51f33d9f0789208d49b75919409f448501b21037b696
|
|
BLAKE2b-256 checksum How to use checksums |
3f08f71328ff17bcbfbf7753bbb202f4942d8596e341727a4aef5c4d60b21802
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Release files / orangecheck-2.0.0-py3-none-any.whl
| Download URL | orangecheck-2.0.0-py3-none-any.whl |
|---|---|
| Size | 40.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
9ed3c8b45cb0a4918942e344c0ab4253787d3f5873cd8bb6fadc47562ab83496
|
|
BLAKE2b-256 checksum How to use checksums |
2c416411ba23ebf70efc54f2d671a3b22e801da7a1f258fba3a0cf44f5d09d59
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|