reference

Unified Payment Intents

docs/reference/unified-intents.mdupdated: 2026-08-09

Unified Payment Intents

Overview

Daxchain uses a Unified Intent & Transaction Model to handle all forms of cryptocurrency reception (Invoices, API Checkouts, and Pay-To links). This architecture separates the "Intent" (what we expect to happen) from the "Transaction" (what actually happened on-chain).

The payment_intents Table

The payment_intents table is the single source of truth for all checkout-style requests. It replaces the legacy invoices, api_orders, and payment_orders tables.

Key Fields

  • kind: Identifies the source domain — invoice, order, or pay_to. (The enum value is order, not api_order.)
  • external_id: The user-visible reference (e.g., INV-2024-001).
  • deposit_address: The unique on-chain address (Fireblocks sub-vault) allocated for this specific intent.
  • fireblocks_vault_id: The allocated sub-vault id (0039_payment_intents_fireblocks_vault_id).
  • crypto_amount_minor: The gross amount the payer is expected to send. Nullable for pay_to — amountless pay-to intents are supported (0041_payment_intents_pay_to_amountless).
  • status: The unified state of the intent.

Status State Machine

  1. draft: (Invoices only) Prepared but not yet "opened" or visible to the payer.
  2. pending: Awaiting payment. The deposit address is active.
  3. detected: A transaction has been seen on-chain but hasn't reached the required confirmation threshold.
  4. confirmed: Exact payment received (within 5 BPS tolerance). Funds credited to merchant.
  5. underpaid: Payment received, but less than expected. Actual amount credited; merchant notified for manual reconciliation.
  6. overpaid: Payment received, more than expected. Actual amount credited; merchant notified.
  7. due: Invoice past its due date without full settlement.
  8. expired: Intent TTL reached before payment was confirmed.
  9. void: Manually cancelled by the merchant or admin.

The full payment_intent_status enum is: draft, pending, detected, confirmed, underpaid, overpaid, due, expired, void.

Reconciliation & Monitoring

Daxchain runs a generic Deposit Indexer that operates across all intents.

  1. Inbound Event: A provider callback lands on /api/webhooks/fireblocks, is persisted to the durable provider_webhook_events inbox, and is processed off the request path by the provider.webhook.process pg-boss queue, which inserts into observed_deposits. Local dev enqueues the same shape via dev-only.provider.webhook.dispatch, so emulation exercises the production ingestion path.
  2. Lookup: The system finds the payment_intents row where deposit_address == observed.to_address.
  3. Amount Match:
    • Fixed-amount (invoice, order): Compares observed.amount against intent.crypto_amount_minor.
    • Open-ended (pay_to): Always transitions to confirmed.
  4. Accounting:
    • A row is written to transactions (type: deposit), linked via intent_id.
    • ledger_entries are written to credit the merchant's balance.
  5. Webhooks: A webhook_event is enqueued, linked via intent_id, alerting the merchant's system.

Generic Metadata

Domain-specific data is stored in the metadata JSONB column:

  • Invoice: clientEmail, fromSnapshot, toSnapshot.
  • API Order: callbackUrl, returnUrl.
  • Pay-To: payerEmail, payerName.

This allows the core logic (searching, pagination, status tracking) to remain generic while supporting diverse feature sets.