diff --git a/packages/app/src/components/chat/CashuToken.vue b/packages/app/src/components/chat/CashuToken.vue
new file mode 100644
index 00000000..2a6c6868
--- /dev/null
+++ b/packages/app/src/components/chat/CashuToken.vue
@@ -0,0 +1,116 @@
+
+
+
+
+
+
+
+
+ Cashu Token
+
+
+ {{ formattedAmount }}
+
+
+
+
+
+ Mint: {{ displayMint }}
+
+
+
+
+ {{ parsed.memo }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ truncatedRaw }}
+
+
+
+
diff --git a/packages/app/src/components/chat/ChatMessage.vue b/packages/app/src/components/chat/ChatMessage.vue
index 78f909c8..0438722b 100644
--- a/packages/app/src/components/chat/ChatMessage.vue
+++ b/packages/app/src/components/chat/ChatMessage.vue
@@ -83,6 +83,14 @@
/>
+
+
+
+
{
return matches ? [...new Set(matches)] : []
})
+const cashuTokens = computed(() => {
+ if (isUser.value) return []
+ return extractCashuTokens(props.message.content)
+})
+
const isCodeResponse = computed(() =>
!isUser.value && props.triggeringQuery.trim().toLowerCase() === '/code'
)
@@ -278,6 +293,12 @@ const displayText = computed(() => {
let text = stripContentTags(props.message.content)
if (inlineNewsLinks.value.length > 0 || inlineWebsitesLinks.value.length > 0) text = stripMarkdownLinks(text)
if (nostrUris.value.length > 0) text = text.replace(NOSTR_URI_RE, '').replace(/\n{3,}/g, '\n\n').trim()
+ if (cashuTokens.value.length > 0) {
+ for (const token of cashuTokens.value) {
+ text = text.replace(token, '')
+ }
+ text = text.replace(/\n{3,}/g, '\n\n').trim()
+ }
return text
})
diff --git a/packages/app/src/utils/cashu.ts b/packages/app/src/utils/cashu.ts
new file mode 100644
index 00000000..7aa09384
--- /dev/null
+++ b/packages/app/src/utils/cashu.ts
@@ -0,0 +1,98 @@
+/**
+ * Cashu ecash token parsing and display utilities.
+ * AIUI is never a wallet — only parses, displays, and deep-links to external wallets.
+ */
+
+interface CashuProof {
+ amount: number
+ id: string
+ secret: string
+ C: string
+}
+
+interface CashuTokenEntry {
+ mint: string
+ proofs: CashuProof[]
+}
+
+interface CashuTokenData {
+ token: CashuTokenEntry[]
+ unit?: string
+ memo?: string
+}
+
+export interface ParsedCashuToken {
+ mint: string
+ amount: number
+ unit: string
+ memo?: string
+ raw: string
+}
+
+/** Check if a string looks like a Cashu token */
+export function isCashuToken(text: string): boolean {
+ return text.trim().startsWith('cashuA')
+}
+
+/** Extract Cashu tokens from a text string */
+export function extractCashuTokens(text: string): string[] {
+ const regex = /cashuA[A-Za-z0-9_-]+/g
+ return [...text.matchAll(regex)].map(m => m[0])
+}
+
+/** Parse a Cashu token string (cashuA...) into structured data */
+export function parseCashuToken(token: string): ParsedCashuToken | null {
+ if (!isCashuToken(token)) return null
+
+ try {
+ // Remove 'cashuA' prefix and decode base64url
+ const encoded = token.slice(6)
+ const padded = encoded.replace(/-/g, '+').replace(/_/g, '/')
+ const json = atob(padded)
+ const data = JSON.parse(json) as CashuTokenData
+
+ if (!data.token || !Array.isArray(data.token) || data.token.length === 0) {
+ return null
+ }
+
+ const entry = data.token[0]
+ const totalAmount = entry.proofs.reduce((sum, p) => sum + p.amount, 0)
+
+ return {
+ mint: entry.mint,
+ amount: totalAmount,
+ unit: data.unit ?? 'sat',
+ memo: data.memo,
+ raw: token,
+ }
+ } catch {
+ return null
+ }
+}
+
+/** Format a mint URL for display (truncate) */
+export function formatMintUrl(url: string): string {
+ try {
+ const parsed = new URL(url)
+ const host = parsed.hostname
+ if (host.length > 30) {
+ return host.slice(0, 15) + '...' + host.slice(-12)
+ }
+ return host
+ } catch {
+ if (url.length > 30) {
+ return url.slice(0, 15) + '...' + url.slice(-12)
+ }
+ return url
+ }
+}
+
+/** Format amount with unit */
+export function formatCashuAmount(amount: number, unit: string): string {
+ if (unit === 'sat' || unit === 'sats') {
+ if (amount >= 1_000_000) return `${(amount / 1_000_000).toFixed(2)}M sats`
+ if (amount >= 1_000) return `${(amount / 1_000).toFixed(amount >= 10_000 ? 0 : 1)}k sats`
+ return `${amount} sats`
+ }
+ return `${amount} ${unit}`
+}