Skip to content

Batch and cache Alchemy price API requests to reduce quota usage - #2593

Merged
MusabShakeel576 merged 4 commits into
masterfrom
claude/alchemy-threshold-limit-emails-k9buga
Sep 28, 2026
Merged

MusabShakeel576 merged 4 commits into
masterfrom
claude/alchemy-threshold-limit-emails-k9buga

Conversation

@MusabShakeel576

@MusabShakeel576 MusabShakeel576 commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Refactored token price fetching from Alchemy to batch multiple lookups into single requests and cache results, reducing API quota consumption from ~40 requests per balance refresh to just 2-3 requests total.

Key Changes

  • New BatchedLoader utility (lib/batchedLoader.ts): Generic batching and caching layer that merges lookups started in the same tick into minimal upstream requests, with configurable TTLs, stale-value handling, and shared rate-limit pausing across loaders.

  • Refactored Alchemy price fetching (lib/api.ts):

    • Split fetchTokenPriceUsd into fetchTokenPricesBySymbol (batch up to 25 symbols per request) and kept the original as a wrapper
    • Refactored fetchTokenPricesByAddress to use batching with network-aware chunking (respects 3-network limit per request)
    • Both endpoints now share a cooldown to pause all requests when rate-limited (429), preventing quota waste
    • Added clearAlchemyPriceCache() for test isolation
    • Implemented intelligent caching: 60s TTL for hits, 5min for misses, 15s for errors, 10min max staleness
  • Updated balance fetching (hooks/useBalances.ts):

    • Changed from per-symbol requests to single batched call via fetchTokenPricesBySymbol
    • Simplified error handling (batching layer never rejects)
    • Added comments explaining the quota optimization
  • Optimized viem client usage (lib/wagmi.ts):

    • Reuse public clients per chain instead of creating new ones per call
    • Enabled viem's multicall batching to merge contract reads into single RPC calls
  • Polling intervals unchanged from master: balances every 5s, native prices every 5s, vaults every 3s. An earlier commit slowed these down, and 0de3947 restores them: price lookups are now cached for a minute (in the app and by the backend), so polling this often no longer costs Prices API requests.

  • Comprehensive test coverage (lib/__tests__/batchedLoader.test.ts, lib/__tests__/alchemyTokenPrices.test.ts):

    • Tests for batching, caching, TTL expiration, rate-limit handling, and error recovery
    • Tests for symbol encoding, network limits, and price reuse

