Skip to main content

Overview

Because Cashu mints have custody of your Bitcoin, choosing trustworthy mints is critical. Sovran provides two complementary systems for evaluating mints:
  1. Auditor Data - Automated uptime and success rate monitoring
  2. KYM (Know Your Mint) - Community-driven Nostr-based ratings

Auditor System

Sovran queries the Cashu Auditor API to fetch real-time mint health metrics.

Audit Data Structure

interface AuditInfo {
  url: string;
  name: string;
  state: 'OK' | 'ERROR' | 'OFFLINE';
  
  // Swap-based metrics (preferred)
  successRate?: number;     // 0.0 to 1.0
  swapTotal?: number;       // Number of swaps in sample
  swapSuccess?: number;     // Successful swaps
  avgTimeMs?: number;       // Average successful swap time
  score?: number;           // 0-5 score derived from successRate
  
  // Operation counts
  auditorData: {
    name: string;
    state: string;
    mints: number;          // Total mint operations
    melts: number;          // Total melt operations
    errors: number;         // Total errors
  };
}

Using the Audit Hook

import { useAuditedMint } from 'hooks/coco/useAuditedMint';

function MintHealthIndicator({ mintUrl }: { mintUrl: string }) {
  const { auditInfo, mintInfo, loading, error } = useAuditedMint(mintUrl);
  
  if (loading) return <Skeleton />;
  if (error) return <Text>Failed to load audit data</Text>;
  
  const successRate = auditInfo?.successRate 
    ? Math.round(auditInfo.successRate * 100) 
    : 0;
  
  return (
    <View>
      <Text>State: {auditInfo?.state}</Text>
      <Text>Success Rate: {successRate}%</Text>
      <Text>
        {auditInfo?.swapSuccess} of {auditInfo?.swapTotal} swaps succeeded
      </Text>
      <Text>Avg Time: {auditInfo?.avgTimeMs}ms</Text>
    </View>
  );
}

Batch Loading for Lists

For mint lists, use useAuditedMints to load multiple mints efficiently:
import { useAuditedMints } from 'hooks/coco/useAuditedMints';

function MintList({ mintUrls }: { mintUrls: string[] }) {
  const { getAuditData } = useAuditedMints(mintUrls);
  
  return (
    <>
      {mintUrls.map((url) => {
        const auditData = getAuditData(url);
        return (
          <MintRow 
            key={url}
            url={url}
            auditData={auditData}
          />
        );
      })}
    </>
  );
}

Caching Strategy

Audit data is cached in auditMintStore.ts with a 5-minute TTL:
import { useAuditMintStore } from 'stores/auditMintStore';

const getCached = useAuditMintStore((state) => state.getCached);
const setCached = useAuditMintStore((state) => state.setCached);
const isStale = useAuditMintStore((state) => state.isStale);

// Check cache before fetching
const cached = getCached(mintUrl);
if (cached && !isStale(mintUrl)) {
  // Use cached data
} else {
  // Fetch fresh data
  const result = await auditMint({ mintUrl });
  if (result.isOk()) {
    setCached(mintUrl, result.value, mintInfo);
  }
}

Health Badge Variants

function MintStateBadge({ state }: { state: string }) {
  const variant = state === 'ERROR' ? 'error' : 'success';
  
  return (
    <Badge 
      variant={variant} 
      icon={state === 'OK' ? 'fluent:checkmark-16-filled' : 'nonicons:error-16'}
    >
      {state}
    </Badge>
  );
}

KYM (Know Your Mint) Ratings

KYM is a Nostr-based community rating system using kind 38000 events.

Rating Event Structure

// Kind 38000: Cashu mint recommendation
{
  kind: 38000,
  pubkey: '...',  // Reviewer's pubkey
  content: '[5] Great uptime, fast melts',
  tags: [
    ['u', 'https://mint.example.com'],  // Mint URL
    ['rating', '5']                      // 0-5 score
  ],
  created_at: 1234567890
}

Fetching KYM Scores

Single mint:
import { useKYMMint } from 'hooks/coco/useKYMMint';

