Sardis

Payment Objects

UTXO-inspired one-time payment tokens with a 22-state lifecycle, funding cells, and cryptographic merchant verification.

Key insight: Traditional payment systems rely on account balances and mutable state, leading to race conditions and double-spend risks in concurrent agent environments. Sardis uses a UTXO-inspired model where each payment is an atomic, signed, single-use token backed by claimed funding cells -- making double-spend structurally impossible.

Overview

A Payment Object is the atomic unit of payment in Sardis. Inspired by Bitcoin's UTXO (Unspent Transaction Output) model, each payment object is a one-time-use, cryptographically signed token that bridges an off-chain spending mandate to an on-chain settlement.

The UTXO model provides three critical properties for AI agent payments:

  1. Double-spend prevention -- Each funding cell can only be consumed once. Once spent, it is gone.
  2. Concurrent safety -- Multiple agents can claim cells simultaneously without deadlocks using SELECT ... FOR UPDATE SKIP LOCKED.
  3. Deterministic audit trails -- Every cent is traceable from funding commitment through cell claim to payment object to settlement.

Unlike account-balance systems where concurrent withdrawals can overdraw, the cell-based model ensures that if an agent claims a cell, no other agent can claim the same cell -- even under heavy concurrent load.

Core Concepts

Payment Object Dataclass

A PaymentObject is an immutable-by-design dataclass with the following fields:

FieldTypeDescription
object_idstrUnique identifier (e.g., po_a1b2c3d4e5f6g7h8)
mandate_idstrSource spending mandate that authorized minting
merchant_idstrMerchant this payment is payable to
cell_idslist[str]Funding cells claimed from the mandate budget
exact_amountDecimalExact payment amount (never float)
currencystrCurrency code (default: USDC)
one_time_useboolAlways True -- enforced invariant
signature_chainlist[str]Signatures from principal, issuer, and agent
session_hashstrSHA-256 replay protection hash
expires_atdatetimeExpiration timestamp
statusPaymentObjectStatusCurrent lifecycle state
privacy_tierPrivacyTierOn-chain data exposure level
created_atdatetimeCreation timestamp
metadatadictExtensible key-value metadata

Privacy Tiers

Payment objects support three levels of on-chain data exposure:

TierDescription
transparentFull payment details visible on-chain
hybridAmount visible, parties hashed
full_zkZero-knowledge proof, no details exposed

When hybrid or full_zk is selected, the minter generates a ZKP proof (via sardis_zkp) that proves mandate compliance without revealing the underlying data.

Integrity Hash

The compute_hash() method produces a deterministic SHA-256 digest of the payment object's immutable fields (identity, financial, and security fields). Mutable fields like status and metadata are intentionally excluded so that the hash remains stable across state transitions.

canonical = {
    "object_id": self.object_id,
    "mandate_id": self.mandate_id,
    "merchant_id": self.merchant_id,
    "cell_ids": sorted(self.cell_ids),
    "exact_amount": str(self.exact_amount),
    "currency": self.currency,
    "one_time_use": self.one_time_use,
    "session_hash": self.session_hash,
    "expires_at": self.expires_at.isoformat() if self.expires_at else None,
    "privacy_tier": self.privacy_tier.value,
    "created_at": self.created_at.isoformat(),
}
hash = sha256(json.dumps(canonical, sort_keys=True).encode()).hexdigest()

Payment Object Status Enum

Payment objects progress through 13 statuses defined in PaymentObjectStatus:

StatusDescriptionTerminal?
mintedCreated and signedNo
presentedShown to merchant for verificationNo
verifiedMerchant verified signaturesNo
lockedFunds locked for settlementNo
settlingOn-chain settlement in progressNo
settledSettlement confirmed on-chainNo
fulfilledDelivery confirmed, payment completeYes
escrowedFunds held in escrowNo
disputingUnder disputeNo
revokedCancelled before settlementYes
expiredPast expiration without settlementYes
failedSettlement failed on-chainYes
refundedRefund processedYes

Funding Cells

A FundingCell is an indivisible (or splittable) unit of value carved from a funding commitment. Cells follow UTXO semantics:

  • Each cell holds a fixed value in a given currency (always Decimal, never float)
  • Cells are consumed (spent) by payment objects
  • When a cell exceeds the required amount, it is split and change is returned as a new cell
  • Cells can be merged to reduce fragmentation

Cell Status Lifecycle

AVAILABLE --> CLAIMED --> SPENT
                     \--> RETURNED --> AVAILABLE  (re-entry)
