Skip to main content

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_verify failed (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.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for orangecheck 2.2.0
File Size Uploaded
orangecheck-2.2.0.tar.gz 55.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for orangecheck 2.2.0
File Interpreter ABI Platform
orangecheck-2.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 99.1 kB

Release files / orangecheck-2.2.0.tar.gz

Download URL orangecheck-2.2.0.tar.gz
Size 55.2 kB
Tags Source
SHA-256 checksum
How to use checksums
aef9d9186d44fd839d1923e8881ba2ce4d4bae997df3cb9218258888e6eb7c10
BLAKE2b-256 checksum
How to use checksums
9f2963185db7cd9f47e197bf20d6b2884966b6d00ab354b05066aa956f566be0
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.2.0-py3-none-any.whl

Download URL orangecheck-2.2.0-py3-none-any.whl
Size 43.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d1fe20c30919c527f34d0f22eff21a4c924289dc2e3b8d454cc6c80a1630564f
BLAKE2b-256 checksum
How to use checksums
3f07b1af344f74e7a40e65fd6350b812eaef40547276ca0967f168ebf40cfa4a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release history Release notifications | RSS feed

This release

2.2.0 This release

2 release files

2.1.0

2 release files

2.0.0

2 release files

1.0.0

2 release files

0.3.0

2 release files

0.2.1

2 release files

0.2.0

2 release files

0.1.3

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page