diff --git a/app/components/trade/DepositTrigger.tsx b/app/components/trade/DepositTrigger.tsx
index d53df779..281a8434 100644
--- a/app/components/trade/DepositTrigger.tsx
+++ b/app/components/trade/DepositTrigger.tsx
@@ -5,6 +5,7 @@ import { useWalletCompat } from "@/hooks/useWalletCompat";
import { useUserAccount } from "@/hooks/useUserAccount";
import { useSlabState } from "@/components/providers/SlabProvider";
import { useTokenMeta } from "@/hooks/useTokenMeta";
+import { useWalletAtaBalance } from "@/hooks/useWalletAtaBalance";
import { formatTokenAmount } from "@/lib/format";
import { DepositWithdrawCard } from "./DepositWithdrawCard";
import { isMockMode } from "@/lib/mock-mode";
@@ -31,6 +32,23 @@ export const DepositTrigger: FC<{ slabAddress: string }> = ({ slabAddress }) =>
const capital = userAccount?.account.capital ?? 0n;
const prevCapitalRef = useRef(capital);
+ // The bar at the top of the trade panel now shows the user's PHANTOM
+ // (or whichever wallet) USDC balance — what they have available to
+ // deposit — NOT what's already deposited into this market. Account
+ // balance (capital) is shown alongside the order-form Size row as
+ // "Account Bal:" instead.
+ //
+ // Pass `capital` as the refresh trigger so a deposit (which decreases
+ // wallet ATA balance and increases capital) re-fetches the wallet
+ // balance. Without this the bar would keep showing the pre-deposit
+ // number until the user reloaded the page.
+ const { balance: walletBalance, decimals: walletDecimals } = useWalletAtaBalance(
+ config?.collateralMint ?? null,
+ capital,
+ );
+ const displayBalance = walletBalance ?? 0n;
+ const displayDecimals = walletDecimals ?? decimals;
+
// Read localStorage on mount
useEffect(() => {
const stored = localStorage.getItem(lsKey(slabAddress));
@@ -65,9 +83,12 @@ export const DepositTrigger: FC<{ slabAddress: string }> = ({ slabAddress }) =>
onClick={() => setExpanded(!expanded)}
>
- Account
+ {/* Label and value share the same brightness so the whole
+ "Wallet Balance: X USDC" line reads as one unit. The
+ earlier "Account" label was at --text-dim, which made
+ users squint to read which balance this was. */}
- {formatTokenAmount(capital, decimals)} {symbol}
+ Wallet Balance: {formatTokenAmount(displayBalance, displayDecimals)} {symbol}
diff --git a/app/hooks/useWalletAtaBalance.ts b/app/hooks/useWalletAtaBalance.ts
new file mode 100644
index 00000000..ec326d70
--- /dev/null
+++ b/app/hooks/useWalletAtaBalance.ts
@@ -0,0 +1,76 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import type { PublicKey } from "@solana/web3.js";
+import { getAssociatedTokenAddressSync } from "@solana/spl-token";
+import { useWalletCompat, useConnectionCompat } from "@/hooks/useWalletCompat";
+
+export interface WalletAtaBalance {
+ /** Raw atomic balance from the user's ATA, or null if no ATA / not connected. */
+ balance: bigint | null;
+ /** On-chain decimals from the ATA, or null if unavailable. Useful for
+ * tokens where TokenMetadata fails (cross-network, missing). */
+ decimals: number | null;
+}
+
+/** Fetches the user's associated token account balance for a given mint.
+ * Returns `{ balance: null }` when the wallet is disconnected, the mint
+ * is null, or the ATA doesn't exist yet.
+ *
+ * Re-fetch trigger: pass any value as `refreshTrigger` whose identity
+ * changes when an external event invalidates the cached balance.
+ * The canonical use is the in-market `capital` value — a deposit
+ * decreases the wallet ATA balance and increases capital; passing
+ * capital as the trigger forces a re-read of the ATA so the bar
+ * reflects the post-deposit state instead of the stale pre-deposit
+ * number. Without this, only publicKey/mint/connection changes would
+ * trigger a re-read, none of which fire after a deposit/withdraw. */
+export function useWalletAtaBalance(
+ mint: PublicKey | null | undefined,
+ refreshTrigger?: unknown,
+): WalletAtaBalance {
+ const { publicKey } = useWalletCompat();
+ const { connection } = useConnectionCompat();
+ const [state, setState] = useState({
+ balance: null,
+ decimals: null,
+ });
+
+ useEffect(() => {
+ if (!publicKey || !mint) {
+ setState({ balance: null, decimals: null });
+ return;
+ }
+ let cancelled = false;
+ (async () => {
+ try {
+ const ata = getAssociatedTokenAddressSync(mint, publicKey);
+ const info = await connection.getTokenAccountBalance(ata);
+ if (cancelled) return;
+ // RPC returns amount as a string. The happy-path zero balance
+ // is "0" (truthy in JS). The else branch only fires for the
+ // unusual empty-string / falsy case from a malformed response;
+ // even then we want to preserve any decimals the RPC did
+ // hand back rather than silently drop them.
+ const onChainDecimals =
+ info.value.decimals !== undefined ? info.value.decimals : null;
+ if (info.value.amount) {
+ setState({
+ balance: BigInt(info.value.amount),
+ decimals: onChainDecimals,
+ });
+ } else {
+ setState({ balance: null, decimals: onChainDecimals });
+ }
+ } catch {
+ // ATA may not exist yet (user hasn't received this token), keep null.
+ if (!cancelled) setState({ balance: null, decimals: null });
+ }
+ })();
+ return () => {
+ cancelled = true;
+ };
+ }, [publicKey, mint, connection, refreshTrigger]);
+
+ return state;
+}