Skip to content

feat(gateway): execute a held quote, and surface what a quote did not say - #26

Merged
cardosofede merged 15 commits into
mainfrom
feat/clmm-bin-count
Aug 26, 2026
Merged

feat(gateway): execute a held quote, and surface what a quote did not say#26
cardosofede merged 15 commits into
mainfrom
feat/clmm-bin-count

Conversation

@fengtality

Copy link
Copy Markdown
Contributor

Summary

Client coverage for the unified Gateway surface, and the two-step swap flow. Supersedes #25, which was opened from a branch that has since been replaced; the work continues here on feat/clmm-bin-count.

Pairs with hummingbot/hummingbot-api#221.

A quote you can hold, then commit to

/trading/router/execute-quote existed on Gateway and was reachable from no layer above it — a caller could take a quote and had no way to execute that quote, only to re-price. execute_quote(connector, network, wallet_address, quote_id) closes the gap.

Router-only, and deliberately so: a quote id refers to route calldata Gateway cached, which pool-scoped amm/clmm swaps have no equivalent of — they price against a pool at execution. Naming a non-router connector fails here rather than silently re-pricing, which would defeat the point of the flow.

An approximated BUY is now visible

A BUY is an ExactOut order, and many thin tokens have no ExactOut route, so Gateway falls back to quoting the sell leg and quoting that input forward — paying the pool fee and crossing the spread twice. Measured at a near-constant ~2.5% across eleven pools spanning $17 to $1,963 of liquidity, and reached for only on the thin, high-fee pools where it hurts most.

The caller is not overcharged; the order is silently resized, which is what matters to a strategy that asked for a specific quantity. approximation now travels to the caller, and get_swap_quote's docstring says what to do about it — including extra_params={'approximateIfNoExactOut': False} to require an exact route instead.

Volume generated, separated from capital deployed

An executor response now carries volume_traded_quote beside filled_amount_quote. They are the same number for an executor that places orders — the amount it filled IS its volume — and deliberately different for an LP executor, whose filled amount is the capital it deposited. Depositing capital trades nothing.

The surface is pinned, not assumed