AVAILABLE --> MERGED   (combined into a larger cell)
*         --> EXPIRED  (commitment expired)
StatusDescription
availableReady to be claimed
claimedReserved for a mandate
spentUsed in a payment object
returnedReleased back to available pool
mergedCombined into another cell
expiredParent commitment expired

Valid Cell Transitions

FromToTransition
availableclaimedclaim
claimedspentspend
claimedreturnedrelease
returnedavailablerecycle
availablemergedmerge
availableexpiredexpire
claimedexpiredexpire

22-State Lifecycle

The full payment state machine defines 22 states with three primary paths and multiple terminal branches:

                    ┌──────────────────────────────────────────────────────┐
                    │              HAPPY PATH                              │
                    │                                                      │
  ┌────────┐   ┌───────────┐   ┌──────────┐   ┌────────┐   ┌──────────┐  │
  │ MINTED ├──>│ PRESENTED ├──>│ VERIFIED ├──>│ LOCKED ├──>│ SETTLING │  │
  └───┬──┬─┘   └─────┬───┬─┘   └────┬───┬─┘   └──┬──┬──┘   └────┬──┬──┘  │
      │  │          │   │         │   │        │  │          │  │     │
      │  │          │   │         │   │        │  │     ┌────▼──┐│     │
      │  │          │   │         │   │        │  │     │SETTLED├┘     │
      │  │          │   │         │   │        │  │     └──┬──┬─┘      │
      │  │          │   │         │   │        │  │        │  │        │
      │  │          │   │         │   │        │  │   ┌────▼──┐│        │
      │  │          │   │         │   │        │  │   │FULFLLD││        │
      │  │          │   │         │   │        │  │   └───────┘│        │
      │  │          │   │         │   │        │  │            │        │
      │  └──────────┘───┘─────────┘   │        │  │       ┌────▼───┐   │
      │        ▼                      │        │  │       │REFUNDED│   │
      │   ┌─────────┐                │        │  │       └────────┘   │
      │   │ REVOKED │                │        │  │                    │
      │   └─────────┘                │        │  │                    │
      │                              │        │  │                    │
      └──────────────────────────────┘        │  │                    │
      ▼                                       │  │                    │
  ┌─────────┐                                 │  │                    │
  │ EXPIRED │  <──────────────────────────────┘  │                    │
  └─────────┘                                    │                    │
                                                 │                    │
              ┌──────────────────────────────────┘                    │
              ▼                                                       │
         ┌─────────┐                                                  │
         │ FAILED  │                                                  │
         └─────────┘                                                  │

                    │              ESCROW PATH                         │
                    │                                                  │
                    │   VERIFIED ──> ESCROWED ──> CONFIRMING           │
                    │                   │             │                │
                    │                   ▼             ▼                │
                    │             AUTO_RELEASING   RELEASED            │
                    │                   │                              │
                    │                   ▼                              │
                    │               RELEASED                          │
                    │                                                  │
                    │              DISPUTE PATH                        │
                    │                                                  │
                    │   ESCROWED/CONFIRMING ──> DISPUTING              │
                    │                              │                   │
                    │                              ▼                   │
                    │                         ARBITRATING              │
                    │                        /     |     \             │
                    │                       ▼      ▼      ▼            │
                    │              RESOLVED_  RESOLVED_  RESOLVED_     │
                    │               REFUND    RELEASE     SPLIT       │
                    │                                                  │
                    │              SPECIAL                             │
                    │                                                  │
                    │   SETTLING ──> PARTIAL_SETTLED                   │
                    │   LOCKED ──> UNLOCKING ──> CANCELLED            │
                    │   MINTED ──> CANCELLED                          │
                    └──────────────────────────────────────────────────┘

State Categories

CategoryStatesDescription
Happy pathminted, presented, verified, locked, settling, settled, fulfilledStandard payment completion
Escrow pathescrowed, confirming, auto_releasing, releasedFunds held pending delivery
Dispute pathdisputing, arbitrating, resolved_refund, resolved_release, resolved_splitConflict resolution
Terminalrevoked, expired, failed, refunded, cancelledFinal states, no further transitions
Specialpartial_settled, unlockingEdge cases for partial settlement and fund release

Key Transition Rules

  • Revocation is only possible from minted or presented (before the merchant has verified)
  • Expiration can happen from any pre-settlement state (minted, presented, verified, locked)
  • Refund is only possible after settlement (settled or fulfilled)
  • Disputes can be raised from escrowed or confirming
  • Arbitration resolves to one of three outcomes: full refund, full release, or split

Cell Claim Algorithm

