Skip to content

Stop buying BTC on Scrypt — route EUR via USDT instead #3739

@TaprootFreak

Description

@TaprootFreak

Goal

Stop buying BTC directly on Scrypt. Convert incoming EUR on Scrypt to USDT instead and route USDT to Binance for further BTC acquisition. Same pattern that already exists for CHF (Rule 312 → Action 233).

Why

On 2026-05-21, buy_crypto #123090 triggered a 570'000 EUR → BTC trade. The fallback chain routed the entire trade to Scrypt BTC/EUR because Binance had insufficient BTC (Rule 192) and the Binance USDT refill pipeline (Rule 210) is Inactive. Result: we paid 66'487 EUR/BTC on Scrypt vs. Kraken VWAP @ 13:44 UTC of 66'218 EUR/BTC — a +0.41 % premium, ~2'257 EUR more than a hypothetical Kraken-VWAP fill, ~1'530 EUR more than an actual Kraken trade including their 0.14 % fee.

Scrypt's BTC/EUR pricing is structurally worse than its USDT pairs (in our 6-month history Scrypt BTC/EUR averages 0.6 % spread over reference, while USDT/EUR is ~0.13 %). We should not be using Scrypt's BTC pair when a cheaper route exists.

Related ongoing protective measures (do not block this issue on them):

Current state (verified against PRD DB on 2026-05-21)

LM rule chain that triggered the BTC purchase

