Reconciliation в crypto API: как сверять балансы, события и статусы без ручной паники

Когда crypto API начинает жить на реальном трафике, проблема почти никогда не звучит как «всё упало».

Гораздо чаще система формально работает, но данные начинают медленно расходиться:

  • callback от провайдера пришёл, но не изменил статус invoice
  • депозит обнаружен on-chain, но не зачислен клиенту
  • withdrawal помечен как completed, хотя broadcast не прошёл
  • баланс hot wallet не совпадает с внутренним ledger
  • провайдер и ваша база показывают разное состояние одной и той же операции

Именно для этого нужен reconciliation — не как разовая аварийная процедура, а как постоянный технический контур доверия к данным.

Что такое reconciliation на практике

Reconciliation — это регулярная сверка между несколькими источниками правды, которая помогает находить расхождения до того, как их заметит клиент или finance-команда.

В платежной или crypto-интеграции обычно есть минимум четыре слоя, которые нужно периодически сравнивать между собой:

  1. внешний источник — blockchain, провайдер платежей, wallet RPC, custodial API
  2. входящие события — callbacks, webhooks, polling results
  3. внутренняя модель — invoice, deposit, withdrawal, payout, ledger entries
  4. исходящие эффекты — уведомления клиенту, callback во внешнюю систему, изменение баланса

Пока эти слои согласованы, система выглядит надёжной. Как только они начинают расходиться, support и операции быстро переходят в режим ручного расследования.

Почему обычных логов и retry недостаточно

Многие команды какое-то время живут на связке:

  • есть retries
  • есть логи
  • есть алерты по 500-кам
  • есть дашборд по latency

Этого недостаточно.

Проблема reconciliation в том, что большая часть дорогих ошибок не выглядит как явный сбой. Система может не падать вообще.

Например:

  • callback обработался без exception
  • worker завершился успешно
  • API вернул 200 OK
  • запись в БД есть

Но внутренняя сущность так и осталась в pending, хотя провайдер уже давно считает её confirmed.

Это не классический outage. Это рассинхрон, который потом бьёт по деньгам, доверию и ручной операционке.

Где reconciliation нужен в первую очередь

Если идти практично, в Sassoft-подобном crypto backend я бы всегда начинал с четырёх зон.

1. Deposits и invoice statuses

Нужно регулярно отвечать на вопросы:

  • все ли on-chain поступления связались с нужным invoice или wallet address
  • все ли подтверждённые депозиты реально зачислены во внутреннем ledger
  • нет ли зависших объектов в pending или processing
  • не было ли повторного crediting по одной и той же транзакции

Типичные симптомы проблем:

  • txid есть, а internal credit нет
  • invoice уже не должен быть pending, но статус не сменился
  • один deposit попал в обработку более одного раза

2. Withdrawals и payouts

Здесь reconciliation особенно важен, потому что ошибка часто дороже.

Нужно сверять:

  • есть ли внутренний withdrawal request
  • был ли реально вызван broadcast
  • какой txid присвоен операции
  • совпадает ли текущий on-chain статус с внутренним статусом payout
  • не ушёл ли payout в completed раньше подтверждения факта отправки

Самые неприятные кейсы обычно такие:

  • withdrawal помечен завершённым без успешного broadcast
  • broadcast прошёл, но txid не сохранился
  • повторный retry создал вторую отправку

3. Wallet balances

Внутренний ledger и фактический баланс кошелька не должны жить независимой жизнью.

Нужно регулярно сверять:

  • доступный баланс hot wallet
  • reserved amount под pending payouts
  • фактический on-chain / custodial balance
  • сумму внутренних ledger entries

Если здесь нет автоматической сверки, операционная команда обычно слишком поздно узнаёт о проблеме — когда баланс уже не бьётся и нужно вручную распутывать цепочку событий.

4. Delivery callbacks наружу

Очень частая недооценённая зона — внешние уведомления клиентам.

Даже если внутренний статус корректный, для клиента система всё ещё выглядит сломанной, если callback в его сторону:

  • не ушёл
  • ушёл с задержкой
  • ушёл со старым статусом
  • ушёл несколько раз без понятной идемпотентности

Поэтому reconciliation должен видеть не только внутреннее состояние, но и последнюю успешную доставку наружу.

Как строить reconciliation job без лишней магии

Нормальная схема обычно состоит из трёх шагов.

Шаг 1. Сформировать список сущностей для проверки

Не нужно каждую минуту сверять всё подряд. Практичнее брать:

  • объекты в нефинальном состоянии
  • недавно изменённые сущности
  • сущности с ошибками или retry history
  • выборочную проверку финальных записей
  • отдельные диапазоны по времени для backfill

Примеры кандидатов:

  • invoices в pending старше 5 минут
  • withdrawals в processing старше 2 минут
  • deposits без credit entry
  • callbacks с processed=false

Шаг 2. Для каждой сущности вычислить expected vs actual

Главный вопрос reconciliation не «что у нас в таблице?», а:

  • expected — каким объект должен быть по бизнес-логике
  • actual — что сейчас показывают все источники

Например для withdrawal:

  • expected: если есть успешный broadcast, статус не должен быть new
  • actual: в БД completed, а txid отсутствует

Или для deposit:

  • expected: при N confirmations должен существовать credit record
  • actual: confirmations есть, credit record нет

