Skip to content

fix: request EXACT_OUTPUT for HyperCore perps deposits so the full margin arrives - #9751

Open
abretonc7s wants to merge 7 commits into
mainfrom
TAT-3400-fix-trade-with-token-has-20-failur
Open

fix: request EXACT_OUTPUT for HyperCore perps deposits so the full margin arrives#9751
abretonc7s wants to merge 7 commits into
mainfrom
TAT-3400-fix-trade-with-token-has-20-failur

Conversation

@abretonc7s

@abretonc7s abretonc7s commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Explanation

Trade-with-token perps deposits failed intermittently with insufficient margin on the order leg.

Root cause. Core requested HyperCore perps deposits from Relay with tradeType: EXPECTED_OUTPUT, which only guarantees target * (1 - slippage) arrives at the destination. Clients size these deposits at exactly marginRequired and immediately place an order needing that same margin, so any slippage inside the 0.5% band left the account short and the order was rejected.

HyperCore deposits end up on this path because skipDelegation is true for them, so they never take the delegation path that already used EXACT_OUTPUT.

Fix. Request EXACT_OUTPUT for HyperCore deposits, so Relay guarantees the full target arrives.

Verified against the live Relay API

Same quote request Core builds, across deposit sizes — no mocks. "Guaranteed" is Relay's currencyOut.minimumAmount; "source cost" is the Arbitrum USDC the user spends (currencyIn).

margin guaranteed before guaranteed after source cost
$10 $9.95 — short $0.05 $10.00 $11.20 → $11.20
$20 $19.90 — short $0.10 $20.00 $21.20 → $21.20
$50 $49.75 — short $0.25 $50.00 $51.20 → $51.20
$100 $99.50 — short $0.50 $100.00 $101.20 → $101.20
$500 $497.50 — short $2.50 $500.00 $501.20 → $501.20
$1000 $995.00 — short $5.00 $1000.00 $1001.20 → $1001.20

The shortfall reproduced at all six sizes before the change and none after.

Relay quotes the same input amount for both trade types, so the user spends exactly what they spend today — this buys a delivery guarantee at no extra cost, rather than padding the deposit.

Client impact

No exported API or type changes. Mobile HyperCore deposits now deliver the full requested margin at the same source cost. Extension does not yet create perpsDepositAndOrder, so it is unaffected today but inherits the fix when trade-with-token ships there.

References

  • Jira: TAT-3400
  • Epic: TAT-2657 Order reliability bugs
  • Related: TAT-2720 (deposit success rate, Mobile), TAT-3343 / TAT-3344 (order-path reliability siblings)

Checklist

  • I've updated the test suite for new or updated code as appropriate
  • I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate
  • I've communicated my changes to consumers by updating changelogs for packages I've changed
  • I've introduced breaking changes in this PR and have prepared draft pull requests for clients and consumer packages to resolve them

Note

Medium Risk
Changes Relay trade-type selection for HyperCore deposit quotes, which affects real money perps deposit-and-order flows but is narrowly scoped with tests and no public API changes.

Overview
Fixes intermittent insufficient margin failures on trade-with-token perps flows by changing how Relay quotes are built for HyperCore USDC deposits.

Relay quote requests now send tradeType: EXACT_OUTPUT (instead of EXPECTED_OUTPUT) when the normalized destination is HyperCore USDC and the flow is not max-amount or post-quote. EXPECTED_OUTPUT only guaranteed delivery within slippage, so deposits sized to exact margin could land short and the follow-on order would fail; EXACT_OUTPUT guarantees the full target arrives. EXACT_INPUT behavior is unchanged for max and post-quote paths.

Implementation adds isHypercoreDeposit and getTradeType in relay-quotes.ts, with tests asserting Hyperliquid perpsDepositAndOrder quotes use EXACT_OUTPUT while other targets still use EXPECTED_OUTPUT. Changelog documents the fix; no exported API changes.

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

