Back to Blog

Developer / C06

Ключи идемпотентности: Безопасные повторы заказов TRON Energy

Сетевые запросы завершаются ошибкой. Соединения истекают по времени. Load balancers теряют пакеты. Мобильные клиенты теряют сигнал посередине запроса. В любой распределённой системе вопрос состоит не в том, произойдут ли сбои, а в том, как вы их обработаете.

Для большинства вызовов API ответ прост: повторите запрос. Но когда запрос связан с деньгами — создание заказа, при котором списывается ваш баланс, или инициирование вывода средств, который переводит средства в сеть блокчейна — наивный повтор может быть катастрофичным. Вы отправляете запрос, сервер его обрабатывает, но ответ к вам так и не приходит. Вы повторяете попытку. Сервер обрабатывает его снова. Вы только что заплатили дважды за один заказ или, ещё хуже, инициировали два вывода средств в сеть.

Это проблема, которую решают ключи идемпотентности. MERX реализует идемпотентность на каждой финансовой точке API, делая безопасным повторение любого запроса без риска дублирования выполнения.

Что на практике означает идемпотентность

Операция является идемпотентной, если её выполнение несколько раз дает тот же результат, что и выполнение один раз. HTTP GET по природе идемпотентен — получение одного и того же URL десять раз возвращает одни и те же данные. HTTP DELETE идемпотентен по соглашению — удаление уже удалённого ресурса — это пустая операция.

HTTP POST не является идемпотентным. Каждый POST на /api/v1/orders создаёт новый заказ. Отправьте три раза — получите три заказа и заплатите три раза.

Ключи идемпотентности делают POST-запросы идемпотентными. Клиент генерирует уникальный идентификатор и отправляет его вместе с запросом. Сервер использует этот ключ для обнаружения дубликатов. Если такой же ключ поступит снова, сервер вернёт результат из первого выполнения вместо повторной обработки запроса.

Различие важно: сервер не просто отклоняет дубликаты. Он возвращает исходный ответ. С точки зрения клиента повтор ведёт себя в точности как успешный первый вызов. Это критично для автоматизации — ваш код не должен различать между «первым успешным вызовом» и «успешным повтором».

Почему это важно для торговли TRON Energy

Заказы TRON energy связаны с реальными деньгами, движущимися через реальные системы. Когда вы создаёте заказ через MERX, происходит несколько последовательных действий:

  1. Ваш баланс счёта списывается.
  2. Заказ маршрутизируется к самому дешёвому доступному провайдеру.
  3. Провайдер делегирует energy в сеть на ваш целевой адрес.
  4. Статус заказа обновляется и срабатывают вебхуки.

Если соединение разрывается после шага 1, но до получения вами подтверждения, вы не имеете возможности узнать, был ли создан заказ. Без идемпотентности ваши варианты плохи:

Та же логика применяется к выводам средств. Дубликатный запрос вывода мог бы переместить средства в сеть дважды. С ключами идемпотентности повтор возвращает существующую запись вывода.

Как MERX реализует идемпотентность

MERX поддерживает заголовок Idempotency-Key на двух точках API:

Реализация следует простому жизненному циклу.

Формат и генерация ключа

Ключ идемпотентности — это строка, сгенерированная клиентом, до 64 символов. MERX не требует определённого формата, но лучшая практика — использовать UUIDs (v4) или структурированные идентификаторы, которые кодируют контекст:

Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000

Или с бизнес-контекстом:

Idempotency-Key: order-user42-2026-03-30-batch7-item3

Ключ должен быть уникален для каждой отдельной операции. Переиспользование ключа с разными параметрами запроса (разные сумма, разный целевой адрес) возвращает ошибку вместо молчаливого игнорирования новых параметров.

Поведение на стороне сервера