Только на таком сравнении появляются реальные сигналы, а не просто очередной набор строк в таблице.

Шаг 3. Классифицировать mismatch и запускать правильное действие

Все расхождения не равны по важности. Их нужно делить хотя бы на три класса.

A. Auto-fixable

Сюда попадают случаи, где система может безопасно поправить себя сама:

  • повторно запросить статус у провайдера
  • заново отправить внутреннюю job
  • пересчитать derived status
  • повторно доставить внешний callback
  • добить пропущенный ledger update, если это безопасно и идемпотентно

B. Requires review

Это расхождения, где автоматический фикс уже рискован:

  • внутренний и внешний статусы конфликтуют
  • есть подозрение на двойную обработку
  • payout выглядит завершённым, но chain-данные не подтверждают это
  • баланс расходится больше допустимого порога

C. Critical integrity issue

Это то, что должно сразу поднимать инцидент:

  • duplicate credit
  • отрицательный доступный баланс
  • повторный withdrawal execution
  • статус completed без подтверждённого факта исполнения

Нельзя складывать все эти случаи в один общий «reconciliation failed». Иначе команда утонет в шуме.

Какие поля и ключи нужны, чтобы reconciliation вообще работал

Если в модели нет нормальных ключей связи, сверка быстро превращается в боль.

Минимальный набор, который обычно нужен:

  • invoice_id
  • payment_id
  • withdrawal_id
  • wallet_id
  • provider
  • external_event_id
  • external_reference
  • txid
  • address
  • asset
  • network
  • idempotency_key
  • last_known_status
  • last_reconciled_at

Если часть этих связок отсутствует или хранится непоследовательно, сначала стоит чинить модель данных, а уже потом усложнять monitoring.

Какая периодичность обычно разумна

Нормальная практика — запускать несколько уровней reconciliation, а не один гигантский cron.

Частый reconciliation

Каждые 1–5 минут:

  • зависшие pending invoices
  • processing withdrawals
  • неотправленные callbacks
  • отсутствие credit после on-chain detection

Периодический расширенный reconciliation

Каждые 15–60 минут:

  • wallet balances
  • провайдерские статусы за последний диапазон
  • выборочная сверка финальных операций
  • очереди failed / dead-letter jobs

Ночной глубокий reconciliation

Раз в сутки:

  • полная сверка диапазона за день
  • поиск orphaned records
  • агрегатная проверка ledger vs wallet balance
  • отчёт по mismatch trends

Такой подход даёт раннее обнаружение и не перегружает систему постоянными тяжёлыми запросами.

Какие метрики действительно полезны

Для reconciliation нужны не только логи задач, но и отдельные сигналы.

Практичный минимум:

  • reconciliation_runs_total{job,status}
  • reconciliation_duration_seconds{job}
  • reconciliation_checked_entities_total{entity}
  • reconciliation_mismatches_total{entity,type,severity}
  • reconciliation_auto_fixed_total{entity,type}
  • reconciliation_review_required_total{entity,type}
  • reconciliation_oldest_unresolved_mismatch_age_seconds

Особенно полезны две вещи:

  1. доля mismatch относительно общего числа проверок
  2. возраст самого старого нерешённого mismatch

Именно возраст нерешённой проблемы часто лучше показывает реальный operational debt, чем абсолютное количество ошибок.

Что команды чаще всего делают неправильно

На практике я чаще всего вижу пять типичных ошибок.

Reconciliation запускается только после инцидента

Тогда это уже не системный механизм, а ручная аварийная процедура.

Нет разделения между data drift и transient failure

Не каждый mismatch означает катастрофу. Но если это не классифицировать, алерты становятся бесполезными.

Автофикс выполняется без идемпотентности

Если remediation job сама может породить дубли или повторный payout, то reconciliation превращается во второй источник проблем.

Сверка не видит внешнюю доставку

Команда проверяет внутренние статусы, но не видит, дошло ли изменение до клиента.

Нет истории mismatch и трендов

Разовые расхождения ещё можно пережить. Гораздо важнее понимать, какой тип расхождений начал расти неделя к неделе.

Практический стартовый чеклист

Если нужно внедрить reconciliation без полугодового проекта, я бы шёл так:

  1. выбрать 2–3 самые дорогие сущности: обычно deposit, withdrawal, invoice
  2. описать для каждой expected state machine
  3. определить внешний источник truth для каждой стадии
  4. собрать лёгкую reconciliation job по недавно изменённым сущностям
  5. завести mismatch table или отдельный журнал расхождений
  6. добавить классификацию: auto-fix / review / critical
  7. повесить алерт на critical mismatches и возраст oldest unresolved
  8. только потом расширять coverage на balances, callbacks и длинные backfill-проверки

Такой порядок даёт быстрый реальный эффект, а не бесконечную архитектурную подготовку.

Вывод

В crypto API надёжность определяется не только тем, что система умеет принимать события, но и тем, что она умеет регулярно доказывать самой себе корректность состояния.

Reconciliation — это как раз такой механизм.

Когда у вас есть регулярная сверка депозитов, выплат, балансов и callback delivery, рассинхроны перестают накапливаться тихо. Команда видит drift раньше клиента, раньше support и раньше ручной паники.

А для production-платформы это один из самых полезных слоёв зрелости — потому что доверие к деньгам и статусам нужно не декларировать, а постоянно перепроверять.