Unactivated trade-with-token deposits sized to exact margin left users short after HyperLiquid's ~$1 first-credit fee, so the auto-placed order failed.

Co-authored-by: Cursor <cursoragent@cursor.com>
@abretonc7s

abretonc7s commented Aug 1, 2026

Copy link
Copy Markdown
Contributor Author

Automated fix-bug run — TAT-3400

Metric Value
Run 2ade7db2
Duration ?
Model cursor/cursor-grok-4.5-high
Nudges 0
Worker report

Explanation

Trade-with-token deposits size the Relay quote to exact marginRequired. For an unactivated HyperCore account, HyperLiquid deducts a one-time ~$1 USDC activation fee from the first inbound credit, so available margin is short and the auto-placed order fails (insufficient margin on the order leg).

Core already reserved this fee on HyperLiquid withdrawals. This change mirrors that for perps deposits: after Arbitrum USDC is normalized to HyperCore, applyHyperliquidActivationFee increases targetAmountMinimum by the configured activation fee for unactivated accounts and records hyperliquidActivationFeeUsd for fee display. Activated accounts are unchanged. Withdrawals remain behind the existing remote feature flag.

No exported API or type surface changes. Clients need no coordinated bump beyond consuming the next @metamask/transaction-pay-controller release.

References

  • Jira: TAT-3400
  • Epic: TAT-2657 Order reliability bugs
  • Related: TAT-2720 (deposit success rate, Mobile), TAT-3343 / TAT-3344 (order-path reliability siblings)

Checklist

  • I've updated the test suite for new or updated code as appropriate
  • I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate
  • I've communicated my changes to consumers by updating changelogs for packages I've changed
  • I've introduced breaking changes in this PR and have prepared draft pull requests for clients and consumer packages to resolve them

@abretonc7s abretonc7s changed the title chore: prepare farmslot publication pkg-2ade7db2-msaheutl fix: trade with token has 20% failure rate Aug 1, 2026
@abretonc7s
abretonc7s marked this pull request as ready for review August 1, 2026 14:44
@abretonc7s
abretonc7s requested review from a team as code owners August 1, 2026 14:44
abretonc7s and others added 2 commits August 1, 2026 23:04
Link the changelog entry to PR #9751 and cover the HyperLiquid-source deposit-target branch so package coverage thresholds pass.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@abretonc7s
abretonc7s marked this pull request as draft August 3, 2026 09:02
Relay quotes for HyperCore perps deposits used tradeType EXPECTED_OUTPUT,
which only guarantees `target * (1 - slippage)` arrives on the destination.
Clients size these deposits at the exact margin required and immediately
place an order needing that same margin, so any slippage inside the 0.5%
band left the order short and it failed with insufficient margin.

HyperCore deposits reach this state because `skipDelegation` is true for
them, so they never take the delegation path that already used
EXACT_OUTPUT. Request EXACT_OUTPUT for them directly instead.

Verified against the live Relay API from $10 to $1000: the guaranteed
minimum previously fell short at every size (-$0.05 to -$5.00) and now
equals the requested target, at identical source cost.

This replaces the previous activation-fee approach, which assumed
HyperLiquid deducts ~$1 from the first inbound credit. That premise was
not supported by mainnet ledger data, and a flat $1 both over-charged
small deposits and under-funded ones above ~$200.
@abretonc7s abretonc7s changed the title fix: trade with token has 20% failure rate fix: request EXACT_OUTPUT for HyperCore perps deposits so the full margin arrives Aug 3, 2026
@abretonc7s
abretonc7s marked this pull request as ready for review August 3, 2026 10:49
…th-token-has-20-failur

# Conflicts:
#	packages/transaction-pay-controller/CHANGELOG.md
aganglada
aganglada previously approved these changes Aug 3, 2026

@aganglada aganglada left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Summary

Verdict: Approve — the fix is correct, minimal, and well-supported. A few small nits below.

What it does

