Python SDK for merchant-side Cardano payments + NFT cert minting (zero-custody)
Find a file
Kayos af41f945b1 v1.0.0-dev: slim to the real product — merchant state machine only
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.
2026-04-23 21:58:26 -07:00
cardano_checkout v1.0.0-dev: slim to the real product — merchant state machine only 2026-04-23 21:58:26 -07:00
tests v1.0.0-dev: slim to the real product — merchant state machine only 2026-04-23 21:58:26 -07:00
.gitignore v0.1.0-dev: initial extraction from TradeCraft + new abstractions 2026-04-23 18:04:00 -07:00
LICENSE v0.1.0-dev: initial extraction from TradeCraft + new abstractions 2026-04-23 18:04:00 -07:00
pyproject.toml v1.0.0-dev: slim to the real product — merchant state machine only 2026-04-23 21:58:26 -07:00
README.md v1.0.0-dev: slim to the real product — merchant state machine only 2026-04-23 21:58:26 -07:00

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

  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.