function MintRating({ mintUrl }: { mintUrl: string }) {
  const { score, recommendations, loading, error } = useKYMMint(mintUrl);
  
  if (loading) return <Skeleton />;
  if (!score) return <Text>No ratings yet</Text>;
  
  return (
    <View>
      <Text>{score.toFixed(1)} / 5.0</Text>
      <Text>{recommendations?.length || 0} reviews</Text>
    </View>
  );
}
Multiple mints (batch):
import { useKYMMints } from 'hooks/coco/useKYMMints';

function MintGrid({ mintUrls }: { mintUrls: string[] }) {
  const { scores, loading } = useKYMMints(mintUrls);
  
  return (
    <>
      {mintUrls.map((url) => {
        const normalized = normalizeMintUrlKey(url);
        const kymData = scores[normalized];
        
        return (
          <MintCard
            key={url}
            url={url}
            score={kymData?.score}
            loading={loading}
          />
        );
      })}
    </>
  );
}

Rating Display Component

The info screen includes an animated rating display:
function RatingDisplay({ score }: { score: number }) {
  const targetRow = Math.max(1, Math.min(5, Math.ceil(score)));
  const goldPercentage = score / targetRow;
  
  return (
    <HStack>
      <VStack>
        <Text>{score.toFixed(1)}</Text>
        <Text>out of 5</Text>
      </VStack>
      
      <VStack>
        {[5, 4, 3, 2, 1].map((stars) => (
          <HStack key={stars}>
            {/* Star icons */}
            <ProgressBar 
              percentage={stars === targetRow ? goldPercentage : 0} 
            />
          </HStack>
        ))}
      </VStack>
    </HStack>
  );
}

Reviews Screen

View individual reviews at app/(mint-flow)/reviews.tsx:
import { useKYMMint } from 'hooks/coco/useKYMMint';

function ReviewsScreen({ mintUrl }: { mintUrl: string }) {
  const { recommendations, loading } = useKYMMint(mintUrl);
  
  return (
    <FlatList
      data={recommendations}
      renderItem={({ item }) => (
        <ReviewItem
          pubkey={item.pubkey}
          score={item.score}
          comment={item.comment}
          created_at={item.created_at}
        />
      )}
    />
  );
}

Nostr Integration

KYM data flows through the Nostr network:
import { useSubscribe } from '@nostr-dev-kit/ndk-mobile';
import { 
  isCashuRecommendationEvent,
  extractMintUrlFromEvent,
  parseRecommendation 
} from 'helper/nostrClient';

// Subscribe to kind 38000 events
const filters = [{ kinds: [38000], limit: 100 }];
const { events, eose } = useSubscribe({ filters });

// Process events
events.forEach((event) => {
  if (!isCashuRecommendationEvent(event)) return;
  
  const mintUrl = extractMintUrlFromEvent(event);
  const recommendation = parseRecommendation(event.content);
  
  // recommendation: { score: number, comment: string }
});

Caching KYM Data

Ratings are cached in kymMintStore.ts:
import { useKYMMintStore } from 'stores/kymMintStore';

const store = useKYMMintStore.getState();

// Cache rating
store.setCached(mintUrl, avgScore, recommendations);

// Retrieve cached
const cached = store.getCached(mintUrl);
if (cached && !store.isStale(mintUrl)) {
  return cached;
}

Mint Info Screen

The comprehensive mint info modal (app/(mint-flow)/info.tsx) combines:

Progress Ring Visualization

function ProgressRing({ 
  progress,    // 0.0 to 1.0 (success rate)
  children 
}) {
  const circumference = 2 * Math.PI * radius;
  const strokeDashoffset = circumference * (1 - progress);
  
  return (
    <Svg>
      {/* Background circle (error color) */}
      <Circle stroke={errorColor} />
      
      {/* Progress circle (success color) */}
      <Circle 
        stroke={successColor}
        strokeDasharray={circumference}
        strokeDashoffset={strokeDashoffset}
      />
      
      {children} {/* Avatar in center */}
    </Svg>
  );
}

Stats Grid