Implementation Details

  • The batching layer uses a tick-based flush mechanism: all lookups started in the same JavaScript tick are merged into one request
  • Rate-limit pauses are shared via a Cooldown object, allowing symbol and address loaders to pause each other when either hits a 429
  • Prices are cached by symbol (upper-cased to match Alchemy's echo) and by chainId:address key
  • Stale values are kept and served when refreshes fail, up to a 10-minute maximum age
  • The loader never rejects, returning empty results on errors so fallback price sources can be tried

Follow-up: prices from the backend's shared cache

  • Price lookups, by symbol and by address, now ask the backend first (POST /accounts/v1/prices, added in Solid-Money/solid-backend#1871). It serves one cache shared by every user, so open copies of the app no longer each spend this key's token_price quota.
  • When the backend can't answer, the app asks Alchemy directly as before and skips the backend for 5 minutes. That covers:
    • a backend without the route yet (404)
    • an expired session (401)
    • the per-user limit (429)
    • a token the backend can't price right now (503)
    • any other error, a 6 s timeout, or an unexpected body
  • Only what the route accepts is sent to it: symbols of up to 64 characters, well-formed addresses, and only the chains the backend prices (BACKEND_PRICE_CHAIN_IDS). A chain added to ALCHEMY_NETWORKS before the backend has it (for example by an OTA) is still priced from Alchemy, instead of coming back unpriced and being cached as a miss.
  • Safe across app versions:
    • The change is JS-only, so it ships by OTA under the appVersion runtime policy.
    • No query cache is persisted, so no stored data changes shape.
    • Auth reuses the existing cookie on web and Bearer token on native.

Tested:

  • alchemyTokenPrices.test.ts (32 tests) covers the backend path, each fallback, the pause and its expiry, input filtering, and the uncovered-chain case.
  • Lint and Prettier are clean, and tsc reports only the 9 errors that were already there.
  • The full suite passes apart from the 2 skipTheLine.test.ts tests, which fail the same way without these changes.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UMuZxhszcH7Kc5s7dc2NNq


&lt;source media="(prefers-color-scheme: dark)" srcset="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v2.svg"&gt;&lt;source media="(prefers-color-scheme: light)" srcset="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-light-v2.svg"&gt;&lt;img alt="View with [code]smith" src="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v2.svg"&gt; <a href="https://backend.blacksmith.sh/track/enable-autofix?expires=1793102576&installation_model_id=437840&pr_number=2593&ref=codesmith_pr_footer&repository=Solid-Money%2Fsolid-ui&return_to=https%3A%2F%2Fgithub.com%2FSolid-Money%2Fsolid-ui%2Fpull%2F2593&signature=df3bb7c772bedc96f5b419c2a2dcb7bcd75d5cf70de878f54abbbe4f02f5b58f"&gt;&lt;picture&gt;&lt;source media="(prefers-color-scheme: dark)" srcset="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark.svg"&gt;&lt;source media="(prefers-color-scheme: light)" srcset="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-light.svg"&gt;&lt;img alt="Autofix with [code]smith" src="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark.svg"&gt;
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

…quota

Production has been getting 429s with "Your payg app has exceeded its limit
of 10000 token_price requests per 1 hours" (Sentry SOLID-PJ, SOLID-DV). That
quota counts requests, and one request can carry 25 tokens, but every price
was its own request and nothing was reused:

- useBalances refreshed every 5s, and each refresh sent five by-symbol GETs
  (ETH once per chain, FUSE, BNB) plus one per token still unpriced. That is
  about 3,600 requests an hour for a single open app, so two or three users
  used up the whole app's quota.
- fusePriceUsd / ethPriceUsd polled every 5s on top of that.

Changes:
- lib/batchedLoader merges lookups started in the same tick into requests
  of up to 25. It caches prices for a minute (misses for five), keeps serving
  the last price while a refresh fails, and pauses both Prices endpoints
  after a 429.
- fetchTokenPriceUsd and the new fetchTokenPricesBySymbol go through it.
  By-address requests stay within the documented limits of 25 addresses and
  3 networks.
- Balance fallback poll goes from 5s to 30s (SSE already invalidates on
  every balance event), native price refresh from 5s to 60s, and vault
  balance poll from 3s to 30s.
- publicClient keeps one client per chain with Multicall3 batching, so reads
  made together share one eth_call.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UMuZxhszcH7Kc5s7dc2NNq
@vercel

vercel Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
solid-app-staging Building Building Preview Sep 28, 2026 11:07am UTC
1 Skipped Deployment
Project Deployment Actions Updated
solid-app Ignored Ignored Preview Sep 28, 2026 11:07am UTC

Request Review

…o Alchemy

Prices now come from POST /accounts/v1/prices, which serves one cache shared
by every user and every pod. So the app key's Alchemy token_price quota no
longer scales with the number of open apps.

The backend is only an optimisation for the app. On any failure it asks
Alchemy directly with its own key, exactly as before, and skips the backend
for five minutes. Failures include a network error, a timeout, an older
backend without the route (404), an expired session (401), the per-user rate
limit (429), a token the backend can't price right now (503), or a reply
that isn't the expected shape.

That keeps every combination safe:
- an app build or OTA that ships before the backend route is deployed
- a backend rollback
- web and native (cookie or Bearer auth)

Old builds keep calling Alchemy directly.

Symbols and addresses the route's validation would reject are left out of
the request, so one malformed token can't fail the lookup for the rest.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UMuZxhszcH7Kc5s7dc2NNq
@claude

claude Bot commented Sep 28, 2026

Copy link
Copy Markdown

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

The backend's price route leaves out tokens on chains it has no Alchemy
network for, and the app takes a token left out as having no price and
remembers that for five minutes. A chain added to ALCHEMY_NETWORKS (which
an OTA can do) before the backend knows it would lose its prices that way.

Address lookups are now chunked by whether the backend covers the chain
(BACKEND_PRICE_CHAIN_IDS, mirroring its ALCHEMY_PRICE_NETWORKS), and tokens
on any other chain are priced from Alchemy directly, as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UMuZxhszcH7Kc5s7dc2NNq
… polls

The slower polls were there to save the Prices API quota. Price lookups are
now cached for a minute, in the app and by the backend's shared cache, so
polling more often no longer costs token_price requests, and the slower
intervals only made balance updates visibly lag.

Balances poll every 5s again (staleTime 5s), native-token prices every 5s,
and vault balances every 3s, as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UMuZxhszcH7Kc5s7dc2NNq
@MusabShakeel576
MusabShakeel576 merged commit cc27db0 into master Sep 28, 2026
6 checks passed
@MusabShakeel576
MusabShakeel576 deleted the claude/alchemy-threshold-limit-emails-k9buga branch September 28, 2026 11:33
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