The CellClaimAlgorithm is the hot path for every agent payment. It uses PostgreSQL row-level locking to prevent concurrent claims on the same cells:

1. Query AVAILABLE cells by currency, ordered by value DESC
2. Lock rows with FOR UPDATE SKIP LOCKED (non-blocking)
3. Greedily select cells until the requested amount is covered
4. If the last cell exceeds the remaining amount, split it
5. Mark selected cells as CLAIMED with the mandate_id
6. Return the claimed cells

Why Largest-First Selection?

The algorithm intentionally selects the largest cells first so that fewer cells are consumed per payment. This reduces row-lock contention under high-throughput agent workloads.

Cell Splitting

When the last selected cell exceeds the remaining amount needed, it is split:

  1. The claimed portion is reduced to the exact remaining amount
  2. A new AVAILABLE cell is created with the overshoot value
  3. Both operations happen within the same database transaction
-- Lock available cells, largest first, skip already-locked rows
SELECT cell_id, commitment_id, value, currency, status
FROM funding_cells
WHERE status = 'available' AND currency = $1
ORDER BY value DESC
FOR UPDATE SKIP LOCKED

The SKIP LOCKED clause is essential -- it means concurrent agents competing for cells will never block each other. If an agent locks a cell, other agents simply skip it and claim the next available one.

Additional Operations

OperationDescription
release_cellsReturn claimed cells back to available (idempotent)
spend_cellsMark claimed cells as spent with the payment object ID
split_cellSplit an available cell into smaller cells (sum must equal original)
merge_cellsCombine multiple available cells into one (same commitment and currency)

PaymentObjectMinter

The PaymentObjectMinter is the only authorized path for creating payment objects. It implements a 3-port architecture with Protocol-based dependency injection:

┌─────────────────────────────┐
│     PaymentObjectMinter     │
│                             │
│  ┌───────────────────────┐  │
│  │ SpendingMandateLookup │  │  Port 1: Fetch and validate the mandate
│  │       Port            │  │
│  └───────────┬───────────┘  │
│              │              │
│  ┌───────────▼───────────┐  │
│  │    CellClaimPort      │  │  Port 2: Claim funding cells from budget
│  └───────────┬───────────┘  │
│              │              │
│  ┌───────────▼───────────┐  │
│  │    SigningPort         │  │  Port 3: Sign the payment object
│  └───────────────────────┘  │
│                             │
└─────────────────────────────┘

Minting Pipeline

The mint() method executes six steps:

  1. Lookup mandate -- Fetch the SpendingMandate by ID via the lookup port
  2. Validate mandate -- Check status (must be active), expiration, valid-from date, amount limits (per-tx, daily, total budget remaining), and merchant scope (allowed/blocked lists with wildcard support)
  3. Claim funding cells -- Reserve budget cells covering the requested amount
  4. Compute session hash -- SHA-256 of mandate_id + merchant + amount + timestamp + nonce for replay protection
  5. Sign -- Build a canonical JSON payload (sorted keys, no whitespace) and sign it via the signing port. If the privacy tier is hybrid or full_zk, generate a ZKP proof via sardis_zkp
  6. Return minted PaymentObject -- Ready for presentation to a merchant

Error Codes

Error CodeHTTP StatusDescription
MANDATE_NOT_FOUND404No mandate with that ID exists
MANDATE_NOT_ACTIVE409Mandate status is not active
MANDATE_EXPIRED410Mandate has passed its expiration
MANDATE_NOT_YET_VALID422Mandate's valid_from is in the future
INVALID_AMOUNT422Amount is zero or negative
PER_TX_LIMIT_EXCEEDED422Amount exceeds per-transaction limit
TOTAL_BUDGET_EXCEEDED422Amount exceeds remaining mandate budget
MERCHANT_BLOCKED422Merchant is on the mandate's blocked list
MERCHANT_NOT_ALLOWED422Merchant is not on the mandate's allowed list
INSUFFICIENT_BUDGET422Not enough funding cells to cover the amount
SIGNING_FAILED500Cryptographic signing failed

Merchant Countersignature

Merchants sign payment verification data to prevent agents from fabricating consumption data. The system supports a 3-tier verification fallback:

TierMethodUse Case
PrimaryEIP-712 typed dataProduction -- structured, chain-aware signing
FallbackEIP-191 personal_signTransitional -- one release deprecation cycle
DevHMAC-SHA256Development/testing -- shared secret signing

EIP-712 Typed Data Structure

