Conversation
The opportunistic in-range path quoted, sized, and balance-checked a fresh USDC->BRLA->USDC run before the state machine got a chance to resume a run that died mid-flow. With the in-flight USDC no longer in the wallet, that balance check failed on every cron run and the stuck state was never resumed. The out-of-range path had its own resume check; hoist a single one to the top of checkForRebalancing for both Base flows. Also stop selecting the profitable USDC->BRLA amount when the Base USDC balance cannot fund it: fall back to the standard amount instead of crashing after two quote round-trips.
Dry-run by default: prints the persisted USDC->BRLA->USDC state from Supabase Storage. With --confirm it resets the phase to idle while keeping history, for runs whose funds were reconciled manually.
…ming Resuming a persisted run in dry-run mode would write state and move funds during an invocation the spec defines as read-only, and resuming before the coverage read let funds move when the indexer read would have failed. Both were latent on the out-of-range paths before the resume was hoisted.
The reset script and a live run write the same Supabase object without locking, and the script's closing message contradicted the README's reconcile-first order.
Continuing into the fresh evaluation quoted and sized a run that assumes the in-flight USDC is still in the wallet, and logged "no rebalancing needed" while a run was paused.
The affordability gate read the wallet balance even when the policy mode is off, which the spec defines as returning before any balance read. The resume-before-fresh-evaluation ordering, the dry-run pause, the --restart bypass and the profitable-amount gate had no automated coverage because index.ts runs the cycle at import. Move that orchestration into rebalanceCycle.ts behind injected dependencies so a later refactor cannot silently reorder it.
…-flight Resume in-flight rebalancer runs before sizing a fresh one
A non-JSON error response from Squidrouter (a Cloudflare or load-balancer
error page) was collapsed to `{}` and the HTTP status was not logged, so
production logs showed "Error fetching route from Squidrouter API: {}"
with no way to tell what the upstream actually returned.
The nightly e2e smoke test failed because production's POST /v1/quotes answered 500: Squidrouter's /route endpoint returned a non-JSON 5xx from the gateway in front of it. Production logs show this ~1-2 times per hour, each one a user-facing 500, while the same request succeeds a second later. Extend the existing retry-once path (previously only for rate limits) to a 5xx whose body is not Squid's JSON error shape. Squid's own deterministic errors, such as low liquidity, keep failing fast without a retry.
…e read `response.text()` rejects when the gateway truncates the error stream. That escaped as a bare TypeError, losing the HTTP status and skipping the gateway retry, whereas the previous `.json().catch()` still produced an HttpError. Treat an unreadable body as empty so the status is logged and a 5xx is still retried.
…y spec The Squid integration spec said only 429s are retried and every other error fails fast. It also described the 429 handling as exponential backoff, whereas `getRoute` retries once after the advertised retryAfter.
…retry Retry Squidrouter route requests that fail at the gateway
The hook is a leftover from the standalone rebalancer repo. Run from apps/rebalancer, husky 9 stops at ".git can't be found", so it never installed anything; the root prepare already installs the hooks for the whole repo. Because the rebalancer does not depend on husky, bun can also run the hook before the root's binary is linked, which fails a cold `bun install --frozen-lockfile` with exit 127, as seen in CI and in `bun bootstrap:worktree`.
…pare Remove the rebalancer's redundant husky prepare hook
The API rejects with "Invalid pixKey or receiverTaxId." (trailing period), so the exact-match comparison never fired and server-side rejections surfaced as a generic VortexSdkError.
GET /v1/ramp/{id}?showUnsignedTxs=true returned the raw unsignedTxs, so a
client could fetch the user's source-of-funds transactions for a SELL ramp
before every ephemeral presign was received and validated, bypassing the
gate that register and update enforce.
…apping Map the API's invalid pixKey rejection to InvalidPixKeyError in the SDK
The gold unit tests cover quote validation, recovery checkpoints and gas preflight, but no root script ran them. Wiring them into .github/workflows/ci.yml is left to a maintainer (see the PR description). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The standalone gold.satoshipay.io release sent nosniff, referrer and permissions headers; the Netlify-served /pt-br/gold/ sends none and can be framed by any site while it signs transactions and collects PIX keys. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The hero PNG was 1.96 MB and the wordmark 273 KB on the first screen; the WebP is 209 KB and the wordmark keeps 3x its largest display size. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The chart only fetched 42 days, so 1A and Tudo showed six weeks, and the change badge always showed the 42-day change whatever the period. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One failed status request ended polling and sent the buyer to the support escalation screen although the ramp kept running server-side. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The countdown decremented once per timer tick, and browsers pause timers while the buyer is in the banking app, so it showed time that had passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The fee disclosure and legal notes used 11px #837d74 on #fffdf9 (4.0:1). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Gold asked /v1/ramp-info whether the user passed KYC, but that route only accepts API credentials and reports on their owner, never on the OTP session. Production sets no public key, so every buy stopped after the e-mail code with "A public or secret API credential is required" and every sell looped back to the code. /v1/brl/getUser resolves the session user's approved Avenia account; skip KYC only on CONFIRMED, as the widget does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GET /v1/ramp/:id returns unsignedTxs only with showUnsignedTxs=true and the SDK status call never sends it, so "Continuar esta venda" always failed with "Não foi possível recuperar as confirmações" after a declined wallet prompt, a reload or a receipt timeout, although the copy invites the user to retry. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both buttons resume the stored operation, and it was only cleared on a terminal status. The API never expires an unstarted ramp, and a failed sell was never cleared, so one abandoned PIX or failed sell locked the wallet out of buying and selling on that device for good. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Status responses have no expiresAt, so a resumed PIX screen invented a fresh ten-minute countdown; start is refused 15 minutes after creation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A failed Squid fetch installed the static-only fallback and set isLoaded, which turned every later initializeEvmTokens() call into a no-op. Squid-only tokens such as PAXG then stayed unlisted and unquotable until the process restarted, and the fetch had no timeout, so a hung request stalled boot. Track whether the Squid list itself loaded, guard on that instead, resolve to whether it did, and bound the fetch at 10 s. The static fallback on failure is unchanged.
Boot awaited a single initializeEvmTokens() call, so one failed Squid request left the API on static tokens until the next restart. Wait for the first attempt only, then retry every 60 s until the list loads.
The business EUR onramp account and its deposit events share the EUR provider activation that production is still waiting on, so they carry the same sandbox note as EUR buys.
One failed status request ended the check, an expired attempt polled for five minutes, and a rejection showed Avenia's English reason code and sent the user back to the old attempt. Tolerate transient and reconciliation errors, stop on every final state, explain it in Portuguese and return to the form for a new attempt; stop polling when the modal closes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The form accepted any 11 digits, so a typo created a subaccount bound to the wrong CPF, and the API is about to reject such CPFs outright. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The keys were stored for every ramp and never removed. A completed ramp's ephemeral accounts are swept with presigned cleanup transactions, so only failed ramps' keys can still recover funds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The page stayed a spinner until the Privy chunk (about 750 kB gzipped) loaded and Privy reported ready. App now renders outside PrivyProvider, which hands its auth state up, so the landing paints from the entry chunk and only the sign-in buttons wait; returning users keep the loader so the landing does not flash before their dashboard. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review found the own-account path sent EUR users to production keys, skipped EUR wallet linking and IBAN provisioning, omitted the country for the payout-account lookup, did not say direct clients sign the ephemeral transactions, and assumed verification creates the payout account.
The API derives the EUR permit owner from the provider binding and ignores walletAddress, which only the SDK uses; accounts holding both legal types must also send customerType.
The sell example reused the buy quote, so registerRamp dispatched it to the onramp handler, and it indexed an empty account list for sellers who had not added a pay-out account.
listDomesticFiatAccounts takes the exported DomesticCountry string enum, so the "MX" literal fails TypeScript type-checking.
…own-account Document the dashboard route to API keys and an own-account ramp path
The API refuses to record or start a ramp 15 minutes after registration. A user-wallet transfer or Squid swap broadcast later moves the funds to the ephemeral of a ramp that can never start, leaving them for manual recovery. Refuse each broadcast once less than four minutes remain; typed-data permits move nothing until Vortex executes them, so they stay unguarded.
…guard Stop widget sell broadcasts after the ramp start window closes
Unblock gold purchases and harden the gold app
The modal re-ran its focus effect whenever onClose changed identity, and every caller passes a new function on each render. The balance poll re-renders the app every 30 seconds, so focus jumped from the field being typed in to the close button, where the next Space or Enter closed the flow and discarded the form. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Closing the modal aborted the wait between polls but not a status request already in flight, so a late approval still called onApproved and, in the sell flow, fetched a new quote for a flow that was gone. The abort path had no test at all; cover both pollers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A 404 or 409 from the KYC status check that never cleared reached the screen as the API's English text after five polls, and a new attempt the API refused (Avenia did not mark the old one retryable) showed an English 409 behind a button that only resubmitted it. Both now say in Portuguese what happened and point to support with the request code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Avenia's tax_id reason means the CPF was not found, not that it differs
from the document, and the new messages drifted from the app's wording
("de novo", the mobile-only "toque em", advice Avenia does not give).
Assert every reason's message and that no reason code leaks through.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A rejection returns to the long form with its explanation below the fields, out of view on a phone and not announced; it now scrolls into view as an alert. After approval the step no longer stays on the checking spinner: when the sell quote still finds the account unapproved while Avenia syncs, the user can continue instead of waiting forever. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The shared CPF check rejects digit runs, and 01234567890 is the one run besides repeated digits whose check digits are valid, so gold accepted it. The field error now names its input for screen readers and sits under the field instead of centred with the OTP step's margin. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Google sign-in returns to the page with privy_oauth_code before Privy has stored its session, so the landing flashed by again; the session check now counts that return and lives in a tested helper. A Privy that failed to initialize left a disabled button with no explanation, and a returning user on an endless loader; the error Privy reports now ends the wait and says to reload. The sign-in buttons show that Privy is loading, and the Privy subtree is kept in state because useMemo may be discarded. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Storing and deleting ephemeral keys skipped db.close() when the transaction rejected, and the caller swallows delete errors, so a failure left a connection open that blocks a later upgrade of the store. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-retry Retry the Squid token list when it fails to load at boot
Gold: faster landing, sturdier KYC polling, CPF check and key cleanup
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.