function StatsGrid({ auditInfo }) {
  const stats = [
    {
      label: 'Success Rate',
      value: `${(auditInfo.successRate * 100).toFixed(1)}%`,
      description: `${auditInfo.swapSuccess} of ${auditInfo.swapTotal} swaps`
    },
    {
      label: 'Average Time',
      value: `${Math.round(auditInfo.avgTimeMs)} ms`,
      description: 'For successful swaps'
    },
    {
      label: 'Total Mints',
      value: auditInfo.auditorData.mints,
      description: 'Total mint operations'
    },
    {
      label: 'Total Melts',
      value: auditInfo.auditorData.melts,
      description: 'Total melt operations'
    }
  ];
  
  return (
    <View style={styles.statsGrid}>
      {stats.map((stat) => (
        <StatCard key={stat.label} {...stat} />
      ))}
    </View>
  );
}

Contact Information

function ContactSection({ mintInfo }) {
  const handleContactPress = async (method: string, info: string) => {
    switch (method.toLowerCase()) {
      case 'email':
        await Linking.openURL(`mailto:${info}`);
        break;
      case 'twitter':
      case 'x':
        await Linking.openURL(`https://x.com/${info.replace('@', '')}`);
        break;
      case 'nostr':
        const pubkey = npubToPubkey(info);
        router.navigate('/userMessages', { pubkey });
        break;
      default:
        await Clipboard.setStringAsync(info);
    }
  };
  
  return (
    <ListGroup>
      {mintInfo.contact.map((contact) => (
        <ListGroup.Item onPress={() => handleContactPress(contact.method, contact.info)}>
          {/* Contact item UI */}
        </ListGroup.Item>
      ))}
    </ListGroup>
  );
}

Sorting by Trust Signals

The add mints screen sorts by audit data and KYM score:
const sortedMints = useMemo(() => {
  return [...mints].sort((a, b) => {
    const auditA = getAuditData(a.url);
    const auditB = getAuditData(b.url);
    
    // Calculate success rates
    const successRateA = auditA.auditInfo?.successRate;
    const successRateB = auditB.auditInfo?.successRate;
    const kymScoreA = kymScores[a.url]?.score;
    const kymScoreB = kymScores[b.url]?.score;
    
    // Sort: success rate desc, then KYM score desc
    if (successRateA && successRateB && successRateA !== successRateB) {
      return successRateB - successRateA;
    }
    if (kymScoreA && kymScoreB) {
      return kymScoreB - kymScoreA;
    }
    return 0;
  });
}, [mints, kymScores, getAuditData]);

Trust Badges

Display combined trust indicators:
function MintTrustBadges({ mintUrl }) {
  const { auditInfo } = useAuditedMint(mintUrl);
  const { score: kymScore } = useKYMMint(mintUrl);
  
  const successRate = auditInfo?.successRate 
    ? Math.round(auditInfo.successRate * 100) 
    : undefined;
  
  return (
    <HStack gap={8}>
      {kymScore && (
        <Badge variant="star" icon="ic:round-star">
          {kymScore.toFixed(1)}
        </Badge>
      )}
      
      {successRate !== undefined && (
        <Badge 
          variant={successRate >= 95 ? 'success' : 'error'}
          icon="lucide:activity"
        >
          {successRate}%
        </Badge>
      )}
    </HStack>
  );
}

Best Practices

Use both auditor data (objective) and KYM scores (subjective) for a complete picture. A mint with 99% uptime but low community ratings may have other issues.
Before trusting a mint with large amounts, verify the operator has multiple contact methods and a known reputation.
Success rates can fluctuate. Check audit data periodically, especially before moving large amounts.
When trying a new mint, start with a small balance and test mint/melt operations before increasing exposure.

API Reference

Helper Functions

import { auditMint, fetchMintInfo } from 'helper/apiClient';

// Fetch audit data
const result = await auditMint({ mintUrl });
if (result.isOk()) {
  const auditData = result.value;
}

// Fetch mint info
const infoResult = await fetchMintInfo(mintUrl);
if (infoResult.isOk()) {
  const mintInfo = infoResult.value;
}

Nostr Helpers

import { 
  isCashuRecommendationEvent,
  extractMintUrlFromEvent,
  parseRecommendation 
} from 'helper/nostrClient';

// Validate event
const isValid = isCashuRecommendationEvent(event);

// Extract mint URL
const mintUrl = extractMintUrlFromEvent(event);

// Parse content
const { score, comment } = parseRecommendation(event.content);
// Returns: { score: number, comment: string }

Mint Management

Add and manage mints

Wallet Rebalancing

Distribute balance based on trust signals

Build docs developers (and LLMs) love