{
  "types": {
    "EIP712Domain": [
      { "name": "name", "type": "string" },
      { "name": "version", "type": "string" }
    ],
    "PaymentVerification": [
      { "name": "objectId", "type": "string" },
      { "name": "objectHash", "type": "string" },
      { "name": "merchantId", "type": "string" },
      { "name": "amount", "type": "string" },
      { "name": "currency", "type": "string" }
    ]
  },
  "primaryType": "PaymentVerification",
  "domain": { "name": "Sardis", "version": "1" }
}

The merchant signs the typed data with their settlement wallet private key. The API recovers the signer address and compares it against the merchant's registered settlement_address.

Usage Report Countersignature

For metered/usage-based billing, merchants also countersign usage reports:

verifier = MerchantCountersignature(shared_secret="merchant_hmac_key")
result = verifier.verify(
    meter_id="meter_abc",
    usage_delta=Decimal("150"),
    timestamp=1711200000,
    signature="a1b2c3...",
)

Timestamp drift is validated to prevent replay attacks (maximum 5 minutes clock skew).

API Reference

All endpoints require authentication via Bearer token. Payment objects are scoped to the authenticated principal's organization.

Mint a Payment Object

POST /api/v2/payment-objects/mint

Request body:

{
  "mandate_id": "mandate_abc123",
  "merchant_id": "merchant_openai",
  "amount": "49.99",
  "currency": "USDC",
  "privacy_tier": "transparent",
  "memo": "GPT-4 API usage for March",
  "expires_in_seconds": 3600,
  "metadata": { "invoice_id": "inv_001" }
}

Response (201 Created):

{
  "object_id": "po_a1b2c3d4e5f6g7h8",
  "mandate_id": "mandate_abc123",
  "merchant_id": "merchant_openai",
  "exact_amount": "49.99",
  "currency": "USDC",
  "status": "minted",
  "privacy_tier": "transparent",
  "session_hash": "e3b0c44298fc1c149afbf4c8996fb924...",
  "cell_ids": ["cell_001a2b3c4d5e", "cell_006f7g8h9i0j"],
  "signature_chain": ["sha256_abc123..."],
  "object_hash": "9f86d081884c7d659a2feaa0c55ad015...",
  "expires_at": "2026-03-25T14:00:00+00:00",
  "created_at": "2026-03-25T13:00:00+00:00",
  "metadata": { "invoice_id": "inv_001" }
}

Get a Payment Object

GET /api/v2/payment-objects/{object_id}

Returns the full payment object including current status, cell IDs, and signature chain.

List Payment Objects

GET /api/v2/payment-objects?mandate_id=...&merchant_id=...&status=...&offset=0&limit=50

Supports filtering by mandate_id, merchant_id, and status. Results are ordered by created_at DESC with pagination.

Present to Merchant

POST /api/v2/payment-objects/{object_id}/present

Transitions minted to presented. Validates merchant ID match and checks expiration.

Request body:

{
  "merchant_id": "merchant_openai",
  "merchant_signature": null
}

Verify (Merchant-Side)

POST /api/v2/payment-objects/{object_id}/verify

Transitions presented to verified. Requires a valid merchant signature over the object hash using EIP-712 typed data, EIP-191, or HMAC fallback.

Request body:

{
  "merchant_id": "merchant_openai",
  "merchant_signature": "0xa1b2c3..."
}

Code Examples

Python SDK

from sardis import Sardis

sardis = Sardis(api_key="sk_live_...")

# Mint a payment object from a spending mandate
po = await sardis.payment_objects.mint(
    mandate_id="mandate_abc123",
    merchant_id="merchant_openai",
    amount="49.99",
    currency="USDC",
    expires_in_seconds=3600,
    metadata={"invoice_id": "inv_001"},
)

print(f"Minted: {po.object_id}, status={po.status}")
# Minted: po_a1b2c3d4e5f6g7h8, status=minted

# Check status
obj = await sardis.payment_objects.get(po.object_id)
print(f"Status: {obj.status}, cells: {obj.cell_ids}")

# List all payment objects for a mandate
objects = await sardis.payment_objects.list(
    mandate_id="mandate_abc123",
    status="minted",
)
for po in objects:
    print(f"  {po.object_id}: {po.exact_amount} {po.currency}")

TypeScript SDK

import { Sardis } from "@sardis/sdk";

const sardis = new Sardis({ apiKey: "sk_live_..." });

// Mint a payment object
const po = await sardis.paymentObjects.mint({
  mandateId: "mandate_abc123",
  merchantId: "merchant_openai",
  amount: "49.99",
  currency: "USDC",
  expiresInSeconds: 3600,
  metadata: { invoiceId: "inv_001" },
});