HyperCore perps deposits go through Relay with skipDelegation = true (because isHypercore is set in processTransactions). Non-HyperCore flows that don't skip delegation already get EXACT_OUTPUT via the delegation path. HyperCore deposits were the gap: they stayed on EXPECTED_OUTPUT, which only guarantees target × (1 − slippage).

Since clients size deposits to exact marginRequired and immediately place an order needing that margin, the 0.5% slippage band could leave the account short and fail the order leg.

The fix detects normalized HyperCore USDC deposits and requests EXACT_OUTPUT instead.


What works well

  1. Root cause is right. The delegation bypass explains why this path differed from other perps flows. The live Relay API table in the PR description ($10–$1000) is strong evidence: guaranteed minimum was short before, exact after, same source cost.

  2. Narrow, safe change. No API/type changes. getTradeType is a clean extraction. isHypercoreDeposit correctly operates on the normalized request (post Arbitrum→HyperCore rewrite) and excludes withdrawals via !request.isHyperliquidSource.

  3. Better than the activation-fee approach. The pivot away from a flat ~$1 top-up is justified — it would over-charge small deposits and under-fund large ones. EXACT_OUTPUT addresses the actual slippage shortfall without padding.

  4. Tests cover the important cases. HyperCore deposit → EXACT_OUTPUT; generic quote → EXPECTED_OUTPUT unchanged.

  5. Aligns with Across. Across already uses exactOutput for target-amount flows; Relay was the outlier here.


Issues / nits

1. Orphaned JSDoc (should fix)

The PR inserts isHypercoreDeposit and getTradeType between the existing normalizeRequest JSDoc and the function itself. Move the helpers above the normalizeRequest block, or restore the JSDoc directly above normalizeRequest.

2. Stale inline comment

The comment in getSingleQuote still says "For regular flows with a target amount, use EXPECTED_OUTPUT." HyperCore deposits are now a third case (EXACT_OUTPUT). Worth a one-line update so future readers don't reintroduce the bug.

3. Test could be tighter

The new test uses perpsDepositAndOrder, but the existing "updates request if Arbitrum deposit to Hyperliquid" test already covers perpsDeposit normalization. Consider adding tradeType: 'EXACT_OUTPUT' to that test's objectContaining assertion — or at least add a perpsDeposit case so both PERPS_DEPOSIT_TYPES are covered explicitly.

4. isHypercoreDeposit is broader than perps-only

It keys off normalized target (HyperCore USDC), not transaction.type. Any Relay quote depositing into HyperCore USDC gets EXACT_OUTPUT, not only perps deposits. That's probably fine — a stricter delivery guarantee is reasonable — but worth a conscious choice.


Risks considered (non-blocking)

Risk Assessment
Higher source cost with EXACT_OUTPUT PR shows identical currencyIn before/after across sizes — not a concern if Relay behavior holds
Post-quote / max-amount flows Correctly excluded: useExactInput takes precedence
HyperLiquid withdrawals (isHyperliquidSource) Correctly excluded: target is not HyperCore USDC
Server strategy same bug Server still uses ExpectedOutput for HyperCore, but trade-with-token goes through Relay today — out of scope, worth a follow-up if Server is ever routed for perps

Suggested test plan before merge

  • Unit tests pass
  • End-to-end trade-with-token deposit at ~$20 margin on Mobile (the reported failure size)
  • Confirm currencyOut.minimumAmount === target in the Relay quote response for a HyperCore deposit
  • Regression: standard non-perps Relay quotes still use EXPECTED_OUTPUT

Bottom line: Ship it after fixing the JSDoc placement. The approach is the right fix for TAT-3400 — you're buying a delivery guarantee at no extra source cost, which is exactly what margin-sized deposits need.

isMaxAmount and isPostQuote are both optional, so useExactInput is
boolean | undefined and the build failed on the narrowed parameter type.
@abretonc7s

Copy link
Copy Markdown
Contributor Author

Automated pr-complete run — #9751

Metric Value
Run 2b7872ba
Duration ?
Model claude/opus
Nudges 0
Worker report