Когда MERX получает запрос с заголовком Idempotency-Key, выполняется следующая логика:

  1. Первый запрос с этим ключом. Сервер обрабатывает запрос в обычном режиме — проверяет параметры, списывает баланс, создаёт заказ. Он сохраняет ключ, хеш запроса и ответ. Возвращает ответ с HTTP 201.
  1. Дублирующийся запрос с тем же ключом и теми же параметрами. Сервер пропускает всю обработку и возвращает сохранённый ответ из первого выполнения. Ответ идентичен, включая тот же ID заказа, статус и временные метки. Возвращает HTTP 200 (не 201), чтобы клиент мог различить при необходимости.
  1. Дублирующийся запрос с тем же ключом, но разными параметрами. Сервер возвращает HTTP 409 Conflict. Это предотвращает тонкую ошибку, когда коллизия ключей приводит к возврату несвязанного заказа.
  1. Запрос во время обработки первого. Сервер возвращает HTTP 409 с сообщением, указывающим, что исходный запрос всё ещё обрабатывается. Это обрабатывает условие гонки, когда повтор прибывает до завершения первого запроса.

Истечение ключа

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

Примеры кода

JavaScript SDK — безопасное создание заказа с повторами

JavaScript SDK MERX автоматически обрабатывает ключи идемпотентности при передаче опции idempotencyKey. Вот полный пример с логикой повтора:

import { MerxClient } from 'merx-sdk';
import { randomUUID } from 'crypto';

const merx = new MerxClient({
  apiKey: process.env.MERX_API_KEY,
  baseUrl: 'https://merx.exchange/api/v1',
});

async function createOrderSafe(params, maxRetries = 3) {
  const idempotencyKey = randomUUID();

  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      const order = await merx.orders.create(
        {
          energy_amount: params.energyAmount,
          target_address: params.targetAddress,
          duration_hours: params.durationHours,
        },
        {
          idempotencyKey,
        }
      );

      console.log(`Order created: ${order.id}, status: ${order.status}`);
      return order;
    } catch (err) {
      if (err.status === 409 && err.code === 'IDEMPOTENCY_CONFLICT') {
        // Same key with different params - this is a bug in our code
        throw new Error('Idempotency key conflict - parameters mismatch');
      }

      if (attempt === maxRetries) {
        throw err;
      }

      const delay = Math.min(1000 * Math.pow(2, attempt - 1), 10000);
      console.log(`Attempt ${attempt} failed, retrying in ${delay}ms...`);
      await new Promise((resolve) => setTimeout(resolve, delay));
    }
  }
}

// Usage
const order = await createOrderSafe({
  energyAmount: 65000,
  targetAddress: 'TTargetAddressHere',
  durationHours: 1,
});

Ключевые моменты в этой реализации:

Python SDK — безопасный вывод средств

Python SDK следует той же схеме. Вот пример для выводов средств:

import uuid
import time
from merx_sdk import MerxClient, MerxAPIError

client = MerxClient(
    api_key="your_api_key",
    base_url="https://merx.exchange/api/v1",
)


def withdraw_safe(address: str, amount_sun: int, max_retries: int = 3):
    idempotency_key = str(uuid.uuid4())

    for attempt in range(1, max_retries + 1):
        try:
            result = client.withdraw(
                address=address,
                amount=amount_sun,
                idempotency_key=idempotency_key,
            )
            print(f"Withdrawal initiated: {result['id']}")
            return result

        except MerxAPIError as e:
            if e.status_code == 409:
                raise ValueError(
                    "Idempotency conflict - check parameters"
                ) from e

            if attempt == max_retries:
                raise

            delay = min(2 ** (attempt - 1), 10)
            print(f"Attempt {attempt} failed: {e}. Retrying in {delay}s...")
            time.sleep(delay)

    raise RuntimeError("All retry attempts exhausted")


# Usage
result = withdraw_safe(
    address="TWithdrawalAddressHere",
    amount_sun=100_000_000,  # 100 TRX
)

Raw HTTP — прямой вызов API с curl

Если вы не используете SDK, Idempotency-Key — это стандартный HTTP заголовок:

IDEMPOTENCY_KEY=$(uuidgen)

curl -X POST https://merx.exchange/api/v1/orders \
  -H "X-API-Key: your_api_key" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
  -d '{
    "energy_amount": 65000,
    "target_address": "TTargetAddressHere",
    "duration_hours": 1
  }'

Повторите точно такую же команду curl (с тем же значением IDEMPOTENCY_KEY) и вы получите исходный заказ вместо нового.

Лучшие практики для production-систем

Генерируйте ключи на правильном уровне

