Python SDK for merchant-side Cardano payments + NFT cert minting (zero-custody)
Find a file
Cobb Hayes c592a58148 Public-flip audit: drop audit-ticket prefixes + topology refs + AI scaffolding
cardano-api: strip 'Fix #N:' audit-ticket prefixes from inline comments (was
50+ in main.py), drop hardening-pass changelog blocks from module docstring,
rewrite README to drop deploy paths + marketing sections, keep tier/auth/TTL
+ policy IDs.

cardano-checkout-py: drop TradeCraft lineage refs, swap chromaticcraft/tradecraft
test fixtures for acme/globex, repository URL → git.sulkta.com.
2026-05-27 11:15:03 -07:00
cardano_checkout Public-flip audit: drop audit-ticket prefixes + topology refs + AI scaffolding 2026-05-27 11:15:03 -07:00
tests Public-flip audit: drop audit-ticket prefixes + topology refs + AI scaffolding 2026-05-27 11:15:03 -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 Public-flip audit: drop audit-ticket prefixes + topology refs + AI scaffolding 2026-05-27 11:15:03 -07:00
README.md Public-flip audit: drop audit-ticket prefixes + topology refs + AI scaffolding 2026-05-27 11:15:03 -07:00

cardano-checkout

Merchant-side Cardano payment lifecycle in Python. Zero-custody.

Ships 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.

Does NOT ship Cardano primitives. Address derivation, chain context, transaction building, native-script minting, signing — use pycardano directly. This library slots next to it.

Quick start

import asyncio
from datetime import datetime, timedelta, timezone

from cardano_checkout import (
    Invoice, InvoiceStatus, InMemoryStore, InvoiceScheduler,
)


# Your oracle. 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

    invoice = Invoice(
        id="ord-0042",
        merchant_id="my-shop",
        derivation_index=42,
        receive_address="addr1q...",  # derive via pycardano
        expected_lovelace=5_000_000,
        usd_amount=2.50,
        expires_at=datetime.now(timezone.utc) + timedelta(minutes=15),
    )
    await store.create(invoice)

    scheduler = InvoiceScheduler(store=store, price_fn=my_price_fn)
    await scheduler.start()

    # ... app runs ...

    await scheduler.stop()

asyncio.run(main())

Deriving addresses with pycardano

from pycardano import HDWallet, Address, Network

# Account-level xpub — 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)

NFT cert: CIP-25 v2 metadata

Copy-paste builder for an on-chain cert per paid order. No dep.

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 the dict to pycardano's AuxiliaryData(Metadata({...})) when building the mint tx.

Implementing your own InvoiceStore

InvoiceStore is a Protocol — implement six methods against whatever backend you want (SQLAlchemy, asyncpg, SQLite, in-memory).

from cardano_checkout import Invoice, InvoiceStatus, InvoiceStore

class MySqliteStore:
    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 reference impl.

Modules

Module Purpose
invoice.py Invoice dataclass + InvoiceStatus enum
store.py InvoiceStore Protocol + InMemoryStore reference impl
monitor.py check_address_utxos (Koios), evaluate_utxos, check_pending_invoices, reprice_expired_invoices
scheduler.py InvoiceScheduler — APScheduler wrapper, 15s check + 60s reprice

Two direct deps: httpx, apscheduler. No pycardano dep.

Design

  1. Protocol-first. Persistence, pricing, side-effects through consumer-supplied interfaces.
  2. Use pycardano directly. No wrapping of primitives.
  3. Zero-custody. Merchant keys never touch this code. xpub-derived addresses, UTxO observation, state transitions. Funds flow directly between customer and merchant wallets.
  4. Offline-first tests. Koios + price oracles stubbed via fixture.

License

Apache-2.0.