Skip to content

Add HoudiniSwap prototype swap plugin - #462

Open
j0ntz wants to merge 2 commits into
masterfrom
jon/houdini-swap-prototype
Open

Add HoudiniSwap prototype swap plugin#462
j0ntz wants to merge 2 commits into
masterfrom
jon/houdini-swap-prototype

Conversation

@j0ntz

@j0ntz j0ntz commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

CHANGELOG

Does this branch warrant an entry to the CHANGELOG?

  • Yes
  • No

Dependencies

none

Description

Prototype swap plugin for HoudiniSwap (privacy-routing CEX aggregator) on the v2 partner API, modeled on changenow.ts from template.ts. Phase 2 of Asana task https://app.asana.com/0/1215088146871429/1215645061309285 (the Phase 1 coverage matrix is attached to that task).

What it does

  • src/swap/central/houdini.ts: resolve Edge assets to Houdini token ids via GET /tokens, fetch quotes via GET /quotes, pick the best private route, and create the order via POST /exchanges. initOptions is { apiKey, apiSecret }.
  • src/mappings/houdini.ts: hand-authored EdgeCurrencyPluginId to Houdini chain shortName map (32 non-memo chains from the Phase 1 matrix). Memo-required and unsupported chains map to null and are rejected by checkWhitelistedMainnetCodes.
  • test/testconfig.ts: wires HOUDINI_INIT the same way other partners do.
  • test/houdiniPartner.ts + test/houdiniFixtures/: a standalone acceptance harness that drives the real quote plus order-creation flow and caches every API response to disk, so repeat runs replay from fixtures and stay inside the free-tier budget.

Endpoint paths and cleaners are derived from the OpenAPI spec (https://api-partner.houdiniswap.com/v2/openapi.json), not guessed.

Auth quirks

  • Auth header is Authorization: <apiKey>:<apiSecret> with no Bearer prefix. Every endpoint returns 402 without it.
  • The quote endpoint keys assets by Houdini token id (a 24-char Mongo ObjectId), not by symbol, so the plugin resolves ids through GET /tokens and memoizes them per chain plus contract.
  • private quotes are leaner than standard ones: no swap, validUntil, or fixed fields, so the cleaner treats those as optional.

Observed rate-limit and routing behavior

  • Free tier: 5/min, 20/hr, 50/day quote calls; 1/min, 5/hr, 10/day exchange creations, shared across the key. Rate-limit responses are 429 with a retryAfter seconds value ({ code: "RATE_LIMIT_EXCEEDED", retryAfter, ... }).
  • Private routing has an effective minimum near 100 USD per swap. Below it, GET /quotes returns only standard routes and zero private routes (e.g. 0.00025 BTC returns 1 standard / 0 private; 0.0016 BTC returns 123 private). The prototype, which filters to private, surfaces this as a currency error today. Follow-up: read the standard route's min and raise SwapBelowLimitError instead.

Spec/docs mismatches

  • OpenAPI declares GET /chains pageSize max 1000, but the server rejects anything over 100 with VALIDATION_ERROR. Paginate at <= 100.
  • Chain.enabled is documented as required but is omitted per-object by GET /chains; the endpoint returns only enabled chains.

Testing

  • test/houdiniPartner.ts drives BTC to ETH and ETH to USDC through makeHoudiniPlugin: quote fetch plus order creation both succeed and return a real deposit address. First run hits the live API once (with a single backoff retry for the 1/min exchange limit); committed fixtures make every later run replay with zero live calls. tsc, eslint, and the mocha suite pass.
  • End-to-end in edge-react-gui on the iOS simulator: HoudiniSwap registered as a swap provider, a real BTC to ETH swap (0.0019 BTC, ~120 USD) quoted "Powered by HoudiniSwap" and executed to the in-app success scene. Proof screenshots attached below.

Scope

This is a prototype. Reverse ("to") and "max" quotes, memo/destinationTag plumbing for XRP/XLM/Cosmos chains, a sub-minimum SwapBelowLimitError, and production gui wiring are deferred follow-ups.


Note

Medium Risk
New third-party swap path that creates real deposit orders and moves user funds on success; prototype limits (from-only quotes, private-route filter, no memo chains) can cause confusing rejections below ~$100 private minimums.

Overview
Adds a HoudiniSwap centralized swap provider (houdini) wired into the plugin registry, targeting Houdini’s v2 partner API for privacy-oriented private CEX routes.

The new plugin resolves Edge assets to Houdini token IDs via GET /tokens, requests GET /quotes, selects the best in-range private quote, and books the swap with POST /exchanges. It uses apiKey/apiSecret auth, forces CORS bypass for host-side calls, supports from-only quotes, and maps ~32 non-memo chains in src/mappings/houdini.ts (memo chains stay unsupported). Amount handling normalizes API floats/scientific notation into native units; Zcash uses transparent addresses.

Testing adds HOUDINI_INIT in test config, a standalone test/houdiniPartner.ts acceptance harness (BTC→ETH, ETH→USDC), and committed test/houdiniFixtures/ so reruns replay cached API responses without live quota.

Reviewed by Cursor Bugbot for commit 7c8e50b. Bugbot is set up for automated code reviews on this repo. Configure here.

@j0ntz

j0ntz commented Jun 12, 2026

Copy link
Copy Markdown
Contributor Author

📸 Test evidence: BTC to ETH swap via HoudiniSwap, iOS sim

agent proof 1215645061309285 01 houdini enabled

agent proof 1215645061309285 01 houdini enabled

agent proof 1215645061309285 02 houdini quote

agent proof 1215645061309285 02 houdini quote

agent proof 1215645061309285 03 houdini swap success

agent proof 1215645061309285 03 houdini swap success

Captured by the agent's in-app test run (build-and-test).

Comment thread src/swap/central/houdini.ts
Comment thread src/swap/central/houdini.ts Outdated
@j0ntz
j0ntz force-pushed the jon/houdini-swap-prototype branch from 3252940 to ae500e8 Compare June 12, 2026 02:32
Comment thread src/swap/central/houdini.ts
Comment thread src/swap/central/houdini.ts Outdated
Prototype CEX swap plugin for HoudiniSwap built on the v2 partner API:
resolve Edge assets to Houdini token ids via GET /tokens, fetch quotes,
pick the best private route, and create the exchange. Credentials are wired
through testconfig.json as { apiKey, apiSecret }.

Includes a standalone acceptance harness that drives BTC->ETH and ETH->USDC
quote plus order creation, caching every API response to disk so repeat runs
replay from fixtures and stay inside the free-tier rate budget.
@j0ntz
j0ntz force-pushed the jon/houdini-swap-prototype branch from ae500e8 to 11a8fbc Compare June 12, 2026 02:38
HoudiniSwap's partner API is server-to-server and answers browser-origin
requests with HTTP 403. The core runs swap plugins inside a WebView, so the
default fetch carries Origin / Sec-Fetch-* headers the API rejects. Pass
corsBypass: 'always' so each call is made host-side, matching the API's
server-to-server contract. Verified by an in-app DASH/ETH to BTC private send
on the iOS simulator.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 7c8e50b. Configure here.

swapInfo,
floatToNativeAmount(fromWallet, largestMax, request.fromTokenId)
)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sentinel max breaks above limit

Medium Severity

When no private route accepts the swap amount, above-limit handling uses Math.max over every route max, including Houdini’s very large sentinel caps. That makes largestMax effectively unlimited, so gt(fromExchangeAmount, floatToDecimalString(largestMax)) almost never holds and SwapAboveLimitError is skipped even when the amount exceeds every finite route maximum.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 7c8e50b. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant