SEAGM
Direct-topup vendor adapter — gaming recharge, HMAC-SHA256 query-string signing, async with signed inbound webhook and a rich failure taxonomy.
Quick facts
Code SEAGM · Line direct-topup (gaming) · Auth HMAC-SHA256 query-string signing · Model async (create → poll/webhook) · Webhook signed (same HMAC) · Lookup none · Adapter services/external_vendors/direct_topup/seagm/
Gaming direct-topup (PUBG UC, BIGO, Tango) against the SEAGM Open API. Implements DirectTopUpVendor. Registered direct_topup/factory.go:176. See Order Lifecycle.
Auth — HMAC-SHA256 in the query string (headline)
Unlike most adapters, SEAGM signs via the query string, not headers. addAuth sets uid, timestamp (now.Unix()), and signature as query params on every call. The signing string covers every param — query + form body — except signature, sorted byte-wise, joined k=v&… with both key and value URL-encoded, then hex(HMAC_SHA256(string, secret_key)). The only header set is Content-Type: application/x-www-form-urlencoded. vendor_attributes: host, uid, secret_key (both in Credentials), optional callback_url. Inbound-webhook timestamp drift window is ±120s.
Catalog
GetCategories— signedGET /v1/recharge-categories(all active,TopupType:"GAMING"), with an N+1GET /v1/product-category-infoper category to fill image/description.GetProducts(categoryID)— signedGET /v1/recharge-categories/{id}/recharge-types(lazy-loaded per category;ShouldSyncVariants()=false).
par_value ≠ money. MinAmount/MaxAmount/Price are set from unit_price (what the wallet debits), NOT par_value (the in-game currency count, e.g. 60 for "60 UC") — using par_value as money once caused a ~60× overcharge. VendorDiscount = ((origin_price − unit_price)/origin_price)×100, matching the DT One convention. par_value is surfaced as a display-only variant field. Input-field labels are derived from field names (via humanizeFieldName) because SEAGM ships duplicate/redundant labels.
Order create
POST /v1/recharge-orders (form-urlencoded): type_id, buy_amount="1" always, mch_order_id, plus InputData (e.g. playerid). Async (pay_status_code=2, send_status_code=1). Duplicate mch_order_id (info_code 20135) → ErrDuplicateMerchantRef. SEAGM returns its error envelope in the body even on HTTP 4xx, so terminal info_codes are promoted to FAILED.
Order status / poll
Signed GET /v1/recharge-orders/{id} with dual lookup via ?query_type=: orderId (SEAGM id) or mchOrderId (our merchant ref → backs SupportsExternalOrderRef()=true). Not-found: code 404 / info_code 20080 → ErrOrderNotFound. Status codes: 10000→PENDING, 10001/10002→PROCESSING, 10003→COMPLETE, 10004→FAILED, 10005→CANCELLED.
Webhook
Inbound JSON callback. VerifyWebhookSignature flattens the body, requires a timestamp, enforces ±120s, and recomputes the same HMAC (constant-time compare, identical rule to outbound). ParseWebhook maps status_code, id, mch_order_id, trade_id, sent_time.
Delivery / balance
No separate confirm call — terminal state arrives via webhook or poll; CompletedAt from sent_time. SupportsLookup()=false. GetBalance = signed GET /v1/me (Ping reuses it).
Failure taxonomy & quirks
⚠️ Security: secret logged in plaintext. signedPost has a "TEMP DIAGNOSTIC" block that logs secret_key, signature, the signing string, and the full body at Info level ("Remove after the prod issue is resolved"). It leaks the vendor secret to logs and signals an unresolved production issue (POST signatures rejected while GETs work). Flag for cleanup.
error_messages.go maps only terminal, customer-clear info_codes to canonical FailureCodes (INVALID_RECIPIENT, RECIPIENT_BARRED, PRODUCT_UNAVAILABLE/OUT_OF_STOCK, AMOUNT_OUT_OF_RANGE, …); absence of a mapping = leave PENDING for the retry job. User-fixable codes surface SEAGM's own message; non-user-fixable use canonical copy (so supplier detail doesn't leak). Sentinels 20135/20080 handled before the map. vendorhttp returns the body even on 4xx/5xx, which the adapter relies on to parse the error envelope.
Tests, mock, docs
Unit (error_messages_test.go) + contract (seagm_contract_test.go, build tag contract) against mocky (http://localhost:8788/seagm). Docs + Postman (prod openapi.seagm.com, sandbox openapi.seagm.io) under docs/seagm/.
Octopus (OCTO) — federated voucher
Voucher vendor adapter that consumes another Octopus instance's /api/v1 as a supplier — login-token auth, self-loop guard, free HMAC webhook.
DT One (Top-up)
Direct-topup vendor adapter — mobile recharge over DT One DVS, Basic auth, async transactions, poll-driven (no webhook by default), reference min/max/discount mapping.