PR #9751 — Comment Triage Report

PR: #9751
Branch: TAT-3400-fix-trade-with-token-has-20-failur @ 33cb4c61b (pushed)
PR state: DRAFT — original approach reverted and replaced.

Context reload

Inherited context: present (inputs/inherited-context.json, family 2ade7db2-1572-4706-ad88-74eae941451f, root TAT-3400).

Resolved inherited artifacts: TASK.md, report.md, learnings.md, recipe.json, recipe-quality.json, recipe-coverage.md, evidence-manifest.json. Missing: recipe library (never produced by parent run).

Summary of prior run:

  • Root cause: trade-with-token sizes the Relay quote to exact marginRequired; HyperLiquid deducts a one-time ~$1 USDC activation fee from the first inbound HyperCore credit, so an unactivated account is under-funded and the auto-placed order fails on insufficient margin. Core already reserved this fee for withdrawals, not deposits.
  • Fix: applyHyperliquidActivationFee in packages/transaction-pay-controller/src/strategy/relay/hyperliquid-activation.ts now bumps targetAmountMinimum for unactivated HyperCore perps deposits (perpsDeposit / perpsDepositAndOrder).
  • Prior validation: targeted Jest 31 passed; recipe proof green → revert → red → restore → green; mm-harness check diff --profile fast pass; build skipped (no export/type changes).

Recipe provenance: RECIPE_SOURCE: family-inherited → trusted. artifacts/recipe.json inspected: only git show/cp/mkdir/yarn jest steps, no network, credential, or destructive operations. Approved for re-run (note: node cmds hard-code the parent task dir temp/tasks/fix/tat-3400-0801-222814/).

Live comment fetch

Source Count Detail
Inline review comments (pulls/9751/comments) 0 none
Issue comments (issues/9751/comments) 1 abretonc7s — automated fix-bug run summary (informational, mirrors PR body)
Reviews 0 reviewDecision: REVIEW_REQUIRED
Cursor Bugbot check SUCCESS, no findings posted
CI status all checks SUCCESS (or SKIPPED for release-only jobs) mergeStateStatus: BLOCKED only because human review is still required

Triage

# Source Author Classification Rationale / Action
1 issue comment 5151910579 abretonc7s OUT_OF_SCOPE (informational) Automated run summary + PR checklist. No requested change. No code action.

No REAL findings. No code fixes applied in this session.

Core compatibility assessment

Changed files touch packages/transaction-pay-controller/src only — no packages/perps-controller/src, no package manifests, no lockfile.

  • Exported surface: unchanged. applyHyperliquidActivationFee keeps its signature; no new/renamed/removed exports, no messenger action or state type changes, no error-code changes.
  • Behavioral change: for unactivated HyperCore accounts on perpsDeposit / perpsDepositAndOrder, targetAmountMinimum increases by the activation fee. Activated accounts and withdrawals are unchanged.
  • Client impact (Mobile/Extension): non-breaking. Users see a slightly larger required source amount for a first HyperCore deposit; the fee surfaces through the existing hyperliquidActivationFeeUsd field already consumed for fee display. No coordinated client bump needed beyond consuming the next @metamask/transaction-pay-controller release.
  • Changelog: Unreleased → Fixed entry present and validated by CI (Validate changelog + Check changelog both green).

Validation this session

Check Result
mm-harness check diff --profile fast pass — policy-suppressions, eslint, oxfmt, jest all pass; typecheck skipped by profile (artifacts/check-diff/validation-summary.json)
Recipe re-run (green → revert → red → restore → green) pass (artifacts/recipe-rerun/summary.json, status: pass)
Targeted Jest (hyperliquid-activation.test.ts) 32 passed, 32 total
Package build skipped — no exported types, package exports, or build-wiring changes
yarn install --immutable skipped — no manifest or lockfile changes

