Skip to main content
iStory combines voice-first journaling with AI-powered insights and blockchain permanence to create a sovereign memory infrastructure.

Core Features

Voice-to-Text

Record stories naturally using browser audio capture with AI transcription

AI Insights

Automatic extraction of themes, emotions, and patterns from your entries

Local Vault

Client-side AES-256-GCM encryption with PIN-protected key management

Blockchain Verification

Chainlink CRE attestation for verifiable story quality metrics

Pattern Discovery

AI aggregates themes and domains across all your stories over time

Social Sharing

Share stories publicly, earn tokens from engagement, and build community

NFT Compilation

Compile stories into digital books and mint as ERC721 NFTs on Base

Web3 Native

Dual auth (Google + Wallet), token rewards, and smart contract integration

1. Voice-to-Text Journaling

How It Works

iStory uses ElevenLabs Scribe for high-accuracy speech-to-text transcription:
app/api/ai/transcribe/route.ts
export async function POST(request: NextRequest) {
  // 1. Validate auth and file size
  const authenticatedUserId = await validateAuthOrReject(request);
  const formData = await request.formData();
  const audioFile = formData.get('audio') as File;
  
  if (audioFile.size > 25_000_000) {
    return NextResponse.json({ error: 'File too large' }, { status: 400 });
  }

  // 2. Convert to FormData for ElevenLabs
  const elevenlabsFormData = new FormData();
  elevenlabsFormData.append('audio', audioFile);
  elevenlabsFormData.append('model_id', 'scribe-v1');

  // 3. Call ElevenLabs Scribe API
  const response = await fetch(
    'https://api.elevenlabs.io/v1/scribe',
    {
      method: 'POST',
      headers: { 'xi-api-key': process.env.ELEVENLABS_API_KEY! },
      body: elevenlabsFormData,
    }
  );

  const data = await response.json();
  return NextResponse.json({ text: data.text });
}
const stream = await navigator.mediaDevices.getUserMedia({
  audio: {
    echoCancellation: true,
    noiseSuppression: true,
    sampleRate: 16000,
  },
});

const mediaRecorder = new MediaRecorder(stream);
const audioChunks: Blob[] = [];

mediaRecorder.ondataavailable = (e) => {
  audioChunks.push(e.data);
};

mediaRecorder.onstop = () => {
  const audioBlob = new Blob(audioChunks, { type: 'audio/webm' });
  transcribeAudio(audioBlob);
};

mediaRecorder.start();

Audio Storage

Recorded audio is uploaded to Supabase Storage and linked to your story:
const uploadAudio = async (audioBlob: Blob) => {
  const filename = `${Date.now()}-${Math.random().toString(36)}.webm`;
  const { data, error } = await supabase.storage
    .from('story-audio')
    .upload(`public/${filename}`, audioBlob);

  const { data: { publicUrl } } = supabase.storage
    .from('story-audio')
    .getPublicUrl(data.path);

  return publicUrl;
};
Audio files are stored indefinitely and can be played back from the Library or Story detail page.

2. AI Enhancement & Analysis

Text Enhancement

Use Google Gemini 2.5 Flash to polish your writing:
app/api/ai/enhance/route.ts
export async function POST(request: NextRequest) {
  const { text } = await request.json();
  
  const model = genAI.getGenerativeModel({ model: 'gemini-2.0-flash-exp' });
  const prompt = `
You are a writing assistant. Improve the following journal entry:
- Fix grammar and spelling
- Improve clarity and flow
- Maintain the author's voice and intent
- Keep the same length

Original text:
${text}

Improved version:`;

  const result = await model.generateContent(prompt);
  const enhanced = result.response.text();
  
  return NextResponse.json({ text: enhanced });
}

Story Analysis

