Skip to content

Add StealthEX swap plugin - #485

Open
j0ntz wants to merge 3 commits into
masterfrom
jon/integrate-stealthex-swap
Open

Add StealthEX swap plugin#485
j0ntz wants to merge 3 commits into
masterfrom
jon/integrate-stealthex-swap

Conversation

@j0ntz

@j0ntz j0ntz commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

CHANGELOG

Does this branch warrant an entry to the CHANGELOG?

  • Yes
  • No

Dependencies

none

Description

Adds StealthEX as a centralized swap provider, on the v4 API.

Asana: https://app.asana.com/0/1215088146871429/1217498055202092

Asset identity. StealthEX names every asset by a symbol plus a network, so src/mappings/stealthex.ts records both halves per chain: the symbol and network of the chain's native asset, plus the network its tokens live on. A chain's native asset usually sits on mainnet, but plenty do not (BNB on bsc, AVAX on avax-c, WAXP on wax, OSMO on cosmos, ETH on each L2's own network), which is why the mapping spells out both rather than deriving one from the other.

Token identity comes from the CONTRACT ADDRESS, never from the Edge currency code. The plugin pulls StealthEX's full /v4/currencies catalog (paged at 250, about a thousand listings, page 0 then batches of four), indexes it by provider network, and canonicalizes each listing's contract_address through that wallet's own currencyConfig.getTokenId so the index is keyed by Edge tokenId. Casing differences and junk values in the provider's contract field drop out of that step. A native asset must match the mapped symbol on its network AND carry no contract address, so a token listing can never be resolved for a native swap. A token StealthEX lists without a contract address (USDC on Tron among them) stays unsupported rather than quoted on a ticker match.

The catalog is cached for an hour and replaced atomically, only after every page arrives non-empty. A failed or empty refresh keeps the previous good index and leaves the stamp expired, so the next quote retries instead of caching an outage. The expiry is not theoretical: ARRR and ZANO were both listed on 2026-08-14 and are absent from the catalog on 2026-08-18.

Rate type. Quotes use a fixed rate when both assets support one, which is also what makes "quote by the amount I want to receive" work (StealthEX only offers reversed estimation on fixed rates). Both assets advertising fixed support does not guarantee the pair has a fixed route, so a missing route falls back to a floating quote with isEstimate: true.

fetchSwapQuote runs checkInvalidTokenIds first, so the shared same-asset guard and the default invalid-token list apply here as they do in the sibling central plugins.

Other notes. Requests go through io.fetchCors: StealthEX blocks browser-origin requests, and swap plugins run inside a WebView. Limits come from POST /v4/rates/range and map to SwapBelowLimitError / SwapAboveLimitError; NoPair / NoExchangeRoute / RouteIsDisabled / MarketUnavailable map to SwapCurrencyError and NotAllowed to a reasonless SwapPermissionError: StealthEX returns NotAllowed for causes it does not name (it was observed refusing a route minutes after the identical call succeeded), so claiming a geographic restriction would be a guess.

Telos and Monad are listed by StealthEX but mapped to null: neither listing carries an address format, so which address space they expect cannot be confirmed, and a wrong guess loses funds.

Errors. One helper maps a StealthEX error kind to its Edge error for every call site (/rates/range, /rates/estimated-amount, /exchanges), so a kind cannot mean an unsupported pair on one path and a generic failure on another. A catalog outage raises a real provider error rather than SwapCurrencyError, so it keeps its error ranking instead of reading as an unsupported pair.

Fixed orders always carry a locked rate. A fixed estimate that comes back without rate.id is treated as no fixed route: a direct quote falls back to floating with the range, limit check and estimate re-run for that rate, so isEstimate always describes the order actually created.

Max quotes create one order, not two. getMaxSwappable runs the quote function twice, so the quote path is split: fetchQuote resolves the route, checks limits and returns both addresses without creating anything; fetchProbeOrder builds the probe spendInfo from it (the user's own from-address, skipChecks: true) so getMaxSpendable can price the network fee; and fetchSwapQuoteInner is the only caller that POSTs /exchanges. Above the maximum the probe quotes the range maximum instead of throwing, because StealthEX rejects an out-of-range amount at /rates/estimated-amount with 400 InvalidAmount and a max swap that fits once the fee is subtracted would otherwise never get priced; the real quote still raises SwapAboveLimitError. Measured against the live API, a max quote issued two orders before this and one after.

Trust boundary. The deposit amount StealthEX returns becomes a signed spend, so a from quote (a max quote arrives here as one) rejects an order whose deposit exceeds the requested amount. A reversed quote pins the receive side, so there is nothing local to bound the source against.

The review rules and the API requirement this integration surfaced are in #489: per-endpoint auth, catalog cache expiry, the scope a fixed-then-floating fallback is allowed to cover, and the per-route rate-type reporting StealthEX does not publish.

A compliance and gap report for StealthEX v4 against docs/API_REQUIREMENTS.md is published at https://gist.github.com/j0ntz/ae71f231a5939489cf4611325058c00c

Section 7 of that report, account activation, now reads Pass, settled with real funded swaps on every reserve network Edge and StealthEX both support: XRP (26.337 TRX for 6.1363 XRP), ALGO (0.0035 ETH on Base for 87.20 ALGO), XLM (1968.5 DGB for 42.697 XLM) and SOL (0.0036226 ETH on Base for 0.09283 SOL). Each destination wallet was created in Edge for the test and did not exist on chain when the swap was placed; each payout created the account at exactly the quoted amount, with nothing needed from Edge or from the user. StealthEX also pre-checks the destination on XRP, where a payout below the 1 XRP base reserve to an unactivated address is refused outright (NotAllowed, "This address can not be used") while the same payout to an activated address is accepted. That pre-check is chain-specific: Algorand accepts an order whose payout would land below its 0.1 ALGO minimum balance, and every Stellar route already prices its minimum above the 1 XLM reserve, so the case cannot be ordered there at all. Hedera stays untestable from Edge, because a fresh Edge Hedera wallet has no account id to receive into. The one wart is that the refusal reuses the catch-all NotAllowed kind, so this plugin can only surface it as a reasonless SwapPermissionError; that is filed as an ask under the report's error-handling section.

Companion GUI PR wires the plugin up: EdgeApp/edge-react-gui#6160


Note

Medium Risk
New swap path handles real deposits and signed spends with provider-returned amounts; mistakes in mapping, limits, rate fallback, or the trust boundary could misquote or over-debit users, though the implementation follows established central-plugin patterns.

Overview
Adds StealthEX as a new centralized swap provider on the v4 API, registered in src/index.ts as stealthex.

The plugin (src/swap/central/stealthex.ts) pages and caches the /currencies catalog (hourly TTL, batched fetches), resolves assets by contract address via a per-chain token index, then quotes through range → estimate → a single /exchanges order. It prefers fixed rates when both assets advertise them and falls back to floating only when the fixed route is missing (not on limit/pair errors); quoteFor: 'to' uses reversed estimation on fixed routes only. Max swaps use a quote-only probe (skipChecks, no order) before one real order; deposit amounts from the provider are floored to integer native units and capped against the requested from-amount on from quotes.

Chain coverage lives in src/mappings/stealthex.ts (symbol + network per chain, explicit null where support is unconfirmed).

Docs and review guidance are updated: CHANGELOG, new Rate types section in docs/API_REQUIREMENTS.md, and BUGBOT rules for per-endpoint auth, catalog TTL, and scoped rate-type fallback.

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

@j0ntz

j0ntz commented Aug 14, 2026

Copy link
Copy Markdown
Contributor Author

📸 Test evidence

stealthex quote

stealthex quote

stealthex success

stealthex success

stealthex tx details

stealthex tx details

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

@j0ntz
j0ntz marked this pull request as ready for review August 14, 2026 23:58
@j0ntz
j0ntz force-pushed the jon/integrate-stealthex-swap branch from adc61e1 to b442b38 Compare August 14, 2026 23:58
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

Comment thread src/swap/central/stealthex.ts Outdated
@j0ntz
j0ntz force-pushed the jon/integrate-stealthex-swap branch from b442b38 to 5a63ddd Compare August 15, 2026 00:05
Comment thread src/swap/central/stealthex.ts
@j0ntz
j0ntz force-pushed the jon/integrate-stealthex-swap branch from 5a63ddd to 8ae9a4d Compare August 15, 2026 00:13
Comment thread src/swap/central/stealthex.ts
Comment thread src/swap/central/stealthex.ts

@paullinator paullinator left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Review of the StealthEX swap plugin. Six items, numbered for reference: 1 (must fix), 2 (warning), 3-6 (must fix). Item 1 is the main one and includes a suggested implementation approach.

  1. Must fix - token identity is guessed from the Edge currency code instead of resolved from the contract address.
  2. Warning - EVM chains are selected through a static network table rather than the numeric chain ID.
  3. Must fix - resolved currencies are cached for the lifetime of the plugin with no expiry.
  4. Must fix - every currency lookup failure is reported as an unsupported pair.
  5. Must fix - /rates/range maps NotAllowed differently from the estimate and exchange paths.
  6. Must fix - fixed-rate orders are created without requiring rate_id.

Comment thread src/swap/central/stealthex.ts Outdated
Comment thread src/mappings/stealthex.ts
Comment thread src/swap/central/stealthex.ts Outdated
Comment thread src/swap/central/stealthex.ts Outdated
Comment thread src/swap/central/stealthex.ts Outdated
Comment thread src/swap/central/stealthex.ts
Comment thread src/swap/central/stealthex.ts Outdated
Comment thread src/swap/central/stealthex.ts
Comment thread src/swap/central/stealthex.ts
@j0ntz

j0ntz commented Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

All six items handled. 1: assets now resolve from the full /v4/currencies catalog, indexed by provider network and keyed by the Edge tokenId that each listing's contract_address canonicalizes to through currencyConfig.getTokenId; the currency code no longer participates in token identity. 3: the catalog cache is bounded at one hour, replaced atomically only after a complete non-empty fetch, with the previous index kept and the stamp left expired on a failure. 4: currency resolution no longer collapses into SwapCurrencyError; a catalog outage raises a real provider error and NotAllowed becomes SwapPermissionError. 5: one stealthexError(path, kind, request) helper now serves /rates/range, /rates/estimated-amount and /exchanges. 6: a fixed estimate without rate.id is treated as no fixed route, so a direct quote falls back to floating and isEstimate always describes the order created. 2 needs a StealthEX API change (no numeric chain id exists in their catalog, and a contract_address inside a quote route is accepted and ignored); it is documented with the other gaps at https://gist.github.com/j0ntz/ae71f231a5939489cf4611325058c00c and summarized in the PR comment above.

@j0ntz
j0ntz force-pushed the jon/integrate-stealthex-swap branch 3 times, most recently from 4978ebe to 125c91a Compare August 18, 2026 23:43
Comment thread src/swap/central/stealthex.ts
@j0ntz
j0ntz force-pushed the jon/integrate-stealthex-swap branch 3 times, most recently from 7299094 to 6bd3f85 Compare August 19, 2026 00:12

@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 using high effort 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 6bd3f85. Configure here.

Comment thread src/swap/central/stealthex.ts
j0ntz added 2 commits August 19, 2026 11:07
StealthEX identifies assets by a symbol plus a network, so the chain mapping
spells out both halves for a chain's native asset and the network its tokens
live on. Token symbols are not unique across listings, so each side of a quote
is confirmed against StealthEX's own currency record before quoting.

Quotes use fixed rates when the route offers them, which also covers quoting by
the amount the user wants to receive, and fall back to floating rates
otherwise.
@j0ntz
j0ntz force-pushed the jon/integrate-stealthex-swap branch from 6bd3f85 to 2944889 Compare August 19, 2026 18:10
j0ntz added a commit that referenced this pull request Aug 21, 2026
ff4cc3d is the only revision of this document that relaxed anything,
and none of the changes below appear in its commit message, which
describes its scope as mapping errors to edge-core-js classes, naming
the integration points, and expanding the Reporting API section.

The reference point throughout is Paul's "2025 Exchange Provider API
Requirements" doc, which ef51604 transcribes almost verbatim.

Native units are now the single convention, stated once:

The document had drifted into per-section conventions. Section 3 asked
for display units and called itself "the one place the native-unit
preference does not apply", section 6 asked for display units citing
StandardTx's asSafeNumber typing, and sections 4 and 6 asked for native
units. Amount Representation now states outright that it governs every
amount field and that no section carries a different one, sections 3, 4
and 6 all read the same, and the asSafeNumber fact moves to where it
belongs: it constrains Edge's storage, not the provider's wire format,
and the reporting plugin already converts.

The source doc states no convention at all. Its only amounts example
sits inside the error-handling section, whose purpose is to show that
every applicable error returns in one response, so the display units in
it are incidental to the point rather than a decision. Reading a
convention out of an incidental example is what drifted twice.

Section 3's example also loses a 1000x error. The December example was
9.789 BTC against 1,000,000 USDT, two sides of the same $1M cap. #442
converted both correctly (978900000 satoshis, 1000000000000 micro-units)
but labelled the satoshi value "0.009789 BTC" in its comment. ff4cc3d
resolved the value/comment mismatch by trusting the comment, rewriting
the limit as the display string "0.009789", which is roughly $1,000
against a 1,000,000 USDT destination. The value is restored and the
comment corrected.

Six requirements weakened in ff4cc3d, each weaker than both #442 and the
source doc, are restored:

- Section 1, EVM chainId: back to must, with the reason the source doc
  gives and ff4cc3d dropped along with the obligation. Numeric chain ids
  let a newly listed EVM work the day it is added, with no new mapping
  entry.
- Section 6, pagination: back to a required start date, end date and
  record count. ff4cc3d made date filtering one of three acceptable
  approaches, so an offset-only API conformed.
- Section 6, required fields: depositAddress/payoutAddress,
  depositTxid/payoutTxid, depositTokenId/payoutTokenId and
  depositEvmChainId/payoutEvmChainId are required again.
- Section 6, chain identifier: ff4cc3d rewrote the table around
  StandardTx names and lost sourceNetwork/destinationNetwork with no
  replacement row, leaving tokenId as the only asset identifier and no
  way to tell which chain a contract address belongs to. StandardTx has
  the field as depositChainPluginId/payoutChainPluginId.
- Section 9: the authKey is Edge-generated and cryptographically random
  again. ff4cc3d replaced it with "a unique per-user identifier" and
  listed provider-created sessions as acceptable, inverting who issues
  it.
- Section 14: accepting a return URI is the requirement again, flat,
  where ff4cc3d had listed it as one of three equal options. It is not a
  choice between equivalents. Any step taking a card payment, Apple Pay
  or Google Pay has to run in the system browser, where Edge cannot
  observe the page and a registered redirect is the only route back. The
  observable signals ff4cc3d listed alongside it work only for the
  non-payment steps Edge shows in its own WebView, and the section now
  says which is which.

Every example now carries native amounts annotated with their display
equivalent, and native amounts are specified as JSON strings, since a
high-decimal asset in native units runs past the safe integer range.

AGENTS.md and .cursor/BUGBOT.md pick up the consequences. AGENTS.md names
this doc as pre-implementation reading, both in the docs list and at the
top of "Starting a new provider", where the point is that a gap you are
about to work around may be the provider's to fix rather than yours.
Both files also record what the doc is not: it states what Edge asks
partners for, not what any given provider sends. Most existing
integrations predate the request and send decimals, so the doc settles
nothing in a units argument about a specific provider, and a live
response still does.

docs/CREATING_AN_EXCHANGE_PLUGIN.md carried the same defect, in the
block plugin authors copy. Its Step 6 limit sample called
denominationToNative unconditionally and unrounded, then derived one
nativeLimit and threw it as both SwapBelowLimitError and
SwapAboveLimitError, so no single rounding direction could have been
right for it. It now selects the side, rounds a minimum up and a maximum
down, and says in a comment that the unit convention has to be confirmed
against a live response. Step 5 gains the same caveat. That sample also
failed the guide's own pre-PR checklist, which already required every
denominationToNative result to be rounded and minimums to round up.

The two documents state different things and had drifted into each
other's job. API_REQUIREMENTS.md states what Edge asks a partner to
send, so it can name one convention. CREATING_AN_EXCHANGE_PLUGIN.md
states what plugin code does with what arrives, so it cannot: compliance
is not guaranteed, and most existing integrations predate the request.

Folded in from #485, which drops its documentation commits: section 4
gains a rate-types requirement, and BUGBOT.md gains per-endpoint auth,
catalog cache expiry, and the scope a fixed-then-floating fallback is
allowed to cover. The rate-types requirement is an addition rather than
a restoration. Neither the source doc nor #442 says anything about rate
types, and it creates a new must on partners: a provider publishing rate
types per asset but not per route leaves the client attempting a type
and inferring support from the failure, which is indistinguishable from
an outage.

The StealthEX plugin on that branch confirms the Step 6 fix rather than
motivating it. It already rounds its below-limit with ceil and its
above-limit with floor, which is what the corrected sample now shows and
what the old one did not.

CHANGELOG gains entries for both documents. This repo lists doc-only
work there: two of the four Unreleased entries are doc-only today.

Both documents also get a prose pass against the house writing rules,
which neither had had. 16 em dashes are replaced with commas, colons or
sentence breaks, including one inside a code comment that a fence hid
from the linter. Gone with them: two "Additionally" openers, two "What
matters is that" forward references, three count-announcement openers,
"load-bearing", and a run of seven consecutive bullets that each carried
mid-prose bold, so none of it landed. Headings move to sentence case in
both files, and the plugin guide's opening line stops restating its own
title. Its table of contents was also missing the pre-PR checklist.

No requirement changes in that pass, every in-document anchor still
resolves, and no-slop-lint.sh returns clean on all four files.
@j0ntz j0ntz mentioned this pull request Aug 21, 2026
2 tasks
j0ntz added a commit that referenced this pull request Aug 21, 2026
ff4cc3d is the only revision of this document that relaxed anything,
and none of the changes below appear in its commit message, which
describes its scope as mapping errors to edge-core-js classes, naming
the integration points, and expanding the Reporting API section.

The reference point throughout is Paul's "2025 Exchange Provider API
Requirements" doc, which ef51604 transcribes almost verbatim.

Native units are now the single convention, stated once:

The document had drifted into per-section conventions. Section 3 asked
for display units and called itself "the one place the native-unit
preference does not apply", section 6 asked for display units citing
StandardTx's asSafeNumber typing, and sections 4 and 6 asked for native
units. Amount Representation now states outright that it governs every
amount field and that no section carries a different one, sections 3, 4
and 6 all read the same, and the asSafeNumber fact moves to where it
belongs: it constrains Edge's storage, not the provider's wire format,
and the reporting plugin already converts.

The source doc states no convention at all. Its only amounts example
sits inside the error-handling section, whose purpose is to show that
every applicable error returns in one response, so the display units in
it are incidental to the point rather than a decision. Reading a
convention out of an incidental example is what drifted twice.

Section 3's example also loses a 1000x error. The December example was
9.789 BTC against 1,000,000 USDT, two sides of the same $1M cap. #442
converted both correctly (978900000 satoshis, 1000000000000 micro-units)
but labelled the satoshi value "0.009789 BTC" in its comment. ff4cc3d
resolved the value/comment mismatch by trusting the comment, rewriting
the limit as the display string "0.009789", which is roughly $1,000
against a 1,000,000 USDT destination. The value is restored and the
comment corrected.

Six requirements weakened in ff4cc3d, each weaker than both #442 and the
source doc, are restored:

- Section 1, EVM chainId: back to must, with the reason the source doc
  gives and ff4cc3d dropped along with the obligation. Numeric chain ids
  let a newly listed EVM work the day it is added, with no new mapping
  entry.
- Section 6, pagination: back to a required start date, end date and
  record count. ff4cc3d made date filtering one of three acceptable
  approaches, so an offset-only API conformed.
- Section 6, required fields: depositAddress/payoutAddress,
  depositTxid/payoutTxid, depositTokenId/payoutTokenId and
  depositEvmChainId/payoutEvmChainId are required again.
- Section 6, chain identifier: ff4cc3d rewrote the table around
  StandardTx names and lost sourceNetwork/destinationNetwork with no
  replacement row, leaving tokenId as the only asset identifier and no
  way to tell which chain a contract address belongs to. StandardTx has
  the field as depositChainPluginId/payoutChainPluginId.
- Section 9: the authKey is Edge-generated and cryptographically random
  again. ff4cc3d replaced it with "a unique per-user identifier" and
  listed provider-created sessions as acceptable, inverting who issues
  it.
- Section 14: accepting a return URI is the requirement again, flat,
  where ff4cc3d had listed it as one of three equal options. It is not a
  choice between equivalents. Any step taking a card payment, Apple Pay
  or Google Pay has to run in the system browser, where Edge cannot
  observe the page and a registered redirect is the only route back. The
  observable signals ff4cc3d listed alongside it work only for the
  non-payment steps Edge shows in its own WebView, and the section now
  says which is which.

Every example now carries native amounts annotated with their display
equivalent, and native amounts are specified as JSON strings, since a
high-decimal asset in native units runs past the safe integer range.

AGENTS.md and .cursor/BUGBOT.md pick up the consequences. AGENTS.md names
this doc as pre-implementation reading, both in the docs list and at the
top of "Starting a new provider", where the point is that a gap you are
about to work around may be the provider's to fix rather than yours.
Both files also record what the doc is not: it states what Edge asks
partners for, not what any given provider sends. Most existing
integrations predate the request and send decimals, so the doc settles
nothing in a units argument about a specific provider, and a live
response still does.

docs/CREATING_AN_EXCHANGE_PLUGIN.md carried the same defect, in the
block plugin authors copy. Its Step 6 limit sample called
denominationToNative unconditionally and unrounded, then derived one
nativeLimit and threw it as both SwapBelowLimitError and
SwapAboveLimitError, so no single rounding direction could have been
right for it. It now selects the side, rounds a minimum up and a maximum
down, and says in a comment that the unit convention has to be confirmed
against a live response. Step 5 gains the same caveat. That sample also
failed the guide's own pre-PR checklist, which already required every
denominationToNative result to be rounded and minimums to round up.

The two documents state different things and had drifted into each
other's job. API_REQUIREMENTS.md states what Edge asks a partner to
send, so it can name one convention. CREATING_AN_EXCHANGE_PLUGIN.md
states what plugin code does with what arrives, so it cannot: compliance
is not guaranteed, and most existing integrations predate the request.

Folded in from #485, which drops its documentation commits: section 4
gains a rate-types requirement, and BUGBOT.md gains per-endpoint auth,
catalog cache expiry, and the scope a fixed-then-floating fallback is
allowed to cover. The rate-types requirement is an addition rather than
a restoration. Neither the source doc nor #442 says anything about rate
types, and it creates a new must on partners: a provider publishing rate
types per asset but not per route leaves the client attempting a type
and inferring support from the failure, which is indistinguishable from
an outage.

The StealthEX plugin on that branch confirms the Step 6 fix rather than
motivating it. It already rounds its below-limit with ceil and its
above-limit with floor, which is what the corrected sample now shows and
what the old one did not.

Both documents also get a prose pass against the house writing rules,
which neither had had. 16 em dashes are replaced with commas, colons or
sentence breaks, including one inside a code comment that a fence hid
from the linter. Gone with them: two "Additionally" openers, two "What
matters is that" forward references, three count-announcement openers,
"load-bearing", and a run of seven consecutive bullets that each carried
mid-prose bold, so none of it landed. Headings move to sentence case in
both files, and the plugin guide's opening line stops restating its own
title. Its table of contents was also missing the pre-PR checklist.

No requirement changes in that pass, every in-document anchor still
resolves, and no-slop-lint.sh returns clean on all four files.
j0ntz added a commit that referenced this pull request Aug 21, 2026
ff4cc3d is the only revision of this document that relaxed anything,
and none of the changes below appear in its commit message, which
describes its scope as mapping errors to edge-core-js classes, naming
the integration points, and expanding the Reporting API section.

The reference point throughout is Paul's "2025 Exchange Provider API
Requirements" doc, which ef51604 transcribes almost verbatim.

Native units are now the single convention, stated once:

The document had drifted into per-section conventions. Section 3 asked
for display units and called itself "the one place the native-unit
preference does not apply", section 6 asked for display units citing
StandardTx's asSafeNumber typing, and sections 4 and 6 asked for native
units. Amount Representation now states outright that it governs every
amount field and that no section carries a different one, sections 3, 4
and 6 all read the same, and the asSafeNumber fact moves to where it
belongs: it constrains Edge's storage, not the provider's wire format,
and the reporting plugin already converts.

The source doc states no convention at all. Its only amounts example
sits inside the error-handling section, whose purpose is to show that
every applicable error returns in one response, so the display units in
it are incidental to the point rather than a decision. Reading a
convention out of an incidental example is what drifted twice.

Section 3's example also loses a 1000x error. The December example was
9.789 BTC against 1,000,000 USDT, two sides of the same $1M cap. #442
converted both correctly (978900000 satoshis, 1000000000000 micro-units)
but labelled the satoshi value "0.009789 BTC" in its comment. ff4cc3d
resolved the value/comment mismatch by trusting the comment, rewriting
the limit as the display string "0.009789", which is roughly $1,000
against a 1,000,000 USDT destination. The value is restored and the
comment corrected.

Six requirements weakened in ff4cc3d, each weaker than both #442 and the
source doc, are restored:

- Section 1, EVM chainId: back to must, with the reason the source doc
  gives and ff4cc3d dropped along with the obligation. Numeric chain ids
  let a newly listed EVM work the day it is added, with no new mapping
  entry.
- Section 6, pagination: back to a required start date, end date and
  record count. ff4cc3d made date filtering one of three acceptable
  approaches, so an offset-only API conformed.
- Section 6, required fields: depositAddress/payoutAddress,
  depositTxid/payoutTxid, depositTokenId/payoutTokenId and
  depositEvmChainId/payoutEvmChainId are required again.
- Section 6, chain identifier: ff4cc3d rewrote the table around
  StandardTx names and lost sourceNetwork/destinationNetwork with no
  replacement row, leaving tokenId as the only asset identifier and no
  way to tell which chain a contract address belongs to. StandardTx has
  the field as depositChainPluginId/payoutChainPluginId.
- Section 9: the authKey is Edge-generated and cryptographically random
  again. ff4cc3d replaced it with "a unique per-user identifier" and
  listed provider-created sessions as acceptable, inverting who issues
  it.
- Section 14: accepting a return URI is the requirement again, flat,
  where ff4cc3d had listed it as one of three equal options. It is not a
  choice between equivalents. Any step taking a card payment, Apple Pay
  or Google Pay has to run in the system browser, where Edge cannot
  observe the page and a registered redirect is the only route back. The
  observable signals ff4cc3d listed alongside it work only for the
  non-payment steps Edge shows in its own WebView, and the section now
  says which is which.

Every example now carries native amounts annotated with their display
equivalent, and native amounts are specified as JSON strings, since a
high-decimal asset in native units runs past the safe integer range.

AGENTS.md and .cursor/BUGBOT.md pick up the consequences. AGENTS.md names
this doc as pre-implementation reading, both in the docs list and at the
top of "Starting a new provider", where the point is that a gap you are
about to work around may be the provider's to fix rather than yours.
Both files also record what the doc is not: it states what Edge asks
partners for, not what any given provider sends. Most existing
integrations predate the request and send decimals, so the doc settles
nothing in a units argument about a specific provider, and a live
response still does.

docs/CREATING_AN_EXCHANGE_PLUGIN.md carried the same defect, in the
block plugin authors copy. Its Step 6 limit sample called
denominationToNative unconditionally and unrounded, then derived one
nativeLimit and threw it as both SwapBelowLimitError and
SwapAboveLimitError, so no single rounding direction could have been
right for it. It now selects the side, rounds a minimum up and a maximum
down, and says in a comment that the unit convention has to be confirmed
against a live response. Step 5 gains the same caveat. That sample also
failed the guide's own pre-PR checklist, which already required every
denominationToNative result to be rounded and minimums to round up.

The two documents state different things and had drifted into each
other's job. API_REQUIREMENTS.md states what Edge asks a partner to
send, so it can name one convention. CREATING_AN_EXCHANGE_PLUGIN.md
states what plugin code does with what arrives, so it cannot: compliance
is not guaranteed, and most existing integrations predate the request.

Folded in from #485, which drops its documentation commits: section 4
gains a rate-types requirement, and BUGBOT.md gains per-endpoint auth,
catalog cache expiry, and the scope a fixed-then-floating fallback is
allowed to cover. The rate-types requirement is an addition rather than
a restoration. Neither the source doc nor #442 says anything about rate
types, and it creates a new must on partners: a provider publishing rate
types per asset but not per route leaves the client attempting a type
and inferring support from the failure, which is indistinguishable from
an outage.

The StealthEX plugin on that branch confirms the Step 6 fix rather than
motivating it. It already rounds its below-limit with ceil and its
above-limit with floor, which is what the corrected sample now shows and
what the old one did not.

Both documents also get a prose pass against the house writing rules,
which neither had had. 16 em dashes are replaced with commas, colons or
sentence breaks, including one inside a code comment that a fence hid
from the linter. Gone with them: two "Additionally" openers, two "What
matters is that" forward references, three count-announcement openers,
"load-bearing", and a run of seven consecutive bullets that each carried
mid-prose bold, so none of it landed. Headings move to sentence case in
both files, and the plugin guide's opening line stops restating its own
title. Its table of contents was also missing the pre-PR checklist.

No requirement changes in that pass, every in-document anchor still
resolves, and no-slop-lint.sh returns clean on all four files.
j0ntz added a commit that referenced this pull request Aug 21, 2026
Native units everywhere. Restores six requirements weakened in ff4cc3d.
Fixes the plugin guide's limit sample. Folds in #485's docs.
@j0ntz
j0ntz force-pushed the jon/integrate-stealthex-swap branch from 2944889 to 8ec625a Compare August 21, 2026 19:05
@j0ntz

j0ntz commented Aug 21, 2026

Copy link
Copy Markdown
Contributor Author

Adherence to #489

The plugin is written against docs/API_REQUIREMENTS.md as #489 revises it, and against the review rules in .cursor/BUGBOT.md that #489 adds alongside it.

Three of those rules came out of this integration and are in #489 rather than here:

  • per-endpoint-auth: StealthEX authenticates every v4 endpoint including the catalog, so credentials are attached per request rather than assumed global.
  • catalog-cache-expiry: the asset catalog is cached for an hour, replaced atomically, and the previous index is kept when a refresh fails. ARRR and ZANO were listed on 2026-08-14 and gone by 2026-08-18, so an unbounded cache would quote a delisted asset.
  • rate-type-fallback-scope: the fixed-then-floating fallback fires only when the fixed route is missing. A limit error or an unsupported pair is not retried, and no order exists at that point, so no attempt can create a second one.

Where #489 is stricter than the doc this plugin was first written against, the plugin already complies:

#489 requirement Plugin
Native units govern every amount field; a display-unit API must document the convention StealthEX quotes display units and documents nothing, so every amount is cleaned through a number-to-string cleaner before arithmetic, and denominationToNative converts at the boundary with integer rounding
Numeric EVM chainId (raised from should to must) Not offered by StealthEX. The static chain table in src/mappings/stealthex.ts covers it, and the gap is written up rather than coded
Per-route rate types Not offered. The scoped fallback above covers it
Max quote must not create an abandoned order The max probe resolves route, limits and addresses without posting to /v4/exchanges; only the final quote creates the order
Provider amounts bounded before signing A from quote rejects an order whose deposit amount exceeds what the user requested

The compliance and gap report is regenerated against #489 and re-verified against the live API today: https://gist.github.com/j0ntz/ae71f231a5939489cf4611325058c00c

Section 2 (order identification) passes. Sections 1, 3, 4 and 5 are gaps on the StealthEX side, section 6 is partial, and section 7 needs a written answer from StealthEX rather than a code change. Issue 2 from the review is the section 1 gap.

@j0ntz

j0ntz commented Aug 21, 2026

Copy link
Copy Markdown
Contributor Author

📸 Test evidence: account activation on an unactivated XRP wallet

xrp reserve notice

xrp reserve notice

stealthex quote fresh xrp

stealthex quote fresh xrp

stealthex swap success fresh xrp

stealthex swap success fresh xrp

fresh xrp wallet funded

fresh xrp wallet funded

fresh hedera receive

fresh hedera receive

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

@j0ntz

j0ntz commented Aug 22, 2026

Copy link
Copy Markdown
Contributor Author

📸 Test evidence: funded activation on ALGO, XLM and SOL

stealthex quote fresh algo

stealthex quote fresh algo

stealthex swap success fresh algo

stealthex swap success fresh algo

fresh algo wallet funded

fresh algo wallet funded

xlm min balance notice

xlm min balance notice

stealthex quote fresh xlm

stealthex quote fresh xlm

stealthex swap success fresh xlm

stealthex swap success fresh xlm

fresh xlm wallet funded

fresh xlm wallet funded

stealthex quote fresh sol

stealthex quote fresh sol

stealthex swap success fresh sol

stealthex swap success fresh sol

fresh sol wallet funded

fresh sol wallet funded

fresh hedera placeholder address

fresh hedera placeholder address

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

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