Python SDK for merchant-side Cardano payments + NFT cert minting (zero-custody)
Find a file
2026-04-23 20:00:49 -07:00
cardano_checkout v0.2: wire mint + txbuild end-to-end against local Ogmios 2026-04-23 19:55:45 -07:00
tests v0.2: add store, mint, and monitor integration tests 2026-04-23 19:58:46 -07:00
.gitignore v0.1.0-dev: initial public extraction + new abstractions 2026-04-23 18:04:00 -07:00
LICENSE v0.1.0-dev: initial public extraction + new abstractions 2026-04-23 18:04:00 -07:00
pyproject.toml v0.2: refactor monitor + scheduler around InvoiceStore Protocol 2026-04-23 19:55:28 -07:00
README.md v0.2: README rewrite + cold-signer runbook 2026-04-23 20:00:49 -07:00

cardano-checkout

Python SDK for merchant-side Cardano payments + NFT certificate-of-authenticity minting.

Zero-custody by design: the merchant provides a wallet xpub. The SDK derives unique receive addresses per invoice, polls the chain for payment, and optionally mints a CIP-25 NFT cert on confirmation. The platform never holds or moves funds.

Extracted from the host app's services/cardano_*.py modules (2,400+ lines of production code running on the Cardano mainnet) and packaged for reuse across the Sulkta Coop product family.

Status

v0.2.0-dev — Protocol-first core + live mint path. Monitor and scheduler have been refactored off SQLAlchemy and onto the InvoiceStore Protocol. Mint builds real transaction bodies against a local Ogmios endpoint and returns an UnsignedMint for cold-signing.

Module Status Notes
addresses stable CIP-1852 HD derivation via pycardano HDWallet soft derive
oracles stable ADA/USD price via CoinGecko + DexHunter, 5-min cache
invoice + store stable Framework-agnostic invoice + InMemoryStore reference impl
mint v0.2 CIP-25 v2 metadata + real tx body → UnsignedMint bundle
ipfs stable kubo HTTP API client w/ optional mirror-pin
monitor v0.2 Operates purely through InvoiceStore — no ORM coupling
scheduler v0.2 InvoiceScheduler drives check + reprice against the store
hostapp_compat 🟡 compat shim Keeps the host app's subscription + grace-period jobs alive during migration
txbuild v0.2 OgmiosChainContext wiring + submit_signed_tx + address UTxO queries

Migration status for the host app: still imports the old module paths. See the v0.2 migration guide below.

Design

     ┌────────────────────────────────────────────────────────┐
     │                    Merchant App                        │
     │   (the host app / example-studio / your-product)         │
     └──────────────┬───────────────────────┬─────────────────┘
                    │                       │
         uses      │ implements             │ imports
                    ▼                       ▼
        ┌──────────────┐         ┌────────────────────────┐
        │ InvoiceStore │ ◄────── │  cardano_checkout SDK  │
        │  (your DB)   │         │                        │
        └──────────────┘         │  addresses    ← pure   │
                                 │  oracles      ← pure   │
                                 │  invoice      ← dataclass │
                                 │  store        ← Protocol + InMemoryStore │
                                 │  monitor      ← polls chain via store │
                                 │  scheduler    ← bg loop  │
                                 │  mint         ← NFT cert (cold-signer) │
                                 │  ipfs         ← upload   │
                                 │  txbuild      ← Ogmios wrappers │
                                 └────────────────────────┘
                                           │
                                  talks to │
                                           ▼
                                 ┌────────────────────────┐
                                 │ Koios + Ogmios + kubo  │
                                 └────────────────────────┘

The merchant app provides:

  1. A wallet xpub (account-level extended public key).
  2. An InvoiceStore implementation (SQLAlchemy, Postgres, SQLite, in-memory — whatever).

The SDK provides:

  1. Address derivation from the xpub.
  2. Per-invoice payment monitoring against Koios.
  3. ADA ↔ USD price conversion.
  4. CIP-25 v2 NFT cert minting with a cold-signer hand-off.
  5. IPFS upload + pinning for NFT image metadata.

Quick start

import asyncio
from cardano_checkout import addresses, oracles

# Derive a receive address for invoice #42
addr = addresses.derive_address(
    xpub_hex="<your wallet xpub>",
    index=42,
    network="mainnet",
)

# Convert a USD price to lovelace at current market
async def main() -> None:
    lovelace = await oracles.convert_usd_to_lovelace(99.00)
    ada = lovelace / 1_000_000
    print(f"Customer owes {ada:.4f} ADA for $99")

asyncio.run(main())

Payment monitoring

import asyncio
from cardano_checkout import InMemoryStore, Invoice, InvoiceStatus, InvoiceScheduler

