docs / fastapi dependency

FastAPI dependency

FastAPI's dependency injection is a perfect fit for an OrangeCheck gate — you write it once and inject it into any endpoint.

Install

pip install fastapi uvicorn orangecheck

Minimum working example

from fastapi import FastAPI, Depends, HTTPException, Header
from orangecheck import AsyncClient

app = FastAPI()
oc  = AsyncClient()

async def require_stake(
    x_oc_address: str = Header(...),
    min_sats: int = 100_000,
    min_days: int = 30,
):
    r = await oc.check(addr=x_oc_address, 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}

The x_oc_address header is captured automatically via FastAPI's Header(...). When it's missing, FastAPI returns 422 without ever hitting orangecheck.

From a session cookie

For high-stakes gates, run the signed-challenge flow first:

from fastapi import Cookie

async def require_stake(
    verified_btc_address: str | None = Cookie(None),
    min_sats: int = 100_000,
):
    if not verified_btc_address:
        raise HTTPException(status_code=401, detail="not signed in")
    r = await oc.check(addr=verified_btc_address, min_sats=min_sats)
    if not r.ok:
        raise HTTPException(status_code=403, detail={"reasons": list(r.reasons)})
    return r

Parameterise per-endpoint thresholds

from typing import Callable, Annotated

def gate(*, min_sats: int = 100_000, min_days: int = 30) -> Callable:
    async def dep(x_oc_address: Annotated[str, Header()]):
        r = await oc.check(addr=x_oc_address, min_sats=min_sats, min_days=min_days)
        if not r.ok:
            raise HTTPException(status_code=403, detail={"reasons": list(r.reasons)})
        return r
    return dep

# Different thresholds per endpoint
@app.post("/post",  dependencies=[Depends(gate(min_sats=100_000))])
async def post():   return {"ok": True}

@app.post("/mint",  dependencies=[Depends(gate(min_sats=1_000_000, min_days=180))])
async def mint():   return {"ok": True}

Signed-challenge endpoints

from orangecheck import challenge_issue, challenge_verify, VerificationError
from fastapi import Request
from fastapi.responses import JSONResponse

@app.get("/auth/challenge")
async def auth_challenge(addr: str, request: Request):
    c = challenge_issue(addr=addr, audience="https://example.com", purpose="login")
    request.session["oc_nonce"] = c.nonce
    return {"message": c.message}

@app.post("/auth/verify")
async def auth_verify(body: dict, request: Request):
    try:
        r = challenge_verify(
            message=body["message"],
            signature=body["signature"],
            expected_nonce=request.session.get("oc_nonce"),
            expected_audience="https://example.com",
        )
    except VerificationError as e:
        return JSONResponse({"error": str(e)}, status_code=401)
    request.session["verified_btc_address"] = r.address
    return {"ok": True, "address": r.address}

(Use starlette.middleware.sessions.SessionMiddleware or authlib for session support.)

Further