The routers are hand-written wrappers with nothing checked at import time, so a route the API renames stays here as a method that looks fine and 404s the first time someone calls it. That is how the whole rate-oracle module survived after the API stopped serving /rate-oracle/*, and how pull_image kept sending {name, tag} after the route began requiring {image_name}.

The vendored spec plus test_calls_match_api_spec.py now check every path+method a router calls, every literal query key, and every literal body key — and fail if the count of unchecked (computed) calls ever swamps the checked ones.

Companion PRs

Validation

337 tests pass. The vendored spec is regenerated from the API's own openapi.json, so the checks above run against the surface hummingbot-api#221 actually serves.

🤖 Generated with Claude Code

https://claude.ai/code/session_01GuR4iDtH4ZfT51rJfs3DhK

fengtality and others added 13 commits August 13, 2026 10:30
get_pool_info could not request the per-tick liquidity distribution, so
callers wanting bins had to bypass the client and hit the endpoint directly.
It now forwards bin_count, which hummingbot-api passes to Gateway. Meteora
always returns its bins and ignores it; orca, raydium, uniswap and
pancakeswap compute them on request, so the default of 0 keeps pool-info
cheap.

Also drops the stale note about Raydium rejecting Standard AMM pools — that
came from hummingbot-api's direct-Raydium-API path, which has been removed in
favour of routing every CLMM connector through Gateway.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ct_fees

The API has POST /gateway/clmm/add and /remove, but this router never exposed
them — the only way to shed CLMM liquidity through the client was a full close.
Adds add_liquidity() and remove_liquidity(), and documents the distinction that
matters: remove_liquidity(percentage=100) empties a position but leaves the
account open, while close_position() closes it.

close_position() and collect_fees() gain an optional pool_address. The API reads
a position's pool from its own database; positions opened by an lp_executor are
never in it, because the bot opens those straight against Gateway. Without
pool_address those calls now fail with a 400, so recovering an orphaned position
requires passing it — GET /executors/positions/orphaned reports it per orphan.

Folded into the unreleased 1.5.8.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mt84XBEMVxbbyMG8fDxDKj
The API's pool_address parameter was a silent no-op (Gateway's
positions-owned route has no pool filter), removed there; drop it here
too. Each returned row carries its own pool_address.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0166iQoxKce23GkUwuQJxdkr
…tion_info

Mirrors the new hummingbot-api endpoints closing the gaps against Gateway's
unified trading routes: pre-trade position quoting, CLMM pool creation
(AMM had it, CLMM did not), and fetching one position by address.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0166iQoxKce23GkUwuQJxdkr
Both AMM and CLMM create_pool take extra_params (Gateway-native key names:
configAddress, feeConfigIndex, openTime, binStep, feeBps, ammConfigIndex,
fee, tickSpacing, ammConfig, gasPrice, maxGas, slippagePct) instead of
per-connector named kwargs — the same contract as clmm open. Unknown keys
are rejected by the API.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0166iQoxKce23GkUwuQJxdkr
…e_to_remove; extras coverage

- Never force slippage to 1.0: omit the field when the caller omits it so the
  connector-configured slippage applies; an explicit 0 is preserved (swap
  quote/execute, clmm open/add, amm execute-swap/add-liquidity).
- Send search filters as query parameters on /gateway/swaps/search and
  /gateway/clmm/positions/search — hapi reads query params on those POSTs and
  silently ignored the JSON body, so filters, pagination, and refresh never
  applied.
- Rename CLMM remove_liquidity's percentage to percentage_to_remove (hapi's
  field name; the old key 422s) and expose slippage_pct (orca honors it).
- Expose extra_params on swap quote/execute (approximateIfNoExactOut for the
  jupiter/dflow/okx/titan routers) and clmm add_liquidity; add slippage_pct to
  amm create_pool.
- Correct create-pool docstrings to the accepted extra_params vocabulary
  (binStep/feeBps/ammConfigIndex/configAddress) and drop keys hapi rejects;
  fix the swaps-summary example to volume_by_quote_token.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0166iQoxKce23GkUwuQJxdkr
hapi removed /gateway/amm/quote-swap and execute-swap after Gateway
folded them into the unified /trading/swap route; use
gateway_swap.get_swap_quote/execute_swap with connector as name/type
(e.g. 'raydium/amm', 'meteora/clmm').

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0166iQoxKce23GkUwuQJxdkr
hapi's /gateway/clmm/pools takes a network parameter instead of assuming
mainnet-beta; without it here, no caller could reach any other network.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0166iQoxKce23GkUwuQJxdkr
add_pool POSTed to /gateway/pools, which exists only as a deprecated GET,
and omitted the required base_address/quote_address; delete_pool called
/gateway/pools/{address}, which hapi does not serve at all. Both were
404/405s. The network-scoped add_network_pool/delete_network_pool already
cover the same ground correctly and are what every caller uses.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0166iQoxKce23GkUwuQJxdkr
Every router here is a hand-written wrapper and nothing checks it against the API, so
a renamed or dropped route survives as a method that reads fine and fails only when
someone calls it. Four had been failing that way:

- docker.pull_image sent {name, tag}; the route requires a single {image_name} carrying
  the tag. A 422 on every call. condor's gateway-install flow calls this, and its tests
  mock the client, so nothing caught it.
- docker's start/stop/remove container methods addressed /docker/container/{name}/...;
  the routes are /docker/{start,stop,remove}-container/{name}. remove is a POST taking
  archive_locally and s3_bucket, not a DELETE taking force.
- docker.get_container_status has no route at all — read a container's state from
  get_active_containers()/get_exited_containers(), both of which take a name_filter.
- bot_orchestration's import_strategy_for_bot and configure_bot address routes the API
  does not serve.

The whole rate_oracle module goes too: the API has no rate-oracle router, so all seven
of its methods could only 404. market_data.get_prices pointed at it for "the oracle's
own rates"; that note now says what is actually true.

Adds the two AMM search routes the client never wrapped (events and positions), and
gateway_amm to the sync client's router list, which had been left out.

test/test_calls_match_api_spec.py is the durable half: it parses every _get/_post call
out of the routers and checks the path, query keys and body keys against a vendored
openapi.json. All four breaks above fail it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0166iQoxKce23GkUwuQJxdkr
hummingbot-api serves /gateway/swap/execute-quote and this client had no method for it,
which is one of the three layers that left Gateway's two-step swap flow unreachable. A
router quote holds a price; execute_swap prices again and throws that away.

Spec refreshed from the API that now serves the route.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GuR4iDtH4ZfT51rJfs3DhK
The API now returns `approximation` on a swap quote, the only change in
the spec. The client returns quote dicts unchanged, so the field arrives
on its own; what was missing is any reason for a caller to look at it.
A BUY is an ExactOut order, and a thin token with no ExactOut route is
quoted by pricing the sell leg and quoting that input forward — about
2.5%. The order is silently resized rather than overcharged, which is
exactly what a caller who asked for a specific quantity needs to know.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GuR4iDtH4ZfT51rJfs3DhK
…oyed

An executor response now carries volume_traded_quote beside
filled_amount_quote. They are the same number for an executor that places
orders and deliberately different for an LP executor, whose filled amount
is the capital it deposited — depositing capital trades nothing, and the
volume its range actually saw is derived from the fees it earned.

The three volume descriptions changed with it: "total filled volume" is
the phrasing that made a deposit look like trading.

get_executor's docstring names both, and the Gateway swap fields the wheel
now reports — transaction_hash, swap_provider, wallet_address, and the LIVE
slippage_pct, which reads above the configured start exactly when a
widening has already happened.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GuR4iDtH4ZfT51rJfs3DhK
The API now accepts them, so expose them here. Gateway needs GeckoTerminal for
exactly one thing on that route -- deciding which DEX an address belongs to, and
whether it is amm or clmm -- and the pool's base, quote and fee always come from
the connector afterwards. Passing both answers that directly and skips the
lookup.

It matters for the callers most likely to save a pool. An LP provider config of
'meteora/clmm' splits straight into these two arguments, and the pools such a
caller opens against are the newest ones, which are exactly the ones an indexer
is least likely to have seen.

Both or neither: passing one alone sends no params rather than half a request,
and the API rejects the mismatched case with the legal values.
… type

The docstring described the two-field split -- filled_amount_quote as the
capital deployed, volume_traded_quote as the volume -- and both halves are now
wrong. There is one field, and an LP executor reports the swaps that crossed its
range rather than its deposit, derived from the fees it earned.
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.

2 participants