У крипты нет аналога сохранённого номера карты — нет кнопки «списать автоматически», потому что регулярный платёж может произойти сам только двумя способами: процессор держит деньги на кастодии, либо клиент явно выдаёт on-chain allowance (а это работает только для токен-стандартов, которые его поддерживают, — не для BTC или XMR). Любая крипто-подписка, за которую ты когда-либо платил, незаметно построена на одном из этих двух механизмов. Этот гайд — про третий вариант: полностью non-custodial архитектуру link-per-cycle, которая автоматизирует создание payment request и сверку поверх API SwapZilla Pay, без постоянного разрешения на кошелёк клиента.
Почему крипта не умеет списывать деньги как карта
Сохранённая карта работает, потому что Visa и Mastercard построили рельсы специально под то, чтобы мерчант мог списывать деньги по расписанию, а карточная сеть выступает уровнем принуждения, если что-то идёт не так. У Bitcoin, Monero и большинства base-layer крипто-активов ничего аналогичного нет. Адрес кошелька — не токен разрешения: если ты один раз отправил BTC на адрес, это не даёт никому права списать с этого адреса ещё раз в следующем месяце.
Есть ровно два способа обойти этот разрыв, и оба меняют модель доверия:
- Custodial-биллинг. Процессор держит деньги клиента (или заранее пополненный баланс) и сам инициирует списание по твоему расписанию. Именно так под красивым брендингом работает большинство «крипто-подписок» — регулярность здесь запись в custodial-леджере, а не on-chain событие.
- On-chain allowance. Некоторые токен-стандарты — ERC-20 самый известный — поддерживают паттерн approve/transferFrom, где клиент выдаёт смарт-контракту постоянное разрешение списывать до установленного лимита. Это существует только для токенов, построенных с таким интерфейсом; аналога для BTC или XMR нет, и это значит, что клиент выдаёт именно постоянный allowance, а не подтверждает каждый платёж отдельно.
Любая крипто-подписка, за которую ты платил, незаметно построена на одном из двух механизмов: кастодиан держит твои деньги, либо смарт-контрактный allowance, который ты выдал один раз и забыл.
Ни один вариант не «плохой», но оба чем-то жертвуют ради удобства авто-списания. Кастодия означает, что третья сторона контролирует settlement и может его заморозить или задержать. Allowance означает, что клиент передаёт постоянное разрешение, которое живёт, пока не будет явно отозвано, — это реальная поверхность атаки, если контракт или сторона, держащая allowance, будут скомпрометированы. Стоит сказать прямо: SwapZilla Pay не реализует модель on-chain allowance; паттерн approve/transferFrom выше приведён как контраст, а не как то, что делает API.
Остаётся третий вариант, который большинство гайдов по биллингу пропускают: клиент отправляет отдельный платёж каждый цикл, но всё остальное вокруг этого автоматизировано так, что не ощущается ручным.
Non-custodial модель регулярных платежей: link-per-cycle вместо карты на файле
Замени «карту на файле» на «ссылку на цикл». Вместо того чтобы хранить способ оплаты и списывать с него, ты создаёшь новый payment request на каждый биллинг-период, и клиент оплачивает его так же, как разовый инвойс — одной on-chain транзакцией с кошелька, которым он владеет.
Это сохраняет все non-custodial свойства, описанные в гайде о приёме криптоплатежей без аккаунта мерчанта: ни одна третья сторона не держит settlement-баланс, деньги клиента идут напрямую на твой адрес через routing агрегатора, и между циклами нет постоянного разрешения ни в контракте, ни в леджере процессора.
Здесь важна точность: у SwapZilla Pay нет нативного объекта subscription. Нет endpoint’а «создать подписку», нет встроенного cadence, нет dunning-движка. Pay даёт тебе тот же payment-request API, что описан в /pay/docs и используется для разовых инвойсов — регулярное поведение ты строишь сам, в своём приложении, вызывая этот API по расписанию, которое контролируешь ты.
Это и есть trade-off по сравнению с custodial-продуктом подписок: больше интеграционной работы на старте в обмен на то, что деньги клиента никогда не оседают у тебя на балансе, и никакого постоянного on-chain разрешения не требуется.
Автоматизация создания payment request по расписанию через Pay API
Биллинг-цикл живёт в твоей базе, а не в Pay. Смоделируй его как простую запись на клиента: customer_id, cadence (monthly, weekly и так далее), amount, receive_asset и next_due_date. У Pay нет понятия подписки, так что всё это состояние — целиком твоя ответственность.
Scheduler — cron-задача, serverless-функция по таймеру, воркер очереди — обходит эту таблицу ежедневно и для каждой строки, где next_due_date попадает в твоё окно упреждения (обычно несколько дней), вызывает Pay API и создаёт новый payment request на сумму и актив получения этого цикла. Сохрани возвращённый id запроса в записи клиента сразу, ещё до отправки чего-либо клиенту — он понадобится для сверки webhook позже.
Как именно выдаётся доступ к API — формат ключа, аутентификация, rate limits — смотри в /pay/docs как в источнике истины, а не в пересказе здесь: детали provisioning могут меняться, и на момент интеграции нужно ориентироваться именно на документацию.
Минимальная запись cadence на практике выглядит так:
| Поле | Пример | Комментарий |
|---|---|---|
customer_id | cust_1284 | твой собственный идентификатор |
cadence | monthly | интервал, который следит scheduler |
amount | 49.00 USDT | фиксирована на цикл, если не версионируешь прайсинг |
next_due_date | 2026-08-08 | сдвигается только после сверки |
last_request_id | req_9f2a… | Pay-запрос, привязанный к текущему циклу |
Изменения цены делай явными — если поднял цену плана, обнови сохранённый amount у себя, и следующий сгенерированный запрос отразит это. Внутри уже созданного payment request нет живого прайсинг-хука.
Доставка ссылки на продление и разумное окно expiry
Как только запрос создан, отправляй ссылку с запасом времени до истечения — по email, push-уведомлением или и тем и другим. Ссылка на продление, которая истекает в день создания, почти не оставляет шанса клиенту с медленным кошельком, занятыми выходными или пропущенным уведомлением.
Короткое окно expiry всё равно правильный инстинкт: оно ограничивает, сколько времени должен держаться зафиксированный курс, и не даёт устаревшей неоплаченной ссылке висеть в системе и путать статистику. Большинство мерчантов приходят к окну в несколько дней, не недель — достаточно, чтобы клиент заметил и среагировал, но достаточно коротко, чтобы не гоняться за ссылкой из позапрошлого цикла.
Две привычки здесь помогают. Первая — отправляй ссылку за несколько дней до next_due_date, а не в саму дату платежа: это оставляет запас на неудачную первую попытку. Вторая — считай каждую ссылку на продление одноразовой: если она истекла неоплаченной, создавай новую, а не пытайся «оживить» старый запрос. У payment request нет действия re-open; свежий запрос со свежим expiry — правильный следующий шаг.
Сверка продлений через HMAC-верифицированные webhook
Это шаг, который превращает link-per-cycle flow из ручной гонки за инвойсами в реально автоматизированный процесс. Pay отправляет три события webhook в течение жизни payment request: payment.created при создании запроса, payment.updated при промежуточных изменениях статуса по мере продвижения свапа, и payment.completed, когда платёж клиента settled.
Твоя интеграция должна продвигать next_due_date подписки и открывать доступ на следующий цикл только по верифицированному событию payment.completed — никогда по payment.created, и никогда просто потому что клиент вернулся на твой redirect URL. Каждая доставка webhook несёт заголовок X-SwapZillaPay-Signature — HMAC от payload; сверяй его на сервере со своим webhook-секретом, прежде чем доверять чему-либо внутри body.
Не помечай цикл оплаченным, потому что клиент вернулся в приложение. Помечай его оплаченным, потому что пришёл payment.completed с подписью, которую ты проверил.
Неверифицированный webhook endpoint — открытое приглашение: любой, кто угадает URL твоего endpoint, может отправить POST с поддельным payment.completed и открыть себе доступ. Сначала проверяй, потом действуй — каждый раз, без исключений для «доверенного» трафика.
Обработка пропущенных платежей и dunning без кнопки auto-retry
Поскольку карту нельзя повторить, а постоянного allowance для списания нет — если только ты не построил что-то custodial, чего эта архитектура намеренно избегает, — пропущенное продление это по-настоящему пропущенный платёж, а не временный отказ, который сеть тихо повторит за тебя. Твоя dunning-логика должна полностью заменить этот отсутствующий retry.
Рабочий паттерн: напоминание до даты платежа, второе напоминание если ссылка истекла неоплаченной, и создание свежего запроса вместо переиспользования истёкшего. Дай клиенту короткий grace period с сохранённым доступом после даты платежа — достаточно, чтобы покрыть «собирался оплатить вчера», и достаточно коротко, чтобы неплательщики не катались бесплатно целый лишний цикл. Если grace period прошёл без верифицированного payment.completed — понижай или приостанавливай доступ на своей стороне; это изменение состояния целиком живёт в твоём приложении, потому что у Pay нет понятия о том, что такое «доступ» для твоего продукта.
Ничто из этого не разрешается само, как это делает retry-логика карточной сети. Закладывай реальность саппорт-инбокса: клиенты, которые забыли, клиенты, у которых на кошельке не хватило баланса, и клиенты, которым нужна свежая ссылка потому что старая истекла посреди перевода.
Частые ошибки и когда этот подход не подходит
Ошибки, которые чаще всего всплывают, когда команда строит это в первый раз:
- Предположение, что у Pay есть объект subscription. Его нет — нет endpoint’а подписок, нет встроенного cadence, нет dunning-движка. Всё это состояние и логику строишь сам, опираясь на payment-request API из /pay/docs.
- Пропуск верификации подписи «пока что». Это самый частый способ, которым эксплуатируют link-per-cycle биллинг. Проверяй
X-SwapZillaPay-Signatureна каждом webhook, в каждом окружении, с первого дня. - Слишком короткое окно expiry, из-за которого честных клиентов гоняют за уже мёртвой ссылкой. Давай реальным плательщикам реальный запас времени.
- Считать фиксированную сумму инвойса неуязвимой к движению курса. Если цена задана в волатильной монете оплаты, floating-rate запрос может settle на другую сумму, чем ты ожидал, из-за движения рынка между котировкой и оплатой. Реши заранее, ценообразуешь ли ты в стабильном активе получения или закладываешь variance, и посмотри как работают routing и типы курса в агрегаторе перед тем как фиксировать подход.
- Строить весь цикл сверки, не протестировав scheduler изолированно. Сначала добейся правильной работы создания запросов самих по себе — обработку webhook и dunning-логику проще отлаживать на системе, в создании запросов которой ты уже уверен.
Эта архитектура хорошо подходит для SaaS, платной подписки на контент и регулярного биллинга услуг, где клиенты уже crypto-native и готовы каждый цикл проходить короткий платёжный flow. Она плохо подходит, если тебе нужен невидимый, нулевого трения auto-renew, неотличимый от сохранённой карты, — это custodial trade-off, которого этот гайд намеренно избегает, а не пробел, который можно обойти non-custodial способом. Также не подходит для usage-based биллинга с сильно варьирующимися крошечными суммами за цикл, где overhead создания и отслеживания ссылки на каждое списание перевешивает ценность самого платежа.
Если хочешь self-host весь стек вместо того чтобы строить поверх API, Greenfield API BTCPay Server поддерживает похожий подход «построй свой cadence сам» на инфраструктуре, которую держишь сам, — trade-off там в операционном overhead в обмен на полный суверенитет над каждым запросом в потоке.