HyoNovaDocumentation

HyoNova Balance and direct deposits

The HyoNova Balance ledger and its invariants, the direct-deposit contract and Binance adapter, payment intents, fingerprints, precision and idempotency.

Requires setupBuilt, but works only after a supporting service is configured for your workspace.ConceptFor: Developers, IT & securityLast updated: Introduced:
On this page

Adding funds needs HyoNova’s Binance Direct Deposit to be connected; the balance cannot be withdrawn or transferred.

HyoNova Balance is platform billing: a company's money held by HyoNova. It never touches company finance (the company schema) and never mixes with capacity credits (credit_lots, credit_operations), which stay the ledger of service units.

The ledger (migration 068)#

TableRole
billing_wallet_accountsOne customer account per organization and currency, plus system counter-accounts funding_crypto, funding_operator, invoice_settlement, refund_source. balance_micros is a projection of the entries.
billing_wallet_journalsOne business operation: kind, direction, amount, idempotency key (unique per organization), source type and id, links to invoice / payment / refund / deposit, actor and reason, conversion policy, safe metadata.
billing_wallet_entriesAt least two per journal, summing to zero; balance_after_micros per entry.
billing_wallet_settingsAutomatic renewal preference (off by default).

Database-enforced invariants: journals and entries are append-only (trigger + revoked UPDATE/DELETE); a customer balance cannot go below zero (CHECK); the balance changes only inside the entry trigger (a direct UPDATE … balance_micros is refused); every journal balances and names its customer amount (deferred constraint trigger); only the platform plane writes; operator movements carry actor and reason; a deposit funds at most one journal, a payment row has one journal, a refund returns once (unique indexes). Row security confines companies to their own rows.

packages/database/billing/balance.ts (postJournal) is the only writer. It locks the customer account row, replays an existing key (and refuses the same key for a different operation), refuses an overdraft before writing, and inserts the journal and its two entries.

Journal kinds: CRYPTO_TOPUP, LATE_CRYPTO_TOPUP, MANUAL_DEPOSIT_ASSIGNMENT, INVOICE_PAYMENT, REFUND_CREDIT, OPERATOR_CREDIT, OPERATOR_DEBIT.

Precision#

Wallet value is counted in micros: 1 USD = 1,000,000. Prices are cents (= 10,000 micros). USDT has 6 decimals, so atomic units and micros coincide under the declared policy USDT_USD_PARITY (1:1), stored on every journal with the asset, network, atomic amount and time. All arithmetic is bigint; provider amounts are parsed from their decimal strings (parseAtomic rejects signs, exponents and digits beyond the asset's precision). The balance pays invoices in whole cents, so sub-cent micros stay in the balance.

Revenue#

A top-up is a liability. Only an allocation creates a payment record (method HyoNova Balance), which is what revenue metrics count. A $100 top-up followed by a $20 invoice is $20 of revenue and $80 of liability.

Allocation layer#

balance-payments.ts → applyBalanceToInvoice writes, in the caller's transaction: the payment row, the INVOICE_PAYMENT journal, applyInvoicePayment (partial allowed), the timeline and notice, and on full payment the checkout completion and activateFromInvoice or grantPurchase. payWithBalance resolves what is bought with the same resolver as checkout (resolveCheckoutTarget), issues the native invoice for new purchases, and pays full (all or nothing) or partial (explicit consent; optionally a crypto intent for exactly the rest). renewFromBalance pays renewal invoices only when the balance covers them. Refunds of balance payments go back through REFUND_CREDIT.

Direct-deposit provider contract#

A provider declaring the capability direct_deposit implements two optional adapter methods (packages/payments/core/provider.ts):

  • depositTarget() — asset, network and display name, address, tag, asset decimals, window (≤ 15 min), quarantine days, top-up bounds, polling cadence, account-check interval; from settings only.
  • listDeposits({ startTime, endTime, offset, limit }) — account deposit history classified as pending | confirming | final | rejected, amounts as the provider's decimal strings.

health() returns structured checks. Manifests may set enableRequiresHealthyTest: enabling runs the test on the configuration being saved and refuses any result other than connected. New diagnoses: permission_unsafe, configuration_mismatch.

Binance adapter#

packages/payments/providers/binance-deposit calls GET /api/v3/time, /sapi/v1/account/apiRestrictions, /sapi/v1/capital/config/getall, /sapi/v1/capital/deposit/address and /sapi/v1/capital/deposit/hisrec on api.binance.com only, HMAC-SHA256-signed, recvWindow 10 s, with the server-time offset measured and resynchronized on error -1021. Rate limits (429/418) are not retried by the HTTP layer; Retry-After is kept for the worker's back-off. Binance status 1 is final; 0, 6 and 8 are not final; 2 and 7 are rejected.

Payment intents and fingerprints (migration 069)#

crypto_payment_intents stores purpose (wallet_topup, invoice_payment, purchase_payment), target, base and expected atomic amounts, the requested wallet value, status, received/credited/applied amounts and expires_at (database CHECK: at most 15 minutes after creation), grace_until (+10 minutes for API delay) and quarantine_until.

The fingerprint is 1…9999 atomic units added to the base (base amounts from cents always end in four zeros). crypto_fingerprints has one row per (instance, asset, network, address, fingerprint): reserving inserts or takes over only a slot whose reserved_until has passed — the primary key is the guarantee. When no slot is free, creation is refused (CRYPTO_CAPACITY) and an alert is raised.

Sync, matching, finalization#

The worker (deposits.ts → directDepositWork, called every worker tick) leases each instance, re-verifies the account periodically, and reads the history from cursor − 30 min (never past a non-final deposit) in pages of 1,000. Errors set a back-off (Retry-After for rate limits, exponential for 5xx, 5 minutes for credentials) and raise alerts after repetition. Already-decided records are only counted as seen.

processDeposit (one transaction, row-locked, UNIQUE(provider_instance_id, external_deposit_id)): target check → amount parse → candidates by fingerprint whose reservation covered the deposit time → one, none (unmatched) or several (review) → duplicate transaction check → not final: mark detected → final: CRYPTO_TOPUP (or LATE_CRYPTO_TOPUP after grace_until or a cancellation), then for an on-time invoice payment apply floor(credited) − applied cents up to the amount due.

Idempotency summary#

Journal key per organization · one journal per deposit/payment/refund · deposit unique per provider id · intent key per organization (topup:<key>, checkout:<id>) · fingerprint slot primary key · allocation key invoice-payment:<source key> · notices deduplicated per event.

Adding another direct-deposit provider#

Write a module with the direct_deposit capability, depositTarget, listDeposits and a health that verifies read-only access and the address, set enableRequiresHealthyTest, register it in providers/index.ts and seed a disabled instance. The core, schema and screens need no change; asset support is the conversion policy in wallet-money.ts.

Status#

FeatureStatusNotes
HyoNova BalanceRequires setupBuilt, but works only after a supporting service is configured for your workspace.A money balance with HyoNova: add funds with USDT on TRON (TRC20) and pay plans, invoices and products from it, in full or with the rest by crypto.
Adding funds needs HyoNova’s Binance Direct Deposit to be connected; the balance cannot be withdrawn or transferred.
Binance Direct Deposit and HyoNova BalancesRequires setupBuilt, but works only after a supporting service is configured for your workspace.Read-only Binance deposit sync with exact-amount matching, crypto deposit review and assignment, balance adjustments, reconciliation and alerts.
↑↓ to navigate · Enter to open · Esc to close