Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/check_python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
cache-dependency-glob: "python/x402/uv.lock"

- name: Set up Python
run: uv python install
run: uv python install 3.13

- name: Install dependencies
run: uv sync --all-extras --dev
Expand Down
1 change: 1 addition & 0 deletions docs/sdk-features.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ This page tracks which features are implemented in each SDK (TypeScript, Go, Pyt
| exact/evm (EIP-3009) | ✅ | ✅ | ✅ |
| exact/evm (Permit2) | ✅ | ✅ | ✅ |
| exact/svm (SPL) | ✅ | ✅ | ✅ |
| exact/bip122 (Lightning) | ❌ | ❌ | ✅ |
| exact/stellar (Soroban) | ✅ | ❌ | ❌ |
| exact/aptos (Fungible Assets) | ✅ | ❌ | ❌ |
| upto/evm (Permit2) | ✅ | ✅ | ❌ |
Expand Down
2 changes: 2 additions & 0 deletions python/x402/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ uv add x402[flask] # Flask middleware
# Blockchain mechanisms (pick one or both)
uv add x402[evm] # EVM/Ethereum
uv add x402[svm] # Solana
uv add x402[lightning] # Bitcoin Lightning

# Multiple extras
uv add x402[fastapi,httpx,evm]
Expand Down Expand Up @@ -256,6 +257,7 @@ client.register("eip155:8453", CustomScheme())
- `x402.http` - HTTP clients, middleware, and facilitator client
- `x402.mechanisms.evm` - EVM/Ethereum implementation
- `x402.mechanisms.svm` - Solana implementation
- `x402.mechanisms.bip122` - Bitcoin Lightning implementation
- `x402.extensions` - Protocol extensions (Bazaar discovery)

## Examples
Expand Down
3 changes: 3 additions & 0 deletions python/x402/changelog.d/1857.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Added a Python BIP-122 / Lightning `exact` reference mechanism with payer/receiver
adapters, invoice-backed requirement generation, and `payment_hash` settlement
identifiers.
105 changes: 105 additions & 0 deletions python/x402/mechanisms/bip122/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# x402 BIP-122 Mechanism

Bitcoin Lightning implementation of the x402 payment protocol using the `exact`
payment scheme with BOLT11 invoices.

## Installation

```bash
uv add x402[lightning]
```

## Overview

Three components for handling x402 payments on Bitcoin Lightning:

- `ExactBip122ClientScheme` pays a BOLT11 invoice through a `LightningPayer`
- `ExactBip122ServerScheme` builds invoice-backed payment requirements
- `ExactBip122FacilitatorScheme` verifies paid invoices and returns `payment_hash`

## Quick Start

### Client

```python
from x402 import x402Client
from x402.mechanisms.bip122.exact import ExactBip122Scheme

client = x402Client()
client.register("bip122:*", ExactBip122Scheme(payer=lightning_payer))
```

### Server

```python
from x402 import x402ResourceServer
from x402.mechanisms.bip122.exact import ExactBip122ServerScheme

server = x402ResourceServer(facilitator_client)
server.register("bip122:*", ExactBip122ServerScheme(receiver=lightning_receiver))
```

### Facilitator

```python
from x402 import x402Facilitator
from x402.mechanisms.bip122.exact import ExactBip122FacilitatorScheme

facilitator = x402Facilitator()
facilitator.register(
["bip122:000000000019d6689c085ae165831e93"],
ExactBip122FacilitatorScheme(receiver=lightning_receiver),
)
```

## Exports

### `x402.mechanisms.bip122.exact`

| Export | Description |
|--------|-------------|
| `ExactBip122Scheme` | Client scheme (alias for `ExactBip122ClientScheme`) |
| `ExactBip122ClientScheme` | Client-side invoice payment |
| `ExactBip122ServerScheme` | Server-side invoice generation |
| `ExactBip122FacilitatorScheme` | Facilitator verification/settlement |
| `register_exact_bip122_client()` | Helper to register client |
| `register_exact_bip122_server()` | Helper to register server |
| `register_exact_bip122_facilitator()` | Helper to register facilitator |

### `x402.mechanisms.bip122`

| Export | Description |
|--------|-------------|
| `LightningPayer` | Protocol for client payer adapters |
| `LightningReceiver` | Protocol for server/facilitator receiver adapters |
| `ExactBip122Payload` | BOLT11 payload wrapper |
| `LightningInvoiceStatus` | Normalized invoice state |
| `SettlementCache` | Duplicate settlement protection |
| `sat_to_msat()` | Convert sats to millisatoshis |
| `msat_to_sat()` | Convert millisatoshis to sats |

## Supported Networks

- `bip122:000000000019d6689c085ae165831e93` - Bitcoin Mainnet
- `bip122:000000000933ea01ad0ee984209779ba` - Bitcoin Testnet
- `bip122:*` - Wildcard (all supported BIP-122 Bitcoin networks)

## Price Semantics

By default, server-side `parse_price()` treats money values as satoshis and
converts them to millisatoshis internally:

- `100` -> `100000` msat
- `0.1` -> `100` msat

If you need fiat pricing or external rate lookup, register a custom money parser
on `ExactBip122ServerScheme`.

## Technical Details

- `asset` is always `"BTC"`
- `pay_to` is always normalized to `"anonymous"`
- `extra.paymentMethod` is always `"lightning"`
- `extra.invoice` carries the BOLT11 invoice string
- `SettleResponse.transaction` returns the invoice `payment_hash`

51 changes: 51 additions & 0 deletions python/x402/mechanisms/bip122/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""BIP-122 / Bitcoin Lightning mechanism exports."""

from .constants import (
BTC_ASSET,
BTC_MAINNET_CAIP2,
BTC_TESTNET_CAIP2,
DEFAULT_INVOICE_DESCRIPTION,
DEFAULT_SETTLEMENT_TTL_SECONDS,
NETWORK_CONFIGS,
PAY_TO_ANONYMOUS,
PAYMENT_METHOD_LIGHTNING,
SCHEME_EXACT,
)
from .payer import LightningPayer
from .receiver import LightningReceiver
from .settlement_cache import SettlementCache
from .types import ExactBip122Payload, ExactBip122PayloadV2, LightningInvoiceStatus
from .utils import (
decode_invoice,
get_invoice_payment_hash,
get_network_config,
msat_to_sat,
normalize_network,
sat_to_msat,
validate_bip122_network,
)

__all__ = [
"SCHEME_EXACT",
"BTC_ASSET",
"BTC_MAINNET_CAIP2",
"BTC_TESTNET_CAIP2",
"PAYMENT_METHOD_LIGHTNING",
"PAY_TO_ANONYMOUS",
"DEFAULT_INVOICE_DESCRIPTION",
"DEFAULT_SETTLEMENT_TTL_SECONDS",
"NETWORK_CONFIGS",
"LightningPayer",
"LightningReceiver",
"SettlementCache",
"ExactBip122Payload",
"ExactBip122PayloadV2",
"LightningInvoiceStatus",
"normalize_network",
"validate_bip122_network",
"get_network_config",
"decode_invoice",
"get_invoice_payment_hash",
"sat_to_msat",
"msat_to_sat",
]
42 changes: 42 additions & 0 deletions python/x402/mechanisms/bip122/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""BIP-122 mechanism constants for Bitcoin Lightning payments."""

from typing import TypedDict

SCHEME_EXACT = "exact"

BTC_MAINNET_CAIP2 = "bip122:000000000019d6689c085ae165831e93"
BTC_TESTNET_CAIP2 = "bip122:000000000933ea01ad0ee984209779ba"

BTC_ASSET = "BTC"
PAYMENT_METHOD_LIGHTNING = "lightning"
PAY_TO_ANONYMOUS = "anonymous"
DEFAULT_INVOICE_DESCRIPTION = "x402 payment"
DEFAULT_SETTLEMENT_TTL_SECONDS = 86_400.0
SETTLEMENT_TTL_BUFFER_SECONDS = 3_600.0

ERR_UNSUPPORTED_SCHEME = "unsupported_scheme"
ERR_NETWORK_MISMATCH = "network_mismatch"
ERR_INVALID_ASSET = "invalid_asset"
ERR_INVALID_PAY_TO = "invalid_pay_to"
ERR_INVALID_PAYMENT_METHOD = "invalid_payment_method"
ERR_MISSING_INVOICE = "invalid_exact_bip122_payload_missing_invoice"
ERR_INVALID_INVOICE = "invalid_exact_bip122_payload_invalid_invoice"
ERR_INVOICE_MISMATCH = "invalid_exact_bip122_payload_invoice_mismatch"
ERR_UNKNOWN_INVOICE = "unknown_invoice"
ERR_INVOICE_EXPIRED = "invoice_expired"
ERR_AMOUNT_MISMATCH = "amount_mismatch"
ERR_INVOICE_NOT_PAID = "invoice_not_paid"
ERR_INVOICE_IN_FLIGHT = "invoice_in_flight"
ERR_DUPLICATE_SETTLEMENT = "duplicate_settlement"


class NetworkConfig(TypedDict):
"""Configuration for a BIP-122 network."""

currency: str


NETWORK_CONFIGS: dict[str, NetworkConfig] = {
BTC_MAINNET_CAIP2: {"currency": "bc"},
BTC_TESTNET_CAIP2: {"currency": "tb"},
}
22 changes: 22 additions & 0 deletions python/x402/mechanisms/bip122/exact/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"""Exact BIP-122 payment scheme for x402."""

from .client import ExactBip122Scheme as ExactBip122ClientScheme
from .facilitator import ExactBip122Scheme as ExactBip122FacilitatorScheme
from .register import (
register_exact_bip122_client,
register_exact_bip122_facilitator,
register_exact_bip122_server,
)
from .server import ExactBip122Scheme as ExactBip122ServerScheme

ExactBip122Scheme = ExactBip122ClientScheme

__all__ = [
"ExactBip122Scheme",
"ExactBip122ClientScheme",
"ExactBip122ServerScheme",
"ExactBip122FacilitatorScheme",
"register_exact_bip122_client",
"register_exact_bip122_server",
"register_exact_bip122_facilitator",
]
56 changes: 56 additions & 0 deletions python/x402/mechanisms/bip122/exact/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"""BIP-122 client implementation for the Exact payment scheme (V2)."""

from typing import Any

from ....schemas import PaymentRequirements
from ..constants import (
ERR_INVALID_PAYMENT_METHOD,
ERR_INVOICE_MISMATCH,
ERR_MISSING_INVOICE,
ERR_UNSUPPORTED_SCHEME,
PAYMENT_METHOD_LIGHTNING,
SCHEME_EXACT,
)
from ..payer import LightningPayer
from ..types import ExactBip122Payload
from ..utils import decode_invoice, normalize_network


class ExactBip122Scheme:
"""BIP-122 client implementation for the Exact payment scheme (V2)."""

scheme = SCHEME_EXACT

def __init__(self, payer: LightningPayer):
self._payer = payer

def create_payment_payload(
self,
requirements: PaymentRequirements,
) -> dict[str, Any]:
"""Pay the invoice referenced by the requirements and return the inner payload."""
if requirements.scheme != SCHEME_EXACT:
raise ValueError(ERR_UNSUPPORTED_SCHEME)

network = normalize_network(str(requirements.network))
extra = requirements.extra or {}
payment_method = extra.get("paymentMethod")
if payment_method != PAYMENT_METHOD_LIGHTNING:
raise ValueError(ERR_INVALID_PAYMENT_METHOD)

invoice = extra.get("invoice")
if not invoice or not isinstance(invoice, str):
raise ValueError(ERR_MISSING_INVOICE)

decoded = decode_invoice(invoice)
invoice_amount_msat = int(decoded.amount_msat or 0)
if invoice_amount_msat != int(requirements.amount):
raise ValueError(ERR_INVOICE_MISMATCH)

status = self._payer.pay_invoice(invoice, network)
if status.invoice != invoice or status.payment_hash != decoded.payment_hash:
raise ValueError(ERR_INVOICE_MISMATCH)
if status.status != "paid":
raise ValueError(f"Invoice payment did not complete: {status.status}")

return ExactBip122Payload(invoice=invoice).to_dict()
Loading
Loading