Troubleshooting
This page covers concrete debugging strategies for the integration paths that aren't always obvious. If you hit something not listed here, email support@ghostswap.io with the response X-Request-Id header.
"Invalid pair: x-y not available or temporary disabled" on swap creation
You'll see this as:
{ "error": { "type": "validation_error", "code": "invalid_request",
"message": "Invalid pair: btc-eth not available or temporary disabled",
"upstream_code": -32602 } }The literal text isn't always the literal cause. This message wraps several different root causes — the underlying error code (-32602) is "invalid params" at the JSON-RPC layer, and the platform sometimes serves this generic message even when the real issue is something else.
Most common actual causes (in order of likelihood):
- The
addressorrefundAddressfailed the strict creation-time validator. Even thoughPOST /v1/addresses/validateaccepted the address, the swap-creation path runs a stricter chain-specific validator. Try a different address format:- BTC: try a legacy
1…/3…address instead of a bech32bc1…(or vice versa) - ETH: try a checksummed mixed-case address (
0xAbCd…) instead of all-lowercase
- BTC: try a legacy
amountFromhas too many decimal places for the source chain's precision. Try fewer decimals (e.g.0.001instead of0.00100012).- Genuine temporary disable of the pair. Rare for major pairs (BTC↔ETH, ETH↔USDT, etc.). Try a different pair to rule out a global account issue.
Debug recipe — strip the swap to its minimum
// Try with NO refundAddress to isolate which field is rejected.
const res = await fetch(`${BASE}/v1/swaps`, {
method: 'POST',
headers: {
'Authorization': AUTH,
'Content-Type': 'application/json',
'Idempotency-Key': crypto.randomUUID(),
},
body: JSON.stringify({
from: 'btc', to: 'eth',
amountFrom: '0.001', // round number, well above min
address: '0xKnownGoodEthAddr', // an ETH address you control
// NO refundAddress
}),
});If this succeeds → the refund address was the issue. Try a different format. If this fails with the same error → it's the payout address or the amount.
validateAddress accepts but createTransaction rejects
Known behavior. The two validators are different:
POST /v1/addresses/validateruns a permissive chain-format check — basically "is this a syntactically valid address for this chain?"POST /v1/swapsruns a stricter check internally that rejects addresses that parse but won't actually work for the on-chain payout (e.g. addresses with the wrong checksum, or for the wrong network within a chain family).
Don't treat validateAddress as proof the swap will succeed. Treat it as inline UX feedback during typing — fast and lenient. The real verdict comes at POST /v1/swaps.
Verifying a swap was actually created
When POST /v1/swaps returns HTTP 201 with a swap object, the swap is real. Your own DB or dashboard may lag — the source of truth is GET /v1/swaps/:id:
curl https://partners-api.ghostswap.io/v1/swaps/swp_abc123 \
-H "Authorization: Bearer $TOKEN"If it returns the swap row, the swap exists in our system and the upstream liquidity layer.
For the list view of your org's swaps:
curl https://partners-api.ghostswap.io/v1/swaps?limit=20 \
-H "Authorization: Bearer $TOKEN"Where do I see the dashboard for my swaps?
The GhostSwap partner dashboard at /dashboard/transactions shows every swap created with credentials owned by your organization. Auto-refreshes every 10 seconds; flashes rows whose status changed.
apiExtraFee (visible on swap records) is platform-operator config, not partner-controllable. It's the percent the platform charges on top of the upstream cost — already factored into amountUserReceives in quotes.
"I'm getting 503 upstream_not_configured"
Server-side: the platform-shared upstream liquidity key isn't wired up. Email support@ghostswap.io with the X-Request-Id — this is operator action, not something you can fix.
In the meantime, you can develop against a stub. The LLM brief shows a MOCK_GHOSTSWAP=1 pattern: lib/ghostswap.js short-circuits gsFetch to canned responses when the env var is set.
"I'm getting 503 provider_credential_pending"
Your dedicated liquidity key is still being activated by our liquidity team. The error message is:
Your liquidity key is being activated. Reads (currencies, pairs, quotes, address validation) work; swap creation will be enabled within 1–3 business days.
This means:
- Read endpoints (currencies, pairs, quotes, address validation) work right now
POST /v1/swapsis blocked until the activation finishes- Typical wait: 1–3 business days after admin approval
What to do:
- Build the rest of your integration (UI, polling logic, error handling) using the read endpoints
- Use
MOCK_GHOSTSWAP=1against a stub to test swap creation flows end-to-end - Watch your dashboard at
/dashboard/api-credentials— when the activation lands, the next/v1/swapsPOST will succeed
If it's been more than 3 business days, email support@ghostswap.io with your X-Request-Id.
"I'm getting 502 upstream_bad_response after a retry"
Our backend already retries this once internally before surfacing it. If you see it on your side, both attempts hit a non-JSON response from the liquidity provider's edge — almost always a transient Cloudflare/CDN hiccup.
Fix: retry once with the same Idempotency-Key. If it persists for more than a few minutes, email support with your X-Request-Id.
Idempotency key — when does it actually save you?
Idempotency-Key (UUID v4 on POST /v1/swaps) protects against duplicate swap creation in three concrete situations:
- Network retry: your fetch fails with a transient error; you retry with the same key — get the same swap back, no duplicate.
- Concurrent click: user clicks "Confirm" twice in quick succession (e.g., laggy UI). If both requests carry the same key, both return the same swap.
- Process crash mid-call: your service crashes between sending the request and storing the response. On restart, you can replay with the same key and recover.
The key must be the same across retries of the same logical attempt. Generate it once per "Confirm click" and store it before sending. Don't generate a new UUID inside the retry loop.
If you reuse the same Idempotency-Key with a different request body, you get HTTP 409 conflict — that's a guard against accidental key reuse with mismatched data.
429 rate-limited — retry strategy
HTTP/1.1 429 Too Many Requests
Retry-After: 1
Read Retry-After (seconds) from the response header. Wait that long, then retry with the same Idempotency-Key (if it was a swap creation). The upstream rate limit is per-credential — separate credentials don't share a quota.
If you see 429 on read endpoints (/v1/currencies, etc.), back off. These calls are cheap to cache (5–10 min for currencies).
"I lost my secret"
You have two paths:
You think the secret is just lost (not leaked) — visit /dashboard/api-credentials, find the credential row, click Reveal secret. The secret + a fresh bearer token are shown again. Copy them, store securely, click Hide.
You think the secret may have leaked (committed to a repo, posted in chat, etc.) — revoke instead. Click Revoke on the credential row → it's invalidated everywhere within seconds. Then click Create live credential for a new pair.
If a credential was created before recovery was supported (early-stage credentials), Reveal returns "This credential was created before secret recovery was enabled. Revoke it and create a new one to get a recoverable secret." — follow the revoke + reissue path.
Every Reveal action is audit-logged on our side.
Common confusion: amountTo vs amountUserReceives
Always show amountUserReceives to the user — it equals amountTo - networkFee and is the realistic estimate. Showing raw amountTo overstates the user's payout by the network fee.
Currency icons that 404
Some currencies have image: null in the response. Always wrap your icon render in a fallback:
{currency.image
? <img src={currency.image} alt="" onError={(e) => e.target.style.display = 'none'} />
: <span className="ticker-fallback">{currency.ticker[0].toUpperCase()}</span>}See also
- Errors — full error type matrix
- Idempotency — semantics and retry patterns
- Status lifecycle — every state transition
- End-to-end swap guide — happy-path implementation