Skip to main content

Python SDK for OrangeCheck — proof of Bitcoin stake for the open web.

Project description

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(proxies="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.
  • VerificationErrorchallenge_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.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

orangecheck-0.1.0.tar.gz (9.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

orangecheck-0.1.0-py3-none-any.whl (11.0 kB view details)

Uploaded Python 3

File details

Details for the file orangecheck-0.1.0.tar.gz.

File metadata

  • Download URL: orangecheck-0.1.0.tar.gz
  • Upload date:
  • Size: 9.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for orangecheck-0.1.0.tar.gz
Algorithm Hash digest
SHA256 4e3482b3ddd8e5e231f957825037f4feced5ffea9957b774ded92ff38b9d0362
MD5 19959b7460c730c549ef3cae6de84339
BLAKE2b-256 2613bbaa8a372b91c9dc74c5a0e2c58d91d4f8fac0b2428f135a602d98c12b66

See more details on using hashes here.

File details

Details for the file orangecheck-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: orangecheck-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 11.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for orangecheck-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 81801681e56899147d4f894e2341a62de01fb53e397ef2595e2f4ba6497b8762
MD5 fd3ca17370b9e78d00912deb8576cd0a
BLAKE2b-256 b02bc6bbe2a8fa74fb73ad16db289ece3c19efceb808c8c1c6f2024a28f35e16

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page