diff --git a/src/components/boost/boost-button.tsx b/src/components/boost/boost-button.tsx
index 9ca71d72..2bb15a83 100644
--- a/src/components/boost/boost-button.tsx
+++ b/src/components/boost/boost-button.tsx
@@ -25,6 +25,7 @@ export function BoostButton({ refId, pubkey, boostCount = 0, className }: BoostB
const isAdmin = useUserStore((s) => s.isAdmin)
const setBudget = useUserStore((s) => s.setBudget)
+ const refreshBalance = useUserStore((s) => s.refreshBalance)
const handleBoost = useCallback(async () => {
if (boosting) return
@@ -56,13 +57,14 @@ export function BoostButton({ refId, pubkey, boostCount = 0, className }: BoostB
setCount((c) => c + DEFAULT_BOOST_AMOUNT)
setFlash(true)
setTimeout(() => setFlash(false), 600)
+ refreshBalance()
} catch (err) {
console.error("Boost failed:", err)
setError("Boost failed. Please try again.")
} finally {
setBoosting(false)
}
- }, [refId, pubkey, boosting, isAdmin, setBudget])
+ }, [refId, pubkey, boosting, isAdmin, setBudget, refreshBalance])
return (
diff --git a/src/components/layout/node-preview-panel.tsx b/src/components/layout/node-preview-panel.tsx
index e63d6d0e..5567a202 100644
--- a/src/components/layout/node-preview-panel.tsx
+++ b/src/components/layout/node-preview-panel.tsx
@@ -9,6 +9,7 @@ import { ScrollArea } from "@/components/ui/scroll-area"
import { api } from "@/lib/api"
import { useMocks, MOCK_FULL_NODES } from "@/lib/mock-data"
import { usePlayerStore } from "@/stores/player-store"
+import { useUserStore } from "@/stores/user-store"
import type { GraphNode } from "@/lib/graph-api"
import type { SchemaNode } from "@/app/ontology/page"
@@ -206,6 +207,7 @@ function PersonCard({ props }: { props: Record }) {
export function NodePreviewPanel({ node, onBack, schemas }: NodePreviewPanelProps) {
const [unlockState, setUnlockState] = useState("preview")
const [fullNode, setFullNode] = useState(null)
+ const refreshBalance = useUserStore((s) => s.refreshBalance)
const nodeType = node.node_type ?? "Unknown"
const schema = schemas.find((s) => s.type === nodeType)
@@ -242,6 +244,7 @@ export function NodePreviewPanel({ node, onBack, schemas }: NodePreviewPanelProp
setFullNode(result)
}
setUnlockState("unlocked")
+ refreshBalance()
} catch {
setUnlockState("error")
}
diff --git a/src/components/modals/add-content-modal.tsx b/src/components/modals/add-content-modal.tsx
index a7bcdb95..d75f3d39 100644
--- a/src/components/modals/add-content-modal.tsx
+++ b/src/components/modals/add-content-modal.tsx
@@ -28,6 +28,7 @@ import {
export function AddContentModal() {
const { activeModal, close, open: openModal } = useModalStore()
const { budget, setBudget, pubKey } = useUserStore()
+ const refreshBalance = useUserStore((s) => s.refreshBalance)
const [sourceUrl, setSourceUrl] = useState("")
const [detectedType, setDetectedType] = useState(null)
const [detecting, setDetecting] = useState(false)
@@ -109,6 +110,7 @@ export function AddContentModal() {
try {
await submitWithAuth(trimmed, detectedType)
setSuccess(true)
+ refreshBalance()
setTimeout(() => {
setSourceUrl("")
setDetectedType(null)
@@ -125,6 +127,7 @@ export function AddContentModal() {
// Retry after payment
await submitWithAuth(trimmed, detectedType)
setSuccess(true)
+ refreshBalance()
setTimeout(() => {
setSourceUrl("")
setDetectedType(null)
@@ -148,7 +151,7 @@ export function AddContentModal() {
} finally {
setSubmitting(false)
}
- }, [sourceUrl, detectedType, close, setBudget, submitWithAuth])
+ }, [sourceUrl, detectedType, close, setBudget, submitWithAuth, refreshBalance])
const handleOpenChange = useCallback(
(open: boolean) => {
diff --git a/src/components/modals/budget-modal.tsx b/src/components/modals/budget-modal.tsx
index b938a97b..c13c7898 100644
--- a/src/components/modals/budget-modal.tsx
+++ b/src/components/modals/budget-modal.tsx
@@ -14,8 +14,7 @@ import { Button } from "@/components/ui/button"
import { Separator } from "@/components/ui/separator"
import { useModalStore } from "@/stores/modal-store"
import { useUserStore } from "@/stores/user-store"
-import { isSphinx, getL402, hasWebLN, payInvoice, payL402, topUpLsat, topUpConfirm } from "@/lib/sphinx"
-import { api } from "@/lib/api"
+import { isSphinx, hasWebLN, payInvoice, payL402, topUpLsat, topUpConfirm } from "@/lib/sphinx"
type Step = "balance" | "amount" | "invoice" | "success"
@@ -24,6 +23,7 @@ const PRESET_AMOUNTS = [50, 100, 500, 1000]
export function BudgetModal() {
const { activeModal, close } = useModalStore()
const { budget, setBudget } = useUserStore()
+ const refreshBalance = useUserStore((s) => s.refreshBalance)
const [loading, setLoading] = useState(false)
const [error, setError] = useState("")
const [step, setStep] = useState("balance")
@@ -66,15 +66,7 @@ export function BudgetModal() {
}
}, [])
- const refreshBalance = useCallback(async () => {
- const l402 = await getL402()
- if (l402) {
- const bal = await api.get<{ balance: number }>("/balance", {
- Authorization: l402,
- })
- setBudget(bal.balance)
- }
- }, [setBudget])
+
// Route "Top Up" to the right flow
const handleTopUp = useCallback(async () => {
@@ -198,21 +190,11 @@ export function BudgetModal() {
const handleRefreshBalance = useCallback(async () => {
setLoading(true)
try {
- const l402 = await getL402()
- if (!l402) {
- setBudget(0)
- return
- }
- const balance = await api.get<{ balance: number }>("/balance", {
- Authorization: l402,
- })
- setBudget(balance.balance)
- } catch {
- // keep existing budget
+ await refreshBalance()
} finally {
setLoading(false)
}
- }, [setBudget])
+ }, [refreshBalance])
const canTopUp = sphinxConnected || weblnAvailable || hasExistingL402
diff --git a/src/components/search/search-bar.tsx b/src/components/search/search-bar.tsx
index 72b4a2bf..dfdb6df5 100644
--- a/src/components/search/search-bar.tsx
+++ b/src/components/search/search-bar.tsx
@@ -14,7 +14,7 @@ import { useMocks, MOCK_NODES, MOCK_EDGES } from "@/lib/mock-data"
export function SearchBar() {
const setSearchTerm = useAppStore((s) => s.setSearchTerm)
const { setGraphData, setLoading } = useGraphStore()
- const setBudget = useUserStore((s) => s.setBudget)
+ const refreshBalance = useUserStore((s) => s.refreshBalance)
const openModal = useModalStore((s) => s.open)
const [value, setValue] = useState("")
const [focused, setFocused] = useState(false)
@@ -46,6 +46,7 @@ export function SearchBar() {
} else {
const result = await searchNodes(trimmed, { limit: 100 }, controller.signal)
setGraphData(result.nodes ?? [], result.edges ?? [])
+ refreshBalance()
}
} catch (err) {
if (err instanceof DOMException && err.name === "AbortError") return
@@ -53,10 +54,11 @@ export function SearchBar() {
// Handle 402 — need payment to search
if (err instanceof Response && err.status === 402) {
try {
- await payL402(setBudget)
+ await payL402(() => {})
// Retry search after payment
const result = await searchNodes(trimmed, { limit: 100 }, controller.signal)
setGraphData(result.nodes ?? [], result.edges ?? [])
+ refreshBalance()
} catch {
// Payment failed or cancelled — open budget modal
openModal("budget")
@@ -69,7 +71,7 @@ export function SearchBar() {
setLoading(false)
}
},
- [value, setSearchTerm, setGraphData, setLoading]
+ [value, setSearchTerm, setGraphData, setLoading, refreshBalance]
)
const handleClear = useCallback(() => {
diff --git a/src/stores/user-store.ts b/src/stores/user-store.ts
index 18bdf9ff..87a16b63 100644
--- a/src/stores/user-store.ts
+++ b/src/stores/user-store.ts
@@ -1,6 +1,8 @@
"use client"
import { create } from "zustand"
+import { getL402 } from "@/lib/sphinx"
+import { api } from "@/lib/api"
interface UserState {
isAdmin: boolean
@@ -17,6 +19,7 @@ interface UserActions {
setBudget: (val: number | null) => void
incrementNodeCount: () => void
resetNodeCount: () => void
+ refreshBalance: () => Promise
}
export type UserStore = UserState & UserActions
@@ -34,4 +37,15 @@ export const useUserStore = create((set) => ({
incrementNodeCount: () =>
set((s) => ({ nodeCount: s.nodeCount + 1 })),
resetNodeCount: () => set({ nodeCount: 0 }),
+ refreshBalance: async () => {
+ const l402 = await getL402()
+ if (!l402) { set({ budget: 0 }); return }
+ try {
+ const bal = await api.get<{ balance: number }>("/balance", { Authorization: l402 })
+ set({ budget: bal.balance })
+ } catch {
+ localStorage.removeItem("l402")
+ set({ budget: 0 })
+ }
+ },
}))