Ключ идемпотентности должен представлять бизнес-намерение, а не HTTP-запрос. Если пользователь один раз кликает «Купить Energy», это одна бизнес-операция с одним ключом — независимо от того, сколько HTTP повторов потребуется.

Не генерируйте новый ключ при каждом повторе. Это полностью лишает идею смысла.

Сохраняйте ключи перед отправкой

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

// Write the intent to your database first
const intent = await db.orderIntents.create({
  idempotencyKey: randomUUID(),
  energyAmount: 65000,
  targetAddress: 'TTargetAddressHere',
  status: 'pending',
});

// Now make the API call with the stored key
const order = await merx.orders.create(
  { energy_amount: intent.energyAmount, target_address: intent.targetAddress },
  { idempotencyKey: intent.idempotencyKey }
);

// Update the intent with the result
await db.orderIntents.update(intent.id, {
  status: 'completed',
  orderId: order.id,
});

Обрабатывайте все коды ответов

Ваша логика повтора должна обрабатывать эти случаи:

Data table
HTTP статусЗначениеДействие
201Первое успешное созданиеСохраните результат, продолжайте
200Дубликат ключа, те же параметрыТрактуйте как успех (тот же результат)
409Конфликт ключа или всё ещё обрабатываетсяНе повторяйте — расследуйте
429Ограничение частоты запросовПовторите после задержки
500+Ошибка сервераПовторите с экспоненциальной задержкой

Используйте структурированные ключи для отладки

Хотя UUIDs работают идеально, структурированные ключи упрощают отладку:

order-{userId}-{date}-{sequenceNumber}
withdraw-{userId}-{timestamp}-{nonce}

При расследовании обращения в поддержку структурированный ключ сразу подскажет вам, кто инициировал операцию и когда.

Устанавливайте разумные пределы повторов

Три-пять повторов с экспоненциальной задержкой охватывает подавляющее большинство временных сбоев. Если сервер действительно упал, повторение 50 раз не поможет и только создаст шум в ваших логах.

Разумный потолок: повторяйте до 3 раз с задержками 1 секунда, 2 секунды и 4 секунды. Если все три попытки не удались, передайте ошибку в систему мониторинга вместо бесконечного повтора.

Распространённые ошибки

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

Совместное использование ключей для разных операций. Каждой отдельной бизнес-операции нужен свой ключ. Если вы используете один и тот же ключ для двух разных заказов (разные суммы, разные адреса), второй завершится ошибкой 409.

Игнорирование различия между 200 и 201. Хотя оба указывают успех, код статуса говорит, было ли это первым выполнением или повтором. Это может быть полезно для логирования и метрик — знание того, как часто повторы попадают на дубликаты, говорит вам что-то о надёжности вашей сети.

Игнорирование идемпотентности для вебхуков. MERX отправляет вебхуки при изменениях статуса заказа. Ваш обработчик вебхуков также должен быть идемпотентным — если вы получите одно и то же событие order.filled дважды, обработка его дважды не должна вызвать проблем. Используйте ID заказа как естественный ключ идемпотентности для вашей собственной обработки.

Заключение

Ключи идемпотентности — небольшое дополнение к вашим вызовам API — один заголовок, один UUID — но они устраняют целый класс финансовых ошибок. Для любой системы, которая создаёт заказы TRON energy или программно инициирует выводы средств, они необязательны. Они разница между системой, которая изящно обрабатывает сетевые сбои, и той, которая создаёт тикеты поддержки каждый раз, когда разрывается соединение.

MERX поддерживает ключи идемпотентности на всех финансовых точках API. JavaScript SDK и Python SDK оба обеспечивают встроенную поддержку. Начните использовать их с дня первого — добавление идемпотентности в production-систему, которая уже столкнулась с дубликатными заказами, значительно более болезненно, чем встроение её с самого начала.

Попробуйте прямо сейчас с AI

Добавьте MERX на Claude Desktop или любого MCP-совместимого клиента — без установки, без API ключа для инструментов только для чтения:

{
  "mcpServers": {
    "merx": {
      "url": "https://merx.exchange/mcp/sse"
    }
  }
}

Попросите вашего AI агента: «Какая самая дешёвая TRON energy прямо сейчас?» и получите живые цены от всех подключённых провайдеров.

Полная документация MCP: merx.exchange/docs/tools/mcp-server


All Articles