# 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](https://github.com/Python-Cardano/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 ```python 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. ```python 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](https://pycardano.readthedocs.io/). 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. ```python 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). ```python 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 1. **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. 2. **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. 3. **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. 4. **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.