diff --git a/.agents/swap.md b/.agents/swap.md
index d741e6ab..0bd286f3 100644
--- a/.agents/swap.md
+++ b/.agents/swap.md
@@ -158,7 +158,7 @@ getTrackingUrl?(execution): string | undefined
## MCP swap surface (`mobile/src/features/mcp/modules/mcp-calls.ts`)
-Two tools expose Flashnet to remote AI agents. They run on `MCP_BALANCE_ACCOUNT_NUMBER` (= 4) so they don't touch the user's primary account.
+Two tools expose Flashnet to remote AI agents. They run on `MCP_BALANCE_ACCOUNT_NUMBER` (= 4141) so they don't touch the user's primary account.
- **`get_swap_quote(send_asset, receive_asset, send_amount_base_units)`** — `send_asset` / `receive_asset` are strict `AssetId` strings (currently `native:spark` / `token:spark:usdb`). Internally: `lazyInitWallet(NETWORK_SPARK, 4)` → `setFlashnetAccountNumber(4)` → `manager.getQuote()` → `manager.executeTransfer()` (Flashnet: stages params, no funds movement — in-memory only, NOT persisted). Returns `{ quote_id, send_amount_base_units, receive_amount_base_units, fee_base_units, fee_asset, fee_ticker, price_impact_pct, rate, estimated_time_seconds, expires_at_unix, service }`.
- **`execute_swap(quote_id)`** — `manager.executeInstantSwap(quote_id)` → `commitTransfer()` (persists completed row). The manager looks up the owning service from `executionOwners` (populated in `executeTransfer`), so the agent only needs `quote_id`. Idempotency: the manager pops the owner entry on execute and the owning service pops the quote from its pending map; replay fails with _"No pending swap found"_. Quote expiry is enforced by Flashnet's internal `PENDING_SWAP_TTL` (5 min) on top of `TransferQuote.expiresAt` (60 s).
diff --git a/mobile/app/(tabs)/_layout.tsx b/mobile/app/(tabs)/_layout.tsx
index b6b7e3a4..fc8414ec 100644
--- a/mobile/app/(tabs)/_layout.tsx
+++ b/mobile/app/(tabs)/_layout.tsx
@@ -5,8 +5,7 @@ import React, { useContext } from 'react';
import { Platform, StyleSheet } from 'react-native';
import CustomTabBarBackground from '@/components/ui/CustomTabBarBackground';
-import { MCP_BALANCE_ACCOUNT_NUMBER } from '@/src/features/mcp/modules/mcp-constants';
-import { AccountNumberContext } from '@shared/hooks/AccountNumberContext';
+import { AccountNumberContext, MCP_BALANCE_ACCOUNT_NUMBER } from '@shared/hooks/AccountNumberContext';
// Use custom tab bar only on iOS 18–25 for a reliable background. iOS 26+ and Android use native tabs.
const iosVersion = Platform.OS === 'ios' ? (typeof Platform.Version === 'string' ? parseInt(String(Platform.Version), 10) : Number(Platform.Version)) : 0;
diff --git a/mobile/app/Home.tsx b/mobile/app/Home.tsx
index a3073532..ef8969d0 100644
--- a/mobile/app/Home.tsx
+++ b/mobile/app/Home.tsx
@@ -27,10 +27,9 @@ import TokensView from '@/components/TokensView';
import YieldView from '@/components/YieldView';
import TransactionsList from '@/components/TransactionsList';
import { BackgroundExecutor } from '@/src/modules/background-executor';
-import { MCP_BALANCE_ACCOUNT_NUMBER } from '@/src/features/mcp/modules/mcp-constants';
import { getNetworkImageAsset } from '@/utils/networkAssets';
import { getNetworkGradient } from '@shared/constants/Colors';
-import { AccountNumberContext } from '@shared/hooks/AccountNumberContext';
+import { AccountNumberContext, MCP_BALANCE_ACCOUNT_NUMBER } from '@shared/hooks/AccountNumberContext';
import { NetworkContext } from '@shared/hooks/NetworkContext';
import { useAvailableNetworks } from '@shared/hooks/useAvailableNetworks';
import { useSettings } from '@shared/hooks/useSettings';
diff --git a/mobile/app/PocketSwitch.tsx b/mobile/app/PocketSwitch.tsx
index 9155383c..6613b0d1 100644
--- a/mobile/app/PocketSwitch.tsx
+++ b/mobile/app/PocketSwitch.tsx
@@ -22,16 +22,16 @@ const TotalBalanceSection = () => {
const { exchangeRate } = useExchangeRate(NETWORK_BITCOIN, 'USD');
// Get balances for all accounts (hooks must be called unconditionally)
- const { accountBalance: balance0 } = useAccountBalance(0, availableNetworks);
- const { accountBalance: balance1 } = useAccountBalance(1, availableNetworks);
- const { accountBalance: balance2 } = useAccountBalance(2, availableNetworks);
- const { accountBalance: balance3 } = useAccountBalance(3, availableNetworks);
- const { accountBalance: balance4 } = useAccountBalance(4, availableNetworks);
+ const { accountBalance: balance0 } = useAccountBalance(accountItems[0].accountNumber, availableNetworks);
+ const { accountBalance: balance1 } = useAccountBalance(accountItems[1].accountNumber, availableNetworks);
+ const { accountBalance: balance2 } = useAccountBalance(accountItems[2].accountNumber, availableNetworks);
+ const { accountBalance: balance3 } = useAccountBalance(accountItems[3].accountNumber, availableNetworks);
+ const { accountBalance: balanceMcp } = useAccountBalance(accountItems[4].accountNumber, availableNetworks);
const totalBalance = useMemo(() => {
- const balances = [balance0, balance1, balance2, balance3, balance4].slice(0, accountItems.length);
+ const balances = [balance0, balance1, balance2, balance3, balanceMcp].slice(0, accountItems.length);
return balances.reduce((sum, bal) => sum + (parseInt(bal) || 0), 0).toString();
- }, [balance0, balance1, balance2, balance3, balance4]);
+ }, [balance0, balance1, balance2, balance3, balanceMcp]);
const totalUsd = totalBalance && exchangeRate ? formatFiatBalance(totalBalance, getDecimalsByNetwork(NETWORK_BITCOIN), exchangeRate) : '—';
@@ -44,13 +44,13 @@ const TotalBalanceSection = () => {
);
};
-const ListItem = ({ item, onPress, accountNumber, currentAccountNumber }: { item: AccountItem; onPress: () => void; accountNumber: number; currentAccountNumber: number }) => {
+const ListItem = ({ item, onPress, currentAccountNumber }: { item: AccountItem; onPress: () => void; currentAccountNumber: number }) => {
const availableNetworks = useAvailableNetworks();
const IconComponent = item.iconCollection === 'ion' ? Ionicons : item.iconCollection === 'material-community' ? MaterialCommunityIcons : Foundation;
- const { accountBalance } = useAccountBalance(accountNumber, availableNetworks);
+ const { accountBalance } = useAccountBalance(item.accountNumber, availableNetworks);
const { exchangeRate } = useExchangeRate(NETWORK_BITCOIN, 'USD');
- const active = accountNumber === currentAccountNumber;
+ const active = item.accountNumber === currentAccountNumber;
const usdBalance = accountBalance && exchangeRate ? formatFiatBalance(accountBalance, getDecimalsByNetwork(NETWORK_BITCOIN), exchangeRate) : '—';
@@ -81,8 +81,8 @@ export default function PocketSwitch() {
router.back();
};
- const handleSelect = (index: number) => {
- setAccountNumber(index);
+ const handleSelect = (accountNumber: number) => {
+ setAccountNumber(accountNumber);
router.back();
};
@@ -99,8 +99,8 @@ export default function PocketSwitch() {
{/* Target Networks List */}
- {accountItems.map((item, index) => (
- handleSelect(index)} />
+ {accountItems.map((item) => (
+ handleSelect(item.accountNumber)} />
))}
diff --git a/mobile/components/StickyHeader.tsx b/mobile/components/StickyHeader.tsx
index e0598325..75691b78 100644
--- a/mobile/components/StickyHeader.tsx
+++ b/mobile/components/StickyHeader.tsx
@@ -3,7 +3,7 @@ import { StyleSheet, View } from 'react-native';
import { useRouter } from 'expo-router';
import { ThemedText } from './ThemedText';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
-import { AccountItem, AccountNumberContext, accountItems } from '@shared/hooks/AccountNumberContext';
+import { AccountNumberContext, getAccountItem } from '@shared/hooks/AccountNumberContext';
import { ScanQrContext } from '@/src/hooks/ScanQrContext';
import { handleQrIntent } from '@/src/modules/scan-routing';
import { Ionicons, Foundation, MaterialCommunityIcons } from '@expo/vector-icons';
@@ -21,7 +21,7 @@ const StickyHeader: React.FC = ({ scrollY, onSettingsPress })
const router = useRouter();
const insets = useSafeAreaInsets();
const { accountNumber } = React.useContext(AccountNumberContext);
- const accountItem: AccountItem = accountItems[accountNumber];
+ const accountItem = getAccountItem(accountNumber);
const { scanQr } = useContext(ScanQrContext);
// Animated border opacity based on scroll position
diff --git a/mobile/src/features/mcp/README.md b/mobile/src/features/mcp/README.md
index 1c520560..9006816f 100644
--- a/mobile/src/features/mcp/README.md
+++ b/mobile/src/features/mcp/README.md
@@ -48,7 +48,7 @@ features/mcp/
├── mcp-calls.ts ← Tool registrations (the wallet API surface)
├── tunnel.ts ← WebSocket client + auto-reconnect
├── mcp-activity-log.ts ← In-memory store for the last 5 actions (UI)
- └── mcp-constants.ts ← Shared constants (MCP_BALANCE_ACCOUNT_NUMBER, ...)
+ └── mcp-constants.ts ← MCP-only constants (e.g. lightning fee cap)
```
`components/toast-config.tsx` lives at app-level `mobile/components/` — it's shared across features.
@@ -101,7 +101,7 @@ Conventions:
- **Always** call `showMcpSuccessToast(summary, detail?)` on success — it both shows the user-visible dark toast (`type: 'mcpAiSuccess'`) and pushes the activity-log line under the Home status row. The toast is the user's only signal that the LLM did something.
- Return shape is fixed: `{ content: [{ type: 'text', text: }] }` on success, plus `isError: true` on failure.
- All amounts cross the wire as **smallest-unit integer strings** (e.g. satoshis, not BTC). Use `mcpPositiveBaseUnitsString` and let the LLM resolve decimals via `get_network_balance` (returns `decimals`).
-- Wallets always use `MCP_BALANCE_ACCOUNT_NUMBER` (4) — the dedicated MCP pocket. Don't read the user's current `accountNumber` context.
+- Wallets always use `MCP_BALANCE_ACCOUNT_NUMBER` from `@shared/hooks/AccountNumberContext` (4141) — the dedicated MCP pocket. Don't read the user's current `accountNumber` context.
- The `network` argument is always **mainnet-only** for the user-facing surface (`mcpListableNetworks()` filters testnets / lightning aliases / USDT).
## Security model
diff --git a/mobile/src/features/mcp/modules/mcp-calls.ts b/mobile/src/features/mcp/modules/mcp-calls.ts
index eb15d818..45f83c50 100644
--- a/mobile/src/features/mcp/modules/mcp-calls.ts
+++ b/mobile/src/features/mcp/modules/mcp-calls.ts
@@ -13,6 +13,7 @@ import Toast from 'react-native-toast-message';
import { walletCanHaveNfts } from '@shared/class/wallets/interface-can-have-nfts';
import { walletCanHaveTokens } from '@shared/class/wallets/interface-can-have-tokens';
import { walletSupportsLightning } from '@shared/class/wallets/interface-lightning-wallet';
+import { MCP_BALANCE_ACCOUNT_NUMBER } from '@shared/hooks/AccountNumberContext';
import { exchangeRateFetcher } from '@shared/hooks/useExchangeRate';
import { balanceFetcher } from '@shared/hooks/useBalance';
import { getTransferServiceManager, setFlashnetAccountNumber, useTransferService } from '@shared/hooks/useTransferService';
@@ -39,7 +40,7 @@ import { BackgroundExecutor } from '@/src/modules/background-executor';
import { AnalyticsEvents, trackAnalyticsEvent } from '@/src/modules/analytics';
import { pushMcpActivityLog } from './mcp-activity-log';
-import { MCP_BALANCE_ACCOUNT_NUMBER, MCP_LIGHTNING_PAY_MAX_FEE_PERCENT } from './mcp-constants';
+import { MCP_LIGHTNING_PAY_MAX_FEE_PERCENT } from './mcp-constants';
function mcpCallLog(line: string): void {
console.log('[mcp-call] ' + line);
diff --git a/mobile/src/features/mcp/modules/mcp-constants.ts b/mobile/src/features/mcp/modules/mcp-constants.ts
index 1ff1d6ff..87a295e1 100644
--- a/mobile/src/features/mcp/modules/mcp-constants.ts
+++ b/mobile/src/features/mcp/modules/mcp-constants.ts
@@ -1,5 +1,2 @@
-/** MCP wallet tools + Home tunnel row use this account index (keep in sync with `mcp-calls`). */
-export const MCP_BALANCE_ACCOUNT_NUMBER = 4;
-
/** Matches in-app Lightning send confirm (`send-confirm-lightning.tsx`). */
export const MCP_LIGHTNING_PAY_MAX_FEE_PERCENT = 5;
diff --git a/mobile/src/tests/unit-vi/mcp-calls-swap.test.ts b/mobile/src/tests/unit-vi/mcp-calls-swap.test.ts
index f90ad025..02da2c5c 100644
--- a/mobile/src/tests/unit-vi/mcp-calls-swap.test.ts
+++ b/mobile/src/tests/unit-vi/mcp-calls-swap.test.ts
@@ -9,6 +9,7 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { MCP_BALANCE_ACCOUNT_NUMBER } from '@shared/hooks/AccountNumberContext';
import { registerWalletMcpCalls } from '../../features/mcp/modules/mcp-calls';
import { FlashnetTransferService } from '@shared/services/transfer-service-flashnet';
import { TransferServiceManager } from '@shared/services/transfer-service-manager';
@@ -17,7 +18,6 @@ import { TransferServiceManager } from '@shared/services/transfer-service-manage
const BTC_PUBKEY = '020202020202020202020202020202020202020202020202020202020202020202';
const USDB_PUBKEY = '3206c93b24a4d18ea19d0a9a213204af2c7e74a6d16c7535cc5d33eca4ad1eca';
const POOL_ID = 'pool-btc-usdb';
-const MCP_ACCOUNT = 4;
// vi.hoisted — vi.mock factories are lifted above imports, so their closures need
// stable references that exist at hoist time. SDK call spies live here so we can
@@ -217,7 +217,7 @@ describe('MCP swap tools', () => {
});
describe('get_swap_quote — wiring guarantees', () => {
- it('pins all swap activity to MCP_BALANCE_ACCOUNT_NUMBER (4), even if a UI flow set a different one', async () => {
+ it('pins all swap activity to MCP_BALANCE_ACCOUNT_NUMBER, even if a UI flow set a different one', async () => {
// Pretend the UI was on account 7 right before the agent came in.
flashnet.setCurrentAccountNumber(7);
@@ -228,11 +228,11 @@ describe('MCP swap tools', () => {
});
// Spark wallet for the MCP account must have been initialized.
- expect(lazyInitWallet).toHaveBeenCalledWith('spark', MCP_ACCOUNT);
+ expect(lazyInitWallet).toHaveBeenCalledWith('spark', MCP_BALANCE_ACCOUNT_NUMBER);
// FlashnetTransferService.ensureClient resolves its wallet via getSparkWallet(currentAccountNumber).
- // If the wrapper had failed to re-point the service at MCP_ACCOUNT, this would be called with 7.
- expect(getSparkWallet).toHaveBeenCalledWith(MCP_ACCOUNT);
+ // If the wrapper had failed to re-point the service at MCP_BALANCE_ACCOUNT_NUMBER, this would be called with 7.
+ expect(getSparkWallet).toHaveBeenCalledWith(MCP_BALANCE_ACCOUNT_NUMBER);
expect(getSparkWallet).not.toHaveBeenCalledWith(7);
});
@@ -422,8 +422,8 @@ describe('MCP swap tools', () => {
await handlers.get('execute_swap')!({ quote_id: quoteId });
- // Execute must have asked for account 4's wallet, not 9.
- expect(getSparkWallet).toHaveBeenCalledWith(MCP_ACCOUNT);
+ // Execute must have asked for the MCP pocket wallet, not 9.
+ expect(getSparkWallet).toHaveBeenCalledWith(MCP_BALANCE_ACCOUNT_NUMBER);
expect(getSparkWallet).not.toHaveBeenCalledWith(9);
});
diff --git a/shared/hooks/AccountNumberContext.tsx b/shared/hooks/AccountNumberContext.tsx
index 20f721e8..7ae7ae3e 100644
--- a/shared/hooks/AccountNumberContext.tsx
+++ b/shared/hooks/AccountNumberContext.tsx
@@ -7,40 +7,28 @@ import { Networks } from '../types/networks';
import { STORAGE_SELECTED_NETWORK } from './NetworkContext';
import { IStorage } from '../types/IStorage';
+/** Dedicated wallet account index for the AI Agent / MCP pocket (BIP account'). */
+export const MCP_BALANCE_ACCOUNT_NUMBER = 4141;
+
export interface AccountItem {
+ accountNumber: number;
name: string;
icon: string;
iconCollection: string;
}
-export const accountItems: AccountItem[] = [
- {
- name: 'Daily',
- icon: 'wallet-outline',
- iconCollection: 'ion',
- },
- {
- name: 'Investment',
- icon: 'bar-chart-outline',
- iconCollection: 'ion',
- },
- {
- name: 'Lifestyle',
- icon: 'cart-outline',
- iconCollection: 'ion',
- },
- {
- name: 'Emergency',
- icon: 'sound',
- iconCollection: 'foundation',
- },
- {
- name: 'AI Agent',
- icon: 'robot-outline',
- iconCollection: 'material-community',
- },
+export const accountItems: readonly AccountItem[] = [
+ { accountNumber: 0, name: 'Daily', icon: 'wallet-outline', iconCollection: 'ion' },
+ { accountNumber: 1, name: 'Investment', icon: 'bar-chart-outline', iconCollection: 'ion' },
+ { accountNumber: 2, name: 'Lifestyle', icon: 'cart-outline', iconCollection: 'ion' },
+ { accountNumber: 3, name: 'Emergency', icon: 'sound', iconCollection: 'foundation' },
+ { accountNumber: MCP_BALANCE_ACCOUNT_NUMBER, name: 'AI Agent', icon: 'robot-outline', iconCollection: 'material-community' },
] as const;
+export function getAccountItem(accountNumber: number): AccountItem {
+ return accountItems.find((item) => item.accountNumber === accountNumber) ?? accountItems[0];
+}
+
type AccountNumber = number;
interface IAccountNumberContext {
@@ -92,7 +80,7 @@ export const AccountNumberContextProvider: React.FC {
try {
const response = (await props.storage.getItem(STORAGE_SELECTED_NETWORK)) as Networks;
- const addressResponse = await props.backgroundCaller.getAddress(response || DEFAULT_NETWORK, accountNumber);
+ const addressResponse = await props.backgroundCaller.getAddress(response || DEFAULT_NETWORK, value);
await props.messenger.sendEventCallbackFromPopupToContentScript({
for: 'webpage',
event: 'accountsChanged',