Recipe pin correction: the inherited recipe asserted 31 passed, 31 total, which was authored before commits c7b43344a / c7eeb0682 added a test. The suite is now 32/32 green. The pin in artifacts/recipe.json (and the rerun copy) was updated 31 → 32; this is a stale-pin fix, not a regression, and the anti-skip guarantee is preserved. The rerun copy also rewrites the parent run's hard-coded temp/tasks/fix/tat-3400-0801-222814/ evidence paths to this task dir.


Update — original approach reverted and replaced

Triage above found no actionable reviewer comments, and the PR was green. The substantive
problem was not in the comments but in the change itself: its premise was never verified.

Live mainnet validation (mainnet-findings.md, mainnet-proof/) showed the activation-fee
theory was unsupported, and found the actual root cause — Relay EXPECTED_OUTPUT only
guarantees target * (1 - slippage) on HyperCore deposits.

  • PR converted to draft.
  • Activation-fee change reverted (hyperliquid-activation.ts, its test, feature-flags.ts
    back to origin/main).
  • Replacement fix committed as 33cb4c61b and pushed: request EXACT_OUTPUT for HyperCore
    deposits in relay-quotes.ts.
  • PR description rewritten to describe the real cause, the live-Relay evidence table, and why
    the original approach was reverted.

Core compatibility assessment (revised)

Still non-breaking and still confined to packages/transaction-pay-controller/src. No exported
symbol, messenger action/event, state type, or error code changed; getTradeType and
isHypercoreDeposit are module-private. The behavioural change is that HyperCore perps deposit
quotes now use tradeType: EXACT_OUTPUT, so the full requested margin is guaranteed to arrive
— at identical source cost, verified live at six deposit sizes.

Extension does not yet create perpsDepositAndOrder, so it is unaffected today and inherits
the fix when trade-with-token ships there. Mobile benefits immediately with no client change.

Validation (revised)

Check Result
Targeted Jest (relay-quotes, hyperliquid-activation) 168 passed
Fail-closed reverting relay-quotes.ts → 1 failed (Expected "EXACT_OUTPUT")
mm-harness check diff --profile fast pass
Live Relay proof (prove-fix.mjs) pass — bug 6/6, fix 6/6, exit 0
Inherited recipe (recipe.json) obsolete — it asserts the reverted activation-fee behaviour

@abretonc7s

Copy link
Copy Markdown
Contributor Author

Automated update-branch run — #9751

Metric Value
Run 03a02822
Duration ?
Model claude/opus
Nudges 0
Worker report

Update-Branch Report — PR #9751

PR: #9751fix: request EXACT_OUTPUT for HyperCore perps deposits so the full margin arrives
Branch: TAT-3400-fix-trade-with-token-has-20-failur
Base: main

Selected strategy

merge — a merge commit plus a regular (non-force) push.

BRANCH_UPDATE_STRATEGY in the task block requested rebase, which this pack does not
permit. Per the pack rules, the requested strategy was overridden and the branch was updated
with git merge origin/main. No rebase was performed and no force-push was used.

What main brought in

Branch was 2 commits behind:

