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:
- Inserts Action 261 (Scrypt sell-if-deficit BTC)
- 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:
- 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;
- 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
- Deposit 1'000 EUR to the DEV Scrypt EUR account
- Watch
liquidity_management_pipeline — confirm new pipeline targets Rule 313 with action 233 (not 261)
- Confirm
exchange_tx shows a USDT/EUR Scrypt trade, not BTC/EUR
- 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
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
Existing migration that introduced this routing
migration/1774100000000-AddScryptSellIfDeficitAction.jsdoes exactly:liquidity_management_rule.redundancyStartActionIdfor rule 313 from233to 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
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 than1775745823000-ActivateScryptBtcWithdraw.js):Verify before deleting: no other action references 261 via
onSuccessId/onFailId. As of 2026-05-21 the checkSELECT id FROM liquidity_management_action WHERE onSuccessId=261 OR onFailId=261returns[]. Re-run before merge.Do NOT delete:
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:Active. As of 2026-05-21 it isInactive— 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: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 raisingoptimalto reduce churn, but not blocking.Verification
Before merge
After deploy
End-to-end smoke test on DEV
liquidity_management_pipeline— confirm new pipeline targets Rule 313 with action 233 (not 261)exchange_txshows aUSDT/EURScrypt trade, notBTC/EURUseful pointers for the implementing session
./scripts/db-debug.sh "<SQL>"(uses DEBUG_ADDRESS/DEBUG_SIGNATURE from.env)src/subdomains/core/liquidity-management/src/subdomains/core/liquidity-management/adapters/actions/scrypt.adapter.tsmigration/1774100000000-AddScryptSellIfDeficitAction.js.github/workflows/api-migration-check.yamldevelop, draft PR back todevelop, do not runterraform/local DB migrations — let CI applyAcceptance criteria
migration/with a timestamp >1775745823000up()reroutes Rule 313 to Action 233 and deletes Action 261down()restores the previous statedeveloponDFXswiss/api