Metadata-Version: 2.4
Name: goldrail
Version: 0.0.2
Summary: Decision-API client and WSGI/ASGI middleware for a goldrail payment plane (x402 / MPP).
Author-email: Goldsky <oss@goldsky.com>
License-Expression: Apache-2.0
Project-URL: Homepage, https://github.com/goldsky-io/goldrail
Project-URL: Repository, https://github.com/goldsky-io/goldrail
Project-URL: Issues, https://github.com/goldsky-io/goldrail/issues
Project-URL: Changelog, https://github.com/goldsky-io/goldrail/releases
Project-URL: Source (this SDK), https://github.com/goldsky-io/goldrail/tree/main/sdk/python
Keywords: x402,mpp,payments,402,stablecoin,wsgi,asgi,middleware,agents
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: System Administrators
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware
Classifier: Topic :: Office/Business :: Financial
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: dev
Requires-Dist: pytest>=7; extra == "dev"
Dynamic: license-file

# goldrail (Python)

**A Python client for a [goldrail](https://github.com/goldsky-io/goldrail)
payment plane.** Put a price on your Django, Flask, FastAPI, or Starlette
routes and let humans *and* AI agents pay per request — over
[x402](https://www.x402.org) today, MPP next.

```python
from goldrail import GoldrailMiddleware      # WSGI: Django, Flask, Pyramid, ...
application = GoldrailMiddleware(application)
```

```python
from goldrail import GoldrailASGIMiddleware  # ASGI: FastAPI, Starlette, ...
app.add_middleware(GoldrailASGIMiddleware)
```

That is the whole integration. There is no pricing in that code, and there is
meant to be none: prices, rails, exemptions, and facilitators live in
goldrail's own versioned config, so changing a price is not a redeploy of your
app.

## What this package is — and is not

**It is a shell.** It describes each request to a running goldrail process over
that process's decision API, applies the answer verbatim, and reports what your
application actually returned so a failed request gets refunded.

**It is not the engine.** No signature verification, no balance arithmetic, no
settlement, no protocol rendering — none of that happens in Python here.
goldrail's money logic lives in one auditable Rust core, and **money logic is
never reimplemented in a shell**: a second implementation in a second language
is a second set of rounding rules, a second replay-protection story, and a
second place for the two to disagree about what a payer was charged. So this
package deliberately has none of it.

Concretely, that means you need a goldrail process. One binary:

```bash
goldrail dev --ci                                   # a fixture plane for tests
goldrail serve --config goldrail.toml               # the real thing
export GOLDRAIL_URL=http://127.0.0.1:8403           # what this package talks to
```

The `goldrail` command installed by this package is a *launcher*: it finds a
goldrail binary and `exec`s it, and if it cannot find one it says so on stderr —
with every place it looked and how to install one — and exits 127. It never
silently succeeds.

## Install

```bash
pip install goldrail
```

Zero runtime dependencies. A payment path should not drag a dependency tree into
every app that prices a route, so the transport is `urllib` from the standard
library.

## How a request flows

```
request ──► GoldrailMiddleware ──► POST /v1/decide ──► goldrail
                    │                                    │
        serve ◄─────┴──── challenge (402) ── reject ◄─────┘
          │
     your handler answers 200/4xx/5xx
          │
          └──────────────► POST /v1/report ──► refund on 5xx
```

* **serve** — the request is relayed to your application. goldrail's product
  headers (`goldrail-funding`, `goldrail-balance`) are stamped on the response.
* **challenge** — a 402 is answered *verbatim*, carrying every enabled
  dialect's slice of one challenge. Your application never runs.
* **reject** — an RFC 9457 problem document with the engine's stable code.
* **report** — the status your application produced goes back to goldrail. A
  5xx means the payer received nothing, and nothing is not billable, so the
  charge is released. Both middlewares report on every exit path, including an
  exception mid-response.

## Usage

### Flask

```python
from flask import Flask
from goldrail import GoldrailMiddleware

app = Flask(__name__)
app.wsgi_app = GoldrailMiddleware(app.wsgi_app)
```

### Django

```python
# wsgi.py
from django.core.wsgi import get_wsgi_application
from goldrail import GoldrailMiddleware

application = GoldrailMiddleware(get_wsgi_application())
```

### FastAPI / Starlette

```python
from fastapi import FastAPI
from goldrail import GoldrailASGIMiddleware

app = FastAPI()
app.add_middleware(GoldrailASGIMiddleware)
```

### The client on its own

For gateways, background workers, or a framework nobody has written a
middleware for:

```python
from goldrail import DecisionClient, DecideRequest

client = DecisionClient()                      # discovers GOLDRAIL_URL
decision = client.decide(DecideRequest(path="/api/premium", headers={"x-payment": envelope}))

if decision.terminal:
    return respond(decision.status, decision.headers, decision.body)   # verbatim

response = handle_it()
client.report(decision.token, response.status_code)                    # refund on 5xx
```

`decide_async` / `report_async` exist for async callers; they run the blocking
POST in the default thread pool, which is stated here rather than hidden.

### Reading the ledger

```python
from goldrail import AdminClient

admin = AdminClient()                                  # GOLDRAIL_ADMIN_URL
payer = admin.payer("evm:0xabc…")                      # balance + lifetime totals
print(payer.balance)                                   # atomic units, as an int

statement = admin.statement("evm:0xabc…", window="30d")   # "why was I charged?"
revenue = admin.revenue(group_by="config_version")        # v41 vs v42, per version
```

`AdminClient` **reads only**. Config mutation is money redirection — `pay_to`
lives in config — so it stays on the admin plane under RBAC, the write-surface
modes, and the audit trail, driven by `goldrail config apply` or the panel. A
convenience method here would route around all three.

## Configuration

| Environment variable | What it does |
| --- | --- |
| `GOLDRAIL_URL` | The goldrail control plane, e.g. `http://127.0.0.1:8403`. **Required.** |
| `GOLDRAIL_TOKEN` | Bearer token for that plane, when it wants one. |
| `GOLDRAIL_SERVICE` | The service identity decisions are stamped with, for one goldrail fronting several apps. |
| `GOLDRAIL_ADMIN_URL` | The admin plane, when it does not share the data plane's address. |
| `GOLDRAIL_ADMIN_TOKEN` | A scoped API token for admin reads. |
| `GOLDRAIL_BIN` | The goldrail binary the `goldrail` launcher should exec. |

Middleware options: `posture` (`"closed"` — the default 503, or `"open"` — serve
unpaid, loudly), `context` (a callable returning seller-side context
dimensions), `report` (default on), `timeout`, `report_timeout`, `logger`, and
`counters`.

**There is no default URL.** Constructing a client or a middleware without one
raises `ConfigurationError` at wire-up time. An app that booted without a
decision plane would serve every priced route for free, and a loud failure at
startup is the only honest alternative.

## Failure, stated

Nothing here degrades quietly. Every fallback logs *and* increments a counter
you can read off `middleware.counters`:

| Situation | What happens |
| --- | --- |
| Decision plane unreachable, `posture="closed"` | 503 `application/problem+json`, logged at error, `unavailable_refused` |
| Decision plane unreachable, `posture="open"` | Request served, response tagged `goldrail-funding: unpaid`, logged at warning, `unavailable_served_unpaid` |
| Plane answers an unknown `decision` tag | `ProtocolError` — never optimistically read as "serve" |
| Report undeliverable | Request still succeeds; logged, `reports_failed` — the response already reached the caller |
| goldrail does not recognise a token | `reports_unknown_token`; that request keeps its charge, and says so |
| `report=False` | Logged once at construction: those requests lose their refund on upstream failure |
| WebSocket scope (ASGI) | Passed through unpriced — warned once, `websocket_bypassed` every time |
| Context extractor raises | Request proceeds without context dimensions; logged with a traceback, `context_extractor_failed` |

## What leaves your process

An allowlist, never a filter: `x-payment`, any `goldrail-*` header, and
`Authorization` **only** under the `Payment` scheme. Cookies, API keys, and your
application's own bearer tokens are not forwarded to the payment plane. Payment
credentials are stripped before your handler sees them; your own `Authorization`
survives untouched.

The peer address sent for CIDR exemptions is the socket's address only, never a
forwarded-for header — a payer who can name their own peer address can exempt
themselves from paying.

Money is `int` everywhere, parsed from goldrail's decimal strings: these are
`u128` atomic units, and a balance that went through a float is a wrong balance.

## Reading the event stream

`PaymentEvent` decodes a line of goldrail's JSONL event stream, field names
unchanged, so an assertion written against the file matches one written against
the type:

```python
from goldrail import PaymentEvent

for line in open("payments.jsonl"):
    event = PaymentEvent.from_line(line)
    if event.moves_money:
        ...
```

`event.shadow` is a required field on the wire and a plain `bool` here, on
purpose: a shadow route is priced and observed but charges nobody, its records
never reach the ledger, and a consumer that missed the flag would report
imaginary money as real. So `moves_money` is `False` for every shadow event
whatever it *would* have done, while `never_sampled` is `True` for it — the
shadow stream has to sum to the shadow observations exactly as the real stream
sums to the ledger.

## Compatibility

Python 3.9+. Apache-2.0. The engine is a separate install — see
[goldsky-io/goldrail](https://github.com/goldsky-io/goldrail).

Metric names, payment-event fields, and `goldrail-*` header names are
semver-governed contract surface on the engine side, which is why they are
constants in `goldrail.types` rather than string literals at each call site.

## Development

```bash
python -m venv .venv && . .venv/bin/activate
pip install -e ".[dev]"
pytest
```

The tests run against a fake decision plane on loopback — real sockets, no
network, no engine, no money.
