Когда crypto API начинает жить на реальном трафике, проблема почти никогда не звучит как «всё упало».
Гораздо чаще система формально работает, но данные начинают медленно расходиться:
- callback от провайдера пришёл, но не изменил статус invoice
- депозит обнаружен on-chain, но не зачислен клиенту
- withdrawal помечен как
completed, хотя broadcast не прошёл - баланс hot wallet не совпадает с внутренним ledger
- провайдер и ваша база показывают разное состояние одной и той же операции
Именно для этого нужен reconciliation — не как разовая аварийная процедура, а как постоянный технический контур доверия к данным.
Что такое reconciliation на практике
Reconciliation — это регулярная сверка между несколькими источниками правды, которая помогает находить расхождения до того, как их заметит клиент или finance-команда.
В платежной или crypto-интеграции обычно есть минимум четыре слоя, которые нужно периодически сравнивать между собой:
- внешний источник — blockchain, провайдер платежей, wallet RPC, custodial API
- входящие события — callbacks, webhooks, polling results
- внутренняя модель — invoice, deposit, withdrawal, payout, ledger entries
- исходящие эффекты — уведомления клиенту, 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: при
Nconfirmations должен существовать 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_idpayment_idwithdrawal_idwallet_idproviderexternal_event_idexternal_referencetxidaddressassetnetworkidempotency_keylast_known_statuslast_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
Особенно полезны две вещи:
- доля mismatch относительно общего числа проверок
- возраст самого старого нерешённого mismatch
Именно возраст нерешённой проблемы часто лучше показывает реальный operational debt, чем абсолютное количество ошибок.
Что команды чаще всего делают неправильно
На практике я чаще всего вижу пять типичных ошибок.
Reconciliation запускается только после инцидента
Тогда это уже не системный механизм, а ручная аварийная процедура.
Нет разделения между data drift и transient failure
Не каждый mismatch означает катастрофу. Но если это не классифицировать, алерты становятся бесполезными.
Автофикс выполняется без идемпотентности
Если remediation job сама может породить дубли или повторный payout, то reconciliation превращается во второй источник проблем.
Сверка не видит внешнюю доставку
Команда проверяет внутренние статусы, но не видит, дошло ли изменение до клиента.
Нет истории mismatch и трендов
Разовые расхождения ещё можно пережить. Гораздо важнее понимать, какой тип расхождений начал расти неделя к неделе.
Практический стартовый чеклист
Если нужно внедрить reconciliation без полугодового проекта, я бы шёл так:
- выбрать 2–3 самые дорогие сущности: обычно
deposit,withdrawal,invoice - описать для каждой expected state machine
- определить внешний источник truth для каждой стадии
- собрать лёгкую reconciliation job по недавно изменённым сущностям
- завести mismatch table или отдельный журнал расхождений
- добавить классификацию: auto-fix / review / critical
- повесить алерт на critical mismatches и возраст oldest unresolved
- только потом расширять coverage на balances, callbacks и длинные backfill-проверки
Такой порядок даёт быстрый реальный эффект, а не бесконечную архитектурную подготовку.
Вывод
В crypto API надёжность определяется не только тем, что система умеет принимать события, но и тем, что она умеет регулярно доказывать самой себе корректность состояния.
Reconciliation — это как раз такой механизм.
Когда у вас есть регулярная сверка депозитов, выплат, балансов и callback delivery, рассинхроны перестают накапливаться тихо. Команда видит drift раньше клиента, раньше support и раньше ручной паники.
А для production-платформы это один из самых полезных слоёв зрелости — потому что доверие к деньгам и статусам нужно не декларировать, а постоянно перепроверять.