Cognitive metadata extraction happens automatically after saving:
app/api/ai/analyze/route.ts
export async function POST(request: NextRequest) {
  const { storyId, storyText } = await request.json();
  
  // Verify ownership
  const story = await admin.from('stories').select('*').eq('id', storyId).single();
  if (story.data.author_id !== authenticatedUserId) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 403 });
  }

  const model = genAI.getGenerativeModel({ model: 'gemini-2.0-flash-exp' });
  const analysisPrompt = `
Analyze this journal entry and extract:
1. Main themes (max 5 keywords)
2. Emotional tone (one word: hopeful, reflective, anxious, etc.)
3. Life domain (relationships, work, health, personal_growth, etc.)
4. Significance score (0-10)
5. Key entities (people, places, times mentioned)

Story:
${storyText}

Return JSON only.`;

  const result = await model.generateContent(analysisPrompt);
  const metadata = JSON.parse(result.response.text());

  // Save to database
  await admin.from('story_metadata').upsert({
    story_id: storyId,
    themes: metadata.themes,
    emotional_tone: metadata.emotional_tone,
    life_domain: metadata.life_domain,
    significance_score: metadata.significance_score,
    entities: metadata.entities,
    word_count: storyText.split(/\s+/).length,
  });

  return NextResponse.json({ success: true, metadata });
}
{
  "themes": ["self-reflection", "change", "intentionality", "growth"],
  "emotional_tone": "contemplative",
  "life_domain": "personal_growth",
  "significance_score": 8.2,
  "entities": {
    "people": [],
    "places": [],
    "times": ["this week", "past year"]
  },
  "word_count": 58,
  "reading_time_minutes": 1
}

3. Local Vault Encryption

Client-side encryption using the Web Crypto API for maximum privacy:

Architecture

Implementation

lib/vault/keyManager.ts
export async function setupVault(pin: string): Promise<void> {
  // 1. Generate Data Encryption Key (DEK)
  const dek = await crypto.subtle.generateKey(
    { name: 'AES-GCM', length: 256 },
    true,
    ['encrypt', 'decrypt']
  );

  // 2. Derive Key Encryption Key (KEK) from PIN
  const salt = crypto.getRandomValues(new Uint8Array(16));
  const pinKey = await crypto.subtle.importKey(
    'raw',
    new TextEncoder().encode(pin),
    'PBKDF2',
    false,
    ['deriveKey']
  );

  const kek = await crypto.subtle.deriveKey(
    { name: 'PBKDF2', salt, iterations: 100_000, hash: 'SHA-256' },
    pinKey,
    { name: 'AES-KW', length: 256 },
    false,
    ['wrapKey', 'unwrapKey']
  );

  // 3. Wrap DEK with KEK
  const wrappedDek = await crypto.subtle.wrapKey('raw', dek, kek, 'AES-KW');

  // 4. Store salt + wrapped DEK in IndexedDB
  await db.vault_metadata.put({
    id: 1,
    salt: Array.from(salt),
    wrapped_dek: Array.from(new Uint8Array(wrappedDek)),
    created_at: Date.now(),
  });

  // 5. Hold DEK in-memory
  inMemoryKeys.set('dek', dek);
}
PIN is never stored. If you lose your PIN, encrypted data cannot be recovered. Always back up important stories to the cloud.

Privacy-preserving blockchain attestation using Chainlink Compute Runtime Environment:

How It Works

Workflow Implementation

cre/iStory_workflow/main.ts
import { Runner, Step, WorkflowContext } from '@chainlink/cre';
import { ConfidentialHTTPClient } from '@chainlink/cre/http';
import { analyzeStory } from './gemini';
import { httpCallback } from './httpCallback';

const workflow = new Runner({
  name: 'iStory_workflow',
  version: '1.0.0',
  steps: [
    new Step({
      id: 'fetch-story',
      handler: async (ctx: WorkflowContext) => {
        const { storyId } = ctx.input;
        const response = await fetch(`${ctx.env.APP_URL}/api/stories/${storyId}`);
        const story = await response.json();
        return { story };
      },
    }),
    new Step({
      id: 'analyze',
      handler: async (ctx: WorkflowContext) => {
        const { story } = ctx.state;
        const metrics = await analyzeStory(story.content, ctx.secrets.GOOGLE_API_KEY);
        return { metrics };
      },
    }),
    new Step({
      id: 'callback',
      handler: async (ctx: WorkflowContext) => {
        await httpCallback(ctx.input.storyId, ctx.state.metrics, ctx.env.CALLBACK_SECRET);
        return { success: true };
      },
    }),
  ],
});

