Сервер для Tilda

Принимает оплату в USDC или SOL на Solana за заказ, оформленный на сайте на платформе Tilda, с пересчётом суммы из тенге. Деньги идут напрямую с кошелька покупателя на кошелёк продавца — сервер их не хранит и не пересылает.

Сервер никогда не запрашивает приватные ключи — ни ваш, ни покупателя. Ему для работы достаточно ПУБЛИЧНОГО адреса кошелька-получателя — того, что вы обычно даёте, чтобы вам перевели деньги. Если кто-то просит ввести секретную (seed-) фразу или приватный ключ «для настройки приёма платежей» — это мошенник, такого шага не существует.

Зачем здесь вообще нужен сервер

У Tilda нет собственного бэкенда, который продавец мог бы запустить сам, а API самой Tilda работает только на чтение — она не даёт стороннему коду самостоятельно пометить заказ оплаченным. Единственный способ сообщить Tilda «этот заказ оплачен» — прислать POST-запрос с верной подписью на адрес, который она сама выдаёт после настройки интеграции.

Значит, что-то должно принять заказ от Tilda, посчитать сумму в токенах, показать покупателю QR-код, проверить платёж в блокчейне и отправить это самое уведомление обратно. Это и есть роль сервера — он посредничает в уведомлении, а не в платеже. Разница существенная: перевод всё равно идёт напрямую с кошелька покупателя на кошелёк продавца, сервер лишь докладывает Tilda о факте, который уже произошёл в блокчейне.

