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(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.
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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file orangecheck-0.1.3.tar.gz.
File metadata
- Download URL: orangecheck-0.1.3.tar.gz
- Upload date:
- Size: 19.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
64934c2db881ec4bf333fd509d5cdecd8b544058899b0d9341185e08254e75c4
|
|
| MD5 |
0299c3a0ba42ab8c3bb40ab006e335b8
|
|
| BLAKE2b-256 |
5cbce65bdf56a73c232f0880fd0362f0d0f5daf397dbfaa9a1b3d687a4e35c84
|
File details
Details for the file orangecheck-0.1.3-py3-none-any.whl.
File metadata
- Download URL: orangecheck-0.1.3-py3-none-any.whl
- Upload date:
- Size: 15.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9f0a7fd45ae4b945b9be743847ac624d9e9610ab00ae4fbdb4c752eac0f4d78f
|
|
| MD5 |
26b918488e4ad5550128ab6ccdfec7b5
|
|
| BLAKE2b-256 |
35337769b981ec33f387d46b6c7a1a9c7907cb6315973ff293649649fc9935b4
|