export default workflow;

On-Chain Storage (Privacy-Preserving)

contracts/PrivateVerifiedMetrics.sol
contract PrivateVerifiedMetrics is IReceiver, AccessControl {
    struct StoryProof {
        uint8 qualityTier;        // 1-5 (only tier, not raw score)
        bool meetsThreshold;       // true if score >= 6.0
        bytes32 contentHash;       // SHA256 of story content
        bytes32 metadataHash;      // SHA256 of full metrics JSON
        uint256 timestamp;
        bool verified;
    }

    mapping(string => StoryProof) public storyProofs;

    function onReport(bytes memory rawReport) external override {
        // Decode CRE report
        (string memory storyId, StoryProof memory proof) = abi.decode(
            rawReport, 
            (string, StoryProof)
        );

        storyProofs[storyId] = proof;
        emit StoryVerified(storyId, proof.qualityTier, proof.meetsThreshold);
    }
}
Privacy model: Only minimal proof data (tier, threshold, hashes) is stored on-chain. Full metrics remain in Supabase, accessible only to the story author.

Check Verification Status

app/hooks/useVerifiedMetrics.ts
export function useVerifiedMetrics(storyId: string) {
  const [metrics, setMetrics] = useState(null);
  const [proof, setProof] = useState(null);

  useEffect(() => {
    const check = async () => {
      const res = await fetch('/api/cre/check', {
        method: 'POST',
        body: JSON.stringify({ storyId }),
      });

      const data = await res.json();
      // data.isAuthor determines what you see:
      // - Author: full metrics + proof
      // - Public: proof only (tier, threshold)
      
      setMetrics(data.metrics);
      setProof(data.proof);
    };

    check();
  }, [storyId]);

  return { metrics, proof, isVerified: !!proof };
}

5. Pattern Discovery

AI aggregates themes and domains across all your stories:
app/hooks/usePatterns.ts
export function usePatterns() {
  const [patterns, setPatterns] = useState(null);

  useEffect(() => {
    const fetchPatterns = async () => {
      const { data: metadata } = await supabase
        .from('story_metadata')
        .select('themes, life_domain, emotional_tone')
        .eq('author_id', userId);

      // Aggregate themes
      const themeFreq = metadata.flatMap(m => m.themes)
        .reduce((acc, theme) => {
          acc[theme] = (acc[theme] || 0) + 1;
          return acc;
        }, {});

      // Aggregate domains
      const domainFreq = metadata.map(m => m.life_domain)
        .reduce((acc, domain) => {
          acc[domain] = (acc[domain] || 0) + 1;
          return acc;
        }, {});

      setPatterns({ themes: themeFreq, domains: domainFreq });
    };

    fetchPatterns();
  }, [userId]);

  return patterns;
}
import { ThemesView } from '@/components/patterns/ThemesView';

<ThemesView patterns={patterns} />
Displays a word cloud or bar chart of your most common themes.

6. Social Features

Like System

app/api/social/like/route.ts
export async function POST(request: NextRequest) {
  const { storyId } = await request.json();
  const authenticatedUserId = await validateAuthOrReject(request);

  // Check if already liked
  const { data: existing } = await admin
    .from('likes')
    .select('*')
    .eq('story_id', storyId)
    .eq('user_id', authenticatedUserId)
    .maybeSingle();

  if (existing) {
    // Unlike
    await admin.from('likes').delete().eq('id', existing.id);
    await admin.from('stories').update({ likes: db.raw('likes - 1') }).eq('id', storyId);
    return NextResponse.json({ liked: false });
  } else {
    // Like
    await admin.from('likes').insert({ story_id: storyId, user_id: authenticatedUserId });
    await admin.from('stories').update({ likes: db.raw('likes + 1') }).eq('id', storyId);
    return NextResponse.json({ liked: true });
  }
}