Bank → Scrypt EUR deposit (570'000 EUR)
  → Rule 313 (Scrypt/EUR, redundancy max=1000)
    → Action 261 (Scrypt sell-if-deficit BTC, checkAssetId=113)
      → if Rule 79 (Bitcoin/BTC) has deficit → SELL EUR for BTC on Scrypt
      → onFailId=233 (Scrypt sell USDT) — only fires if 261 fails
  → Rule 314 (Scrypt/BTC, redundancy max=0.1)
    → Action 262 (Scrypt withdraw BTC to Bitcoin output wallet)

Existing migration that introduced this routing

migration/1774100000000-AddScryptSellIfDeficitAction.js does exactly:

  1. Inserts Action 261 (Scrypt sell-if-deficit BTC)
  2. Updates liquidity_management_rule.redundancyStartActionId for rule 313 from 233 to the new action's id (= 261)

Its down() method already implements the inverse — that's the change we want.

USDT path already exists end-to-end

Rule Asset redundancyStartActionId Action
312 Scrypt/CHF (max=1000) 233 Scrypt sell USDT
313 Scrypt/EUR (max=1000) 261 ← change target (currently: Scrypt sell-if-deficit BTC)
315 Scrypt/USDT (max=30000) 231 Scrypt withdraw USDT → Tron

Action 233 (Scrypt sell USDT, tradeAsset=USDT) is generic on the EUR/CHF side — it already works for CHF (Rule 312) and will work identically for EUR.

Change requested

Write a new TypeORM migration in migration/ (use timestamp newer than 1775745823000-ActivateScryptBtcWithdraw.js):

async up(queryRunner) {
  // 1. Point Rule 313 (Scrypt/EUR redundancy) at the USDT sell action
  await queryRunner.query(`
    UPDATE "dbo"."liquidity_management_rule"
    SET "redundancyStartActionId" = 233
    WHERE "id" = 313
  `);

  // 2. Remove the now-unreferenced sell-if-deficit BTC action
  await queryRunner.query(`
    DELETE FROM "dbo"."liquidity_management_action"
    WHERE "id" = 261 AND "system" = 'Scrypt' AND "command" = 'sell-if-deficit'
  `);
}

async down(queryRunner) {
  // Re-insert action 261 with its original config and re-point rule 313
  // (see migration/1774100000000-AddScryptSellIfDeficitAction.js for the original up())
}

Verify before deleting: no other action references 261 via onSuccessId/onFailId. As of 2026-05-21 the check SELECT id FROM liquidity_management_action WHERE onSuccessId=261 OR onFailId=261 returns []. Re-run before merge.

Do NOT delete:

  • Action 262 (Scrypt withdraw BTC) — still needed by Rule 314 for any residual BTC sitting on Scrypt
  • Rule 314 (Scrypt/BTC redundancy) — keep as cleanup path

Prerequisites / dependencies

Cutting the Scrypt BTC route means the next large BTC buy_crypto must be served by the Binance chain (USDT → BTC). Two preconditions must hold or buy_crypto orders will pile up on MissingLiquidity:

  1. Rule 210 (Binance/USDT) must be Active. As of 2026-05-21 it is Inactive — this is exactly why the 2026-05-21 trade fell back to Scrypt in the first place. Re-activating it is out of scope of this migration but must happen before deploying. Verify with:
    SELECT id, status, [minimal], optimal FROM liquidity_management_rule WHERE id = 210;
  2. Rule 192 (Binance/BTC) has optimal = 0.1, maximal = 50. With 0.1 BTC of optimal stock, a 8 BTC user trade still requires a full top-up cycle, but at least it will work via the USDT path once Rule 210 is active. Consider raising optimal to reduce churn, but not blocking.

Verification

Before merge

-- Confirm action 261 has no incoming references
SELECT id, system, command, onSuccessId, onFailId
FROM liquidity_management_action
WHERE onSuccessId = 261 OR onFailId = 261;
-- Expected: [] empty

-- Confirm Rule 313 currently points at 261
SELECT id, context, [maximal], redundancyStartActionId
FROM liquidity_management_rule WHERE id = 313;
-- Expected: redundancyStartActionId = 261

After deploy

-- Rule 313 now points at 233
SELECT id, redundancyStartActionId FROM liquidity_management_rule WHERE id = 313;
-- Expected: 233

-- Action 261 is gone
SELECT id FROM liquidity_management_action WHERE id = 261;
-- Expected: [] empty

-- No new Scrypt BTC/EUR trades after migration timestamp
SELECT TOP 5 id, created, symbol, cost, [order]
FROM exchange_tx
WHERE exchange = 'Scrypt' AND symbol = 'BTC/EUR' AND created > '<deploy-date>'
ORDER BY id DESC;
-- Expected: [] empty (any rows mean fallback wasn't fully cut)

End-to-end smoke test on DEV

  1. Deposit 1'000 EUR to the DEV Scrypt EUR account
  2. Watch liquidity_management_pipeline — confirm new pipeline targets Rule 313 with action 233 (not 261)
  3. Confirm exchange_tx shows a USDT/EUR Scrypt trade, not BTC/EUR
  4. Confirm Rule 315 (Scrypt/USDT redundancy) then triggers Action 231 to withdraw USDT to Tron/Binance

Useful pointers for the implementing session

  • DB debug script: ./scripts/db-debug.sh "<SQL>" (uses DEBUG_ADDRESS/DEBUG_SIGNATURE from .env)
  • LM entities: src/subdomains/core/liquidity-management/
  • ScryptAdapter (no code change expected, just verify it tolerates removal of action 261): src/subdomains/core/liquidity-management/adapters/actions/scrypt.adapter.ts
  • Reference migration to copy structure from: migration/1774100000000-AddScryptSellIfDeficitAction.js
  • Migration runner CI check: .github/workflows/api-migration-check.yaml
  • Branch protocol: feature branch off develop, draft PR back to develop, do not run terraform/local DB migrations — let CI apply

Acceptance criteria

  • New migration committed in migration/ with a timestamp > 1775745823000
  • Migration's up() reroutes Rule 313 to Action 233 and deletes Action 261
  • Migration's down() restores the previous state
  • Tests + lint + format + build green
  • Verification queries above produce expected results in DEV
  • Draft PR opened against develop on DFXswiss/api
  • Linked back to this issue

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions