Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/sovranBitcoin/sovran/llms.txt

Use this file to discover all available pages before exploring further.

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