Commit Description
284c4f147 Release/1169.0.0 (#9758)
d62554e43 feat: validate eth_sendTransaction / eth_signTransaction params (#9482)

62 files changed on the incoming side, mostly release version bumps across package manifests,
per-package changelogs, and yarn.lock.

Conflicts resolved

One conflict: packages/transaction-pay-controller/CHANGELOG.md.

Both sides added a section under the same ## [Unreleased] heading:

Resolution: kept both, ordered Changed before Fixed per the Keep a Changelog category
order mandated in AGENTS.md. No entry was dropped, reworded, or reassigned to a released
version. No product decision was involved, so no blocked signal was warranted.

Resolved Unreleased section:

## [Unreleased]

### Changed

- Bump `@metamask/network-controller` from `^35.0.0` to `^35.0.1` ([#9758](...))

### Fixed

- Request `EXACT_OUTPUT` instead of `EXPECTED_OUTPUT` from Relay for HyperCore perps
  deposits, so the full deposit target is guaranteed to arrive ([#9751](...))
  - `EXPECTED_OUTPUT` only guarantees `target * (1 - slippage)` on the destination, ...

Files changed

Conflict resolution touched exactly one file: packages/transaction-pay-controller/CHANGELOG.md.

The PR's own diff against main is unchanged by this run:

File +/-
packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts +44 / −1
packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts +52
packages/transaction-pay-controller/CHANGELOG.md +5

Merge commit: 7303a1e13.

Downstream compatibility

No risk introduced by this run.

  • Conflict resolution was changelog-only — no source, no exported types, no package exports.
  • packages/perps-controller has zero src changes from the merge; it received only a
    version bump in package.json and a release entry in its CHANGELOG.md. Its public API is
    untouched, so no Mobile/Extension breaking-change risk arises here. Package build/tests were
    therefore not run for it, per the bounded-validation guidance in CLAUDE.local.md.
  • The PR's own change remains non-breaking: getTradeType and isHypercoreDeposit are
    module-private, and no exported symbol, messenger action/event, state type, or error code
    changed.

Validation

Check Result
Targeted Jest — src/strategy/relay/relay-quotes.test.ts (the conflict-adjacent suite for this PR) 142 passed, 1 suite passed
changelog:validate (@metamask/transaction-pay-controller) pass (exit 0)
yarn install --immutable pass — lockfile consistent after the release merge; yarn.lock clean afterwards
Package build not run — conflict resolution touched no exported types, package exports, or build wiring
Working tree after commit clean

Push command used

git push origin TAT-3400-fix-trade-with-token-has-20-failur

Regular push, no --force, no --force-with-lease. Only the PR branch was pushed; main was
never written to.

Post-push verification

Check Result
Push 914a41696..7303a1e13 — regular push, PR branch only
gh pr view 9751 --json mergeable MERGEABLE
mergeStateStatus BLOCKED — human review still required; not a conflict or CI problem
GitHub CI on 7303a1e13 41 SUCCESS, 5 SKIPPED, 0 failures

Validation recipe — replaced, not just re-run

The task dir was seeded with an inherited artifacts/recipe.json titled "HyperCore deposit
activation fee top-up"
. That recipe proves the behaviour this PR reverted, so running it
would have failed, and shipping it as evidence would have asserted something untrue about the
current branch. It was replaced rather than re-run (the obsolete copy is not carried forward).

The new artifacts/recipe.json proves the change actually on the branch, and adds a live-API
leg the previous one lacked:

Proof target How
hypercore_uses_exact_output targeted Jest — HyperCore deposit quote sends EXACT_OUTPUT
other_targets_unchanged targeted Jest — non-HyperCore quotes still send EXPECTED_OUTPUT
live_relay_guarantees_margin live api.relay.link across $10–$1000: guaranteed floor short by $0.05–$5.00 under EXPECTED_OUTPUT (6/6), equal to target under EXACT_OUTPUT (6/6), identical source cost
fail_closed revert relay-quotes.ts to origin/mainExpected: "EXACT_OUTPUT" / Received: "EXPECTED_OUTPUT", exit 1; restore → 142/142 green

Run result: artifacts/recipe-run/summary.jsonstatus: pass. Working tree verified
clean afterwards (teardown restored the implementation).

Supporting artifacts: recipe-coverage.md, recipe-quality.json, evidence-manifest.json,
and evidence/ transcripts.

Result

Branch updated to include main and pushed. PR is mergeable with CI fully green. No product
decision or breaking client change arose from the merge, so no blocked signal was needed.

The one judgement call beyond mechanical merge work: the inherited recipe was obsolete and was
replaced with one that proves the current fix. Flagged here because it is a change to packaged
evidence, not to the branch.

@OGPoyraz
OGPoyraz requested review from dan437 and removed request for jpuri August 3, 2026 13:34
@abretonc7s
abretonc7s disabled auto-merge August 3, 2026 14:52
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.

4 participants