Drop everything that duplicates PyCardano. The landscape scan done
2026-04-23 confirmed: no ecosystem gap for wallet/chain/tx-build —
pycardano 0.19.x covers all of it cleanly. The gap is the merchant
state machine, so that's all we ship.
Deleted:
- addresses.py → consumers call pycardano.HDWallet directly
- txbuild.py → consumers use pycardano.OgmiosChainContext directly
- oracles.py → consumers supply a price_fn callable
- mint.py → consumers build mint txs with pycardano;
CIP-25 v2 metadata builder shipped as a copy-paste
snippet in the README
- ipfs.py → py-ipfs-http-client covers it
- tradecraft_compat.py → no one was importing it; kill
- docs/minting-workflow.md → redundant with README pairing guidance
Refactored:
- monitor.evaluate_utxos: ADA-only. The DexHunter token-equivalent
block came out. Consumers who want stablecoin support wrap the
function with their own asset-to-lovelace converter.
- monitor.reprice_expired_invoices: takes a new required kwarg
price_fn: Callable[[float], Awaitable[int]]. No more ADA/USD
oracle shipped in the SDK.
- scheduler.InvoiceScheduler: takes an optional price_fn field;
if None, the reprice job is a no-op (works for fixed-ADA invoices).
Tests (26/26 passing, all offline):
- test_invoice.py — state-machine helpers, 3 tests
- test_store_protocol.py — Protocol conformance + InMemoryStore round-trips, 13 tests
- test_monitor_with_inmemory_store.py — all status transitions + reprice,
rewired to pass price_fn fixtures instead of monkeypatching oracle funcs
Deps dropped: pycardano (consumer pairing, not our dep).
Deps kept: httpx (Koios), apscheduler (background scheduler).
Package shape (1.0.0-dev, ~700 LOC src):
cardano_checkout/
invoice.py — Invoice + InvoiceStatus
store.py — InvoiceStore Protocol + InMemoryStore
monitor.py — Koios poll + UTxO matching + reprice driver
scheduler.py — APScheduler wrapper
README rewritten top-to-bottom: "what we ship", "what we don't ship",
why the niche exists, pycardano-directly examples for the delete-list,
CIP-25 builder as a 20-line copy-paste, InvoiceStore implementation
example. Apache-2.0 license unchanged.
|
||
|---|---|---|
| cardano_checkout | ||
| tests | ||
| .gitignore | ||
| LICENSE | ||
| pyproject.toml | ||
| README.md | ||
cardano-checkout
Merchant-side Cardano payment lifecycle in Python. Zero-custody by design.
What we ship: the invoice state machine + UTxO watcher + reprice loop. Per-invoice HD-derived receive addresses, Koios polling, confirm / underpay / overpay classification, time-windowed repricing against your own oracle.
What we don't ship: Cardano primitives. Address derivation, chain context, transaction building, native-script minting, signing — those are all pycardano's job. pycardano is mature, actively maintained (0.19.x as of 2026), and covers every primitive cleanly. This library slots next to it — no wrapping, no leaky abstraction, no second API to learn.
Why this exists
Nothing else in the Python ecosystem (or any ecosystem — we checked) packages zero-custody merchant Cardano payments as a reusable library. Closest adjacents are all one of:
- CIP-30 browser-wallet plugins (customer-signs, not server-watches)
- cardano-cli vending machines that watch a single static address (no xpub, no per-invoice derivation)
- SaaS APIs (NMKR) — not libraries
- Dormant / pre-1.0 grabs from the 2021-2023 era
The merchant state machine — "derive an address, watch for payment, confirm within tolerance, reprice if the quote lapses, emit a confirmed callback" — is what we package. You keep full control of everything else by using pycardano directly.
Quick start
import asyncio
from datetime import datetime, timedelta, timezone
from cardano_checkout import (
Invoice, InvoiceStatus, InMemoryStore, InvoiceScheduler,
)
# Your oracle — we don't ship one. Anything async returning int lovelace works.
async def my_price_fn(usd: float) -> int:
rate = await fetch_ada_usd_somewhere() # CoinGecko, Koios, fixed rate, etc.
return int(round(usd / rate * 1_000_000))
async def main() -> None:
store = InMemoryStore() # swap for your SQLAlchemy / asyncpg / sqlite adapter
# Create an invoice. In production you'd derive the receive address from
# your wallet xpub via pycardano — see the "Deriving addresses" section.
invoice = Invoice(
id="ord-0042",
merchant_id="chromaticcraft",
derivation_index=42,
receive_address="addr1q...", # derived via pycardano — your code
expected_lovelace=5_000_000,
usd_amount=2.50,
expires_at=datetime.now(timezone.utc) + timedelta(minutes=15),
)
await store.create(invoice)
# Run the background scheduler — Koios poll every 15s + reprice every 60s.
scheduler = InvoiceScheduler(store=store, price_fn=my_price_fn)
await scheduler.start()
# ... app runs ...
await scheduler.stop()
asyncio.run(main())
Deriving addresses with pycardano
We used to wrap this. You don't need the wrapper.
from pycardano import HDWallet, Address, Network
# Your merchant's account-level xpub — the xpub is public, not a secret.
xpub_hex = "..."
account = HDWallet.from_xpub(bytes.fromhex(xpub_hex))
def derive_address(account: HDWallet, index: int, network=Network.MAINNET) -> str:
payment = account.derive(0).derive(index) # external chain, address index
staking = account.derive(2).derive(0) # staking chain, always index 0
addr = Address(
payment_part=payment.public_key.hash(),
staking_part=staking.public_key.hash(),
network=network,
)
return str(addr)
addr = derive_address(account, index=42)
Six lines of pycardano that read cleanly against their docs. Our old wrapper would have added one function call but made you learn our API instead of pycardano's. Skip it.
NFT cert-of-authenticity: CIP-25 v2 metadata
If you want each paid order to ship with an on-chain NFT cert, here's the CIP-25 v2 metadata builder as a copy-paste. It fits in your own code — no dep, no wrapper.
def build_cip25_metadata(
*,
policy_id: str,
asset_name: str,
name: str,
image_cid: str,
description: str = "",
media_type: str = "image/jpeg",
properties: dict | None = None,
) -> dict:
"""Build a CIP-25 v2 metadata envelope.
Returns a dict ready to submit as transaction metadatum label 721.
Handles the 64-char chunking rule for long descriptions.
"""
def chunk64(s: str) -> list[str]:
if len(s) <= 64:
return [s]
return [s[i:i + 64] for i in range(0, len(s), 64)]
body: dict = {
"name": name,
"image": f"ipfs://{image_cid}",
"mediaType": media_type,
}
if description:
body["description"] = description if len(description) <= 64 else chunk64(description)
if properties:
body.update(properties)
return {
"721": {
policy_id: {asset_name: body},
"version": "2.0",
}
}
Hand that dict to pycardano's AuxiliaryData(Metadata({...})) when you
build the mint tx. Straight pycardano from there on.
Implementing your own InvoiceStore
The SDK's InvoiceStore is a Protocol — implement the six methods
against whatever backend you want (SQLAlchemy, asyncpg, SQLite,
in-memory for tests).
from cardano_checkout import Invoice, InvoiceStatus, InvoiceStore
class MySqliteStore:
# Implement these six methods and you're a valid InvoiceStore.
async def create(self, invoice: Invoice) -> None: ...
async def get(self, invoice_id: str) -> Invoice | None: ...
async def list_by_status(self, status: InvoiceStatus, limit: int = 100) -> list[Invoice]: ...
async def update(self, invoice: Invoice) -> None: ...
async def next_derivation_index(self, merchant_id: str) -> int: ...
async def record_tx(self, invoice_id: str, tx_hash: str, lovelace_delta: int) -> None: ...
See InMemoryStore in cardano_checkout/store.py for a 90-line
reference implementation.
Status (1.0.0-dev)
| Module | Purpose |
|---|---|
invoice.py |
Invoice dataclass + InvoiceStatus enum — payment lifecycle states |
store.py |
InvoiceStore Protocol + InMemoryStore reference impl |
monitor.py |
check_address_utxos (Koios), evaluate_utxos (ADA matching + tolerance), check_pending_invoices, reprice_expired_invoices (takes your price_fn) |
scheduler.py |
InvoiceScheduler — APScheduler wrapper, runs check/reprice on the same 15s/60s cadence TradeCraft's used in production since 2025 |
All tests offline, 26/26 green. Two direct deps: httpx (Koios calls),
apscheduler (background scheduling). No pycardano dep — that's the
consumer's pairing.
Design principles
- Protocol-first. Persistence, pricing, and any other side-effect concern goes through a consumer-supplied interface. The SDK has no opinion about your database, your oracle, or your ORM.
- Use pycardano directly. We don't wrap primitives. If you need address derivation, chain context, or transaction building, import pycardano. Our package sits next to it, not on top.
- Zero-custody. The merchant's keys never touch this code. We handle xpub-derived addresses (public), UTxO observation (chain), and state transitions (the store). Funds flow directly between customer and merchant wallets. We are not a custodian.
- Offline-first tests. Koios HTTP and price oracles are stubbed or swapped via fixture. No network in CI. Live tests (preprod mint round-trips, real Koios) are a consumer-side concern.
License
Apache-2.0 — matches the broader Cardano tooling ecosystem.