store = InMemoryStore()  # swap for your real SQLAlchemy / asyncpg / SQLite adapter

# Create an invoice (typically you'd derive the address here via addresses.derive_address)
invoice = Invoice(
    id="ord-0042",
    merchant_id="example-studio",
    derivation_index=42,
    receive_address="addr1q...",
    expected_lovelace=5_000_000,
    usd_amount=2.50,
)
asyncio.run(store.create(invoice))

# Wire the background scheduler — same 15s check / 60s reprice cadence as the host app.
scheduler = InvoiceScheduler(store=store)
asyncio.run(scheduler.start())
# ... your app runs ...
asyncio.run(scheduler.stop())

IPFS: bake-then-mirror pattern

The SDK's IPFSClient expects a local kubo daemon (typically in the same Docker image as the web app) for upload and primary pin, and takes an optional list of mirror endpoints to pin add the CID on a second node for archival redundancy.

Typical example-studio deployment:

from cardano_checkout import ipfs

client = ipfs.IPFSClient(
    api_url="http://127.0.0.1:5001",           # local kubo in the same container
    mirror_api_urls=["http://mirror-node.example:5001"],  # the cold host's kubo over the LAN/VPN
)

cid = await client.add(photo_bytes, filename="order-0001.jpg")
# Image now served by the hot host (low latency) AND pinned on the cold host (durability)

NFT cert-of-authenticity design

One minting policy per merchant studio. Policy is a native script (no Plutus required), optionally time-locked to make "no more editions after X" a cryptographically verifiable claim.

CIP-25 v2 metadata. Single NFT per order. Policy skey never leaves the custody host (the cold host in Sulkta's pattern — 2-of-2 native script: signer 1 + signer 2). The SDK builds the metadata envelope + tx body on the hot node and returns an UnsignedMint bundle; an external offline signer provides the vkey witnesses; the hot node submits the assembled CBOR.

The full operator runbook — including the exact byte-movement sequence, verification checklist, and preprod dry-run procedure — lives in docs/minting-workflow.md.

v0.2 migration guide for the host app

The generic invoice jobs moved to a Protocol-based API. The subscription + grace-period jobs stayed the host app-specific and live in cardano_checkout.hostapp_compat.

Import changes when the host app adopts the SDK:

Was Becomes
from services.cardano_monitor import check_pending_payments, reprice_expired_payments from cardano_checkout.monitor import check_pending_invoices, reprice_expired_invoices
from services.cardano_monitor import _check_address_utxos, _evaluate_payment from cardano_checkout.monitor import check_address_utxos, evaluate_utxos (or import from hostapp_compat for the exact old names)
from services.cardano_scheduler import start_cardano_scheduler, stop_cardano_scheduler from cardano_checkout.scheduler import InvoiceScheduler (instantiate with your store)
from services.cardano_scheduler import _check_subscription_payments, _reprice_subscription_payments, _enforce_grace_period from cardano_checkout.hostapp_compat import check_subscription_payments, reprice_subscription_payments, enforce_grace_period (verbatim jobs — the host app still drives them directly)
from services.cardano_price import * from cardano_checkout.oracles import *
from services.cardano_addresses import derive_address from cardano_checkout.addresses import derive_address

What the host app still needs to write:

A SQLAlchemy adapter implementing InvoiceStore against the existing CardanoPayment table. list_by_status maps to a SELECT ... WHERE status = :s, next_derivation_index to a SELECT MAX(derivation_index) + 1, etc. That's 80-ish lines of wrapper code — nothing exotic. Once that's landed, the generic jobs run through InvoiceScheduler(store=SQLAlchemyInvoiceStore(...)) and the subscription jobs keep running through hostapp_compat unchanged.

TODO for future sprints:

  • Ship a cardano_checkout.adapters.sqlalchemy.SQLAlchemyInvoiceStore so the host app doesn't have to write the adapter from scratch.
  • Once the host app's subscription jobs are migrated to a subscription- specific Protocol, delete hostapp_compat.
  • Refund-path build_payment_tx in txbuild.py (v0.3).
  • Batched mints (sell-sheet of 10 NFTs at once).

Testing

pip install -e '.[test]'
pytest                                        # 42 tests, all offline

The test suite mocks the chain context for mint-tx construction and monkey-patches Koios + the oracle for monitor tests — CI never touches a live node. The address-derivation tests use a deterministic test-vector xpub from the standard "test ... junk" mnemonic so they can't drift.

Installation

pip install 'cardano-checkout[sqlalchemy]'   # if you're using SQLAlchemy
pip install cardano-checkout                  # core only

License

Apache-2.0 — matches upstream Cardano tooling.