README status table moves everything green except the TradeCraft compat shim (still yellow, documented sunset path). Adds a migration guide section mapping every old services/cardano_*.py import to its new cardano_checkout.* equivalent so TradeCraft can adopt in one atomic diff once the SQLAlchemyInvoiceStore adapter lands. docs/minting-workflow.md: step-by-step runbook for the cold-signer pattern — hot host builds UnsignedMint, operator ships three CBOR hex files to Lucy, offline signer produces a signed tx, hot host submits via submit_signed_tx. Covers the tx-id sanity check, skey hygiene rules, time-locked-policy TTL clamp, and the preprod dry-run requirement for every new policy. |
||
|---|---|---|
| cardano_checkout | ||
| docs | ||
| tests | ||
| .gitignore | ||
| LICENSE | ||
| pyproject.toml | ||
| README.md | ||
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 TradeCraft'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 |
tradecraft_compat |
🟡 compat shim | Keeps TradeCraft's subscription + grace-period jobs alive during migration |
txbuild |
✅ v0.2 | OgmiosChainContext wiring + submit_signed_tx + address UTxO queries |
Migration status for TradeCraft: still imports the old module paths. See the v0.2 migration guide below.
Design
┌────────────────────────────────────────────────────────┐
│ Merchant App │
│ (TradeCraft / chromaticcraft / 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:
- A wallet xpub (account-level extended public key).
- An
InvoiceStoreimplementation (SQLAlchemy, Postgres, SQLite, in-memory — whatever).
The SDK provides:
- Address derivation from the xpub.
- Per-invoice payment monitoring against Koios.
- ADA ↔ USD price conversion.
- CIP-25 v2 NFT cert minting with a cold-signer hand-off.
- 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="chromaticcraft",
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 TradeCraft.
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 chromaticcraft 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://192.168.254.5:5001"], # Lucy's kubo over the LAN/VPN
)
cid = await client.add(photo_bytes, filename="order-0001.jpg")
# Image now served by Rackham (low latency) AND pinned on Lucy (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 (Lucy in Sulkta's pattern — 2-of-2 native script: Cobb + Kayos). 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 TradeCraft
The generic invoice jobs moved to a Protocol-based API. The
subscription + grace-period jobs stayed TradeCraft-specific and live in
cardano_checkout.tradecraft_compat.
Import changes when TradeCraft 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 tradecraft_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.tradecraft_compat import check_subscription_payments, reprice_subscription_payments, enforce_grace_period (verbatim jobs — TradeCraft 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 TradeCraft 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 tradecraft_compat unchanged.
TODO for future sprints:
- Ship a
cardano_checkout.adapters.sqlalchemy.SQLAlchemyInvoiceStoreso TradeCraft doesn't have to write the adapter from scratch. - Once TradeCraft's subscription jobs are migrated to a subscription-
specific Protocol, delete
tradecraft_compat. - Refund-path
build_payment_txintxbuild.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.