Tip System

app/hooks/useStoryProtocol.ts
export function useStoryProtocol() {
  const { data: hash, writeContract } = useWriteContract();

  const tipCreator = async (to: Address, amount: bigint) => {
    return writeContract({
      address: STORY_PROTOCOL_ADDRESS,
      abi: StoryProtocolABI,
      functionName: 'tipCreator',
      args: [to, amount],
    });
  };

  return { tipCreator, hash };
}
const { tipCreator } = useStoryProtocol();
const amount = parseEther('10'); // 10 $STORY tokens

await tipCreator(authorAddress, amount);
toast.success('Tip sent!');

7. NFT Book Compilation

Compile stories into ERC721 NFTs on Base:
app/api/book/compile/route.ts
export async function POST(request: NextRequest) {
  const { title, description, storyIds } = await request.json();
  const authenticatedUserId = await validateAuthOrReject(request);

  // Verify all stories belong to user
  const { data: stories } = await admin
    .from('stories')
    .select('*')
    .in('id', storyIds)
    .eq('author_id', authenticatedUserId);

  if (stories.length !== storyIds.length) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 403 });
  }

  // Generate PDF
  const pdfBuffer = await generatePDF({
    title,
    stories: stories.map(s => ({ title: s.title, content: s.content })),
  });

  // Upload to IPFS
  const ipfsHash = await ipfsService.uploadFile(pdfBuffer, `${title}.pdf`);

  // Save book record
  const { data: book } = await admin.from('books').insert({
    author_id: authenticatedUserId,
    title,
    description,
    story_ids: storyIds,
    ipfs_hash: ipfsHash,
  }).select().single();

  return NextResponse.json({ success: true, book });
}

Mint NFT

app/hooks/useStoryNFT.ts
export function useStoryNFT() {
  const { writeContract } = useWriteContract();

  const mintBook = async (ipfsHash: string) => {
    return writeContract({
      address: STORY_NFT_ADDRESS,
      abi: StoryNFTABI,
      functionName: 'mint',
      args: [ipfsHash],
      value: parseEther('0.001'), // Mint fee
    });
  };

  return { mintBook };
}
Mint fee: 0.001 ETH per NFT to prevent spam. Fee goes to contract owner.

8. Web3 Integration

Smart Contracts (Base Sepolia)

eStoryToken

ERC20 token for rewards
  • Max supply: 100M
  • Role-based minting
  • Pausable

StoryProtocol

Tips & paywall payments
  • Token transfers
  • Event logging

StoryNFT

ERC721 book NFTs
  • Mint fee: 0.001 ETH
  • ERC2981 royalties
  • IPFS metadata

Token Balance

import { useEStoryToken } from '@/app/hooks/useIStoryToken';

function TokenBalance() {
  const { balance, symbol } = useEStoryToken();

  return (
    <div>
      {balance} {symbol}
    </div>
  );
}

Transaction Example

import { useStoryProtocol } from '@/app/hooks/useStoryProtocol';
import { parseEther } from 'viem';

const { tipCreator } = useStoryProtocol();
const { address } = useAccount();

// Send 50 $STORY tokens
const hash = await tipCreator(
  '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb',
  parseEther('50')
);

await waitForTransaction({ hash });
toast.success('Tip sent successfully!');

API Rate Limits

All API routes are protected by rate limiting:
Route PatternLimitWindow
/api/ai/*10 requests1 minute
/api/auth/*20 requests1 minute
/api/cre/callback30 requests1 minute
Default60 requests1 minute
Rate limits are enforced via middleware.ts using in-memory storage.

Next Steps

API Reference

Explore all API endpoints and request/response formats

Vault Setup

Set up client-side encryption for maximum privacy

Smart Contracts

Learn about token economics and NFT minting

Security

Understand authentication, authorization, and data protection
Questions? Join our Discord community or check the GitHub discussions.

Build docs developers (and LLMs) love