console.log(`Minted: ${po.objectId}, status=${po.status}`);

// Present to merchant
const presented = await sardis.paymentObjects.present(po.objectId, {
  merchantId: "merchant_openai",
});

// Merchant verifies (merchant-side)
const verified = await sardis.paymentObjects.verify(po.objectId, {
  merchantId: "merchant_openai",
  merchantSignature: await wallet.signTypedData(typedData),
});

Direct API (cURL)

# Mint a payment object
curl -X POST https://api.sardis.sh/api/v2/payment-objects/mint \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "mandate_id": "mandate_abc123",
    "merchant_id": "merchant_openai",
    "amount": "49.99",
    "currency": "USDC"
  }'

# Get payment object status
curl https://api.sardis.sh/api/v2/payment-objects/po_a1b2c3d4e5f6g7h8 \
  -H "Authorization: Bearer sk_live_..."

Architecture Diagram

The full flow from spending mandate to settlement:

┌─────────────────────────────────────────────────────────────────┐
│                        AGENT REQUEST                            │
│                                                                 │
│  "Pay $49.99 to OpenAI for GPT-4 API usage"                    │
└───────────────────────────┬─────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────┐
│                   SPENDING MANDATE LOOKUP                       │
│                                                                 │
│  mandate_abc123: active, $500/day limit, OpenAI allowed         │
│  Remaining budget: $342.50                                      │
│  Per-tx limit: $100                                             │
└───────────────────────────┬─────────────────────────────────────┘
                            │ validated

┌─────────────────────────────────────────────────────────────────┐
│                    CELL CLAIM (UTXO)                            │
│                                                                 │
│  Available cells (largest first):                               │
│    cell_001: $30.00  ──> CLAIMED                                │
│    cell_002: $25.00  ──> CLAIMED ($19.99) + SPLIT ($5.01 avail) │
│                                                                 │
│  SELECT ... FOR UPDATE SKIP LOCKED                              │
│  Total claimed: $49.99                                          │
└───────────────────────────┬─────────────────────────────────────┘
                            │ cells claimed

┌─────────────────────────────────────────────────────────────────┐
│               SESSION HASH + SIGNING                            │
│                                                                 │
│  session_hash = SHA-256(mandate + merchant + amount + ts + nonce│
│  signature = sign(canonical_json_payload)                       │
│  [Optional: ZKP proof for hybrid/full_zk privacy]               │
└───────────────────────────┬─────────────────────────────────────┘
                            │ signed

┌─────────────────────────────────────────────────────────────────┐
│                 PAYMENT OBJECT (MINTED)                         │
│                                                                 │
│  po_a1b2c3d4e5f6g7h8                                           │
│  Amount: $49.99 USDC │ Merchant: merchant_openai                │
│  Cells: [cell_001, cell_002]                                    │
│  Session hash: e3b0c44... │ Expires: +1h                        │
└───────────────────────────┬─────────────────────────────────────┘

              ┌─────────────┼──────────────┐
              ▼             ▼              ▼
         PRESENTED     PRESENTED      (expired/revoked)
              │             │
              ▼             ▼
          VERIFIED      VERIFIED
              │             │
              ▼             ▼
           LOCKED   ──>  ESCROWED  ──>  DISPUTING
              │                            │
              ▼                            ▼
          SETTLING                    ARBITRATING
              │                        /   |   \
              ▼                       ▼    ▼    ▼
           SETTLED              REFUND RELEASE SPLIT


          FULFILLED

Key Invariants

  1. One-time use -- one_time_use is always True. A payment object can never be reused.
  2. Signature chain -- Must contain signatures from the principal, issuer, and agent.
  3. Session hash uniqueness -- No two payment objects can share the same session hash (replay protection via random nonce + timestamp).
  4. Cell accounting -- The sum of claimed cell values always equals exact_amount (enforced by the split algorithm).
  5. State machine enforcement -- Every transition is validated against VALID_TRANSITIONS. Invalid transitions raise ValueError.
  6. Immutable identity -- Once minted, the object_id, mandate_id, merchant_id, exact_amount, cell_ids, and session_hash cannot change.
  • Spending Mandates -- The authorization primitive that controls what agents can spend
  • Payments -- Payment execution pipeline (policy checks, compliance, chain execution)
  • Policy Engine -- Time-based policies, merchant categories, and runtime guardrails
  • Wallets -- Safe Smart Account infrastructure for agent wallets
  • Security -- Cryptographic signing, MPC wallets, and audit trails