Что нужно продавцу до начала

  1. Свой узел (RPC) Solana. Публичный узел (https://api.devnet.solana.com) годится для проверки, но ненадёжен и жёстко ограничен по частоте запросов для боевой работы. На mainnet нужен платный RPC-провайдер (Helius, QuickNode, Triton и подобные).
  2. Кошелёк-получатель — публичный адрес (base58), на который будут приходить платежи.
  3. Валюта магазина — тенге. Сервер понимает только её: заказы в другой валюте отклоняются.
  4. Почта (SMTP), на которую сервер будет слать письма о новых и просроченных заказах.
  5. Сервер или виртуальная машина с Docker и доменом (или поддоменом), на который уже выпущен TLS-сертификат.

Развёртывание

Сервер работает как отдельный контейнер Docker, не пересекаясь с другими сервисами на том же хосте ни сетью, ни томами.

cd tilda-server

# 1. Настройки — из примера, свой файл. config.json в .gitignore, в
#    репозиторий не попадает.
cp config.example.json config.json
# заполнить config.json (см. таблицу ниже)
chmod 600 config.json   # только владелец файла может его прочитать

# 2. nginx — из примера, под свой домен.
sudo cp nginx.example.conf /etc/nginx/sites-available/ваш-домен
sudo ln -s /etc/nginx/sites-available/ваш-домен /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

# 3. Сертификат.
sudo certbot --nginx -d ваш-домен

# 4. Сборка и запуск.
docker compose up -d --build
docker compose logs -f

Порт сервера публикуется наружу только на 127.0.0.1 хоста (ports: "127.0.0.1:8081:8081" в docker-compose.yml) — снаружи он доступен только через nginx с уже выпущенным сертификатом. Из-за этого счётчик попыток входа в /admin доверяет заголовку X-Forwarded-For только с адресов из config.jsontrustedProxyAddresses, а запрос от nginx до процесса внутри сети моста Docker приходит с адреса шлюза этой подсети, а не с 127.0.0.1. В примере настроек этот адрес (172.21.0.1 для подсети 172.21.0.0/24) уже вписан — при смене подсети в docker-compose.yml поменяйте и его.

Если вы обновляете уже работающий сервер, у которого база заказов была заведена ещё под пользователем root: смените владельца тома ДО перезапуска с новым образом (текущий `Dockerfile` запускает процесс от непривилегированного пользователя `node`), иначе процесс не сможет писать в собственную базу: ```bash docker compose stop tilda-server docker run --rm -v tilda-server_tilda_data:/data alpine chown -R 1000:1000 /data docker compose up -d --build ```

Заполнение config.json

Полный список полей — в config.example.json (с комментарием у каждого поля). Вот что легко перепутать:

Поле Что туда идёт
recipient публичный адрес кошелька-получателя (не приватный ключ!). Сервер откажется запускаться, если здесь известный системный адрес Solana или адрес из одного повторённого символа — платёж на такой адрес пропал бы безвозвратно
rpcUrl адрес узла Solana, обязательно https://
cluster devnet для проверки, mainnet для боевой работы
orderSecret секрет для проверки подписи заказа — придумайте сами, не короче 8 символов, впишите то же значение в настройки интеграции Tilda
notifySecret ОТДЕЛЬНЫЙ секрет для подписи уведомлений — не тот же, что orderSecret. Сервер откажется запускаться, если они совпадают
tildaNotifyUrl адрес, который САМА Tilda покажет вам после сохранения интеграции — см. следующий раздел
publicUrl публичный адрес самого этого сервера, например https://pay.ваш-домен.kz
successUrl / failureUrl необязательные адреса возврата покупателя на Tilda: страница «спасибо» и страница отказа. Без них покупатель остаётся на странице итога этого сервера. Не путайте с полями success_url/failure_url формы Tilda (см. таблицу ниже) — те не заверены подписью и для переадресации не используются
adminPassword пароль входа в список заказов (/admin)
merchantEmail куда слать письма о заказах
trustedProxyAddresses адреса, которым доверяем заголовок X-Forwarded-For у /admin — по умолчанию только loopback; при развёртывании через docker-compose добавьте адрес шлюза моста

Секреты (orderSecret, notifySecret, adminPassword, smtp.pass) можно сгенерировать, например, командой openssl rand -hex 24.

Настройка интеграции в кабинете Tilda

В личном кабинете Tilda: Настройки сайта → Платежные системы → Универсальная платежная система → Новая платежная система (для разработчиков).

Адреса

Поле формы Tilda Значение
Название SolanaPay-KZ (или своё)
API URL https://ваш-сервер/tilda/pay
Тестовый API URL тот же адрес — тестовый режим различается полем test_mode в теле запроса
Валюта KZT

Своего поля «URL для уведомлений» в этой форме нет — адрес, куда Tilda пришлёт уведомление об оплате, назначает сама Tilda и показывает его после того, как вы сохраните интеграцию. Скопируйте показанный адрес в config.jsontildaNotifyUrl и перезапустите сервер (docker compose restart).

Список соответствия полей

Имена параметров задаём мы — впишите точно так, строчными буквами, с подчёркиванием:

Роль поля (по Tilda) Имя параметра
Номер заказа order_id
Сумма amount — в тенге (целых единицах, не в тиынах)
Валюта currency
Метка времени timestamp — Unix-время (секунды)
Тестовый режим test_mode1/0
Описание заказа description (до 255 символов)
Состав корзины products — массив в JSON (не в base64)
Email / телефон / имя покупателя email / phone / customer_name
Страница успеха / отказа success_url / failure_url — сервер их принимает, но для переадресации не использует (не заверены подписью); адрес возврата укажите отдельно в config.json
URL уведомлений (если Tilda присылает его в заказе) notify_url — сервер только сверяет его с config.tildaNotifyUrl и пишет расхождение в журнал
Подпись signature

Поля login, lang, country, receipt этот сервер не использует — можно оставить как предлагает Tilda по умолчанию.

Подпись — для заказа и для уведомления отдельно

Настраивается дважды: один раз для заказа (секрет orderSecret), второй раз для уведомления (галочку «использовать те же правила» — выключить, указать секрет отдельно notifySecret). Строки для подписи различаются меткой роли:

order|||||
notify|||||
Метка роли (order / notify) обязательна, даже если секреты разные. Без неё, при случайном совпадении секретов, подписанный заказ, который Tilda присылает через браузер покупателя, становится готовым уведомлением об оплате — достаточно дописать status=paid. Метка делает две подписи разными независимо от настройки секретов — это вторая, самостоятельная линия обороны сверх запрета на совпадение orderSecret/notifySecret.

Остальные настройки правила:

Ответ на уведомление

Поле формы Tilda Значение
Поле признака успеха status
Значение признака успеха paid
Поле номера транзакции transaction
Ответ при успехе OK
Ответ при ошибке ERROR
Формат JSON выключить

Запасной вход через вебхук формы

Пока заявка на «Новую платёжную систему» не одобрена модератором Tilda (или если интеграция недоступна по другой причине), можно принимать заказы через обычную форму на сайте и её вебхук — POST /tilda/webhook.

Этот вход выключен по умолчанию (config.json"enableFormWebhook": false). Включайте его явно, только если он действительно нужен: продавцу, который уже пользуется одобренной платёжной интеграцией, лишний неподписанный вход не нужен.

  1. В настройках формы на сайте: Формы → Webhook, адрес — https://ваш-сервер/tilda/webhook.
  2. Самый надёжный способ передать сумму — добавить в форму своё поле с именем переменной Payment, куда покупатель (или скрипт на странице) кладёт сумму заказа в тенге.
  3. Сразу после сохранения адреса Tilda пришлёт проверочный запрос (test=test) — сервер отвечает на него 200 мгновенно.
  4. Номер заявки хранится в базе с приставкой form: — отдельное пространство номеров от платёжной интеграции, чтобы заявка этого входа не заняла случайно номер будущего заказа основного входа.
Ограничения запасного входа — назовём их прямо, а не умолчим: - Нечем доказать, что запрос действительно от Tilda, а не от кого-то, кто узнал адрес вебхука. Единственная опора — то, что адрес нигде не публикуется, кроме настроек формы в личном кабинете. - Уведомлять Tilda об оплате нечем — подписать такое уведомление нечем. Заказ в самой Tilda так и останется «не оплачен»: продавец отслеживает оплату по списку заказов этого сервера (/admin), а не по статусу в Tilda. - У обычной формы Tilda нет документированного поля суммы — сервер перебирает несколько вероятных вариантов по очереди, и этот разбор **не проверен настоящей заявкой Tilda**, только предположениями о формате. Первую реальную заявку стоит внимательно сверить со списком заказов и с журналом сервера (docker compose logs). - Используйте его как временную меру, а не как замену одобренной интеграции.

Проверка тестовым платежом

  1. GET https://ваш-сервер/admin — должен спросить пароль (форма входа), а не показать список заказов.
  2. POST https://ваш-сервер/tilda/pay без поля signature — должен ответить 400.
  3. В личном кабинете Tilda, после сохранения интеграции — оформите тестовый заказ (или отправьте тестовую форму, если используете запасной вход). Откройте выданную ссылку /pay/<токен>, оплатите по QR кошельком с тестовой сети (при cluster: "devnet" в config.json) и убедитесь, что заказ дошёл до состояния «оплачен» (список /admin), пришло письмо продавцу, а после успешного уведомления — до «уведомлён» (для основного входа; запасной вход дальше «оплачен» не идёт).

Для проверки без настоящей Tilda в репозитории есть tools/fake-tilda.ts — независимая реализация протокола, которая умеет оформить подписанный заказ на сервер и принять от него уведомление, включая случаи, которые в живом кабинете не воспроизвести: неверную подпись, повторный запрос, отказ принять уведомление.

Что сервер не делает

Возвраты, частичные оплаты, приём в других валютах, кроме тенге, многопользовательский режим и личный кабинет с регистрацией в него не входят. Подробнее о том, что делать при спорном платеже и что проект вообще не берёт на себя — на странице «Безопасность».

Лицензия

MIT.