Build your Digital Twin using LazAI

Your Digital Twin is an AI persona that speaks in your voice. We generate it from your Twitter/X archive, store it in a portable Characterfile (character.json), and load it into an Alith agent that writes tweets for you (manually or on a schedule).


Why a Digital Twin?

  • Portable persona: Single JSON file that any LLM agent can use

  • Separation of concerns: Keep style/persona in JSON; keep logic in code

  • Composable: Swap personas without touching the app


Prerequisites

  • macOS/WSL/Linux with Node.js 18+

  • An OpenAI or Anthropic (Claude) API key

  • A Twitter/X account + your archive .zip


Step 0 — Clone the starter kit and install the dependencies

git clone https://github.com/0xLazAI/Digital-Twin-Starter-kit.git

cd Digital-Twin-Starter-kit

Step 1 — Generate your Characterfile from Tweets

  1. Request your archive

    Get it from X/Twitter: Settings → Download an archive.

  2. Run the generator

# You can run this anywhere; no cloning needed
npx tweets2character ~/Downloads/twitter-YYYY-MM-DD-<hash>.zip
  • Choose openai or claude

  • Paste your API key when prompted

  • Output: a character.json in the current directory

  1. Move it into the Digital-Twin-Starter-kit Directrory

    Place character.json at your root (same level as index.js), e.g.:

/Digital-Twin-Starter-kit
  ├─ controller/
  ├─ routes/
  ├─ services/
  ├─ character.json   ← here
  └─ index.js

Step 2 — Feed character.json to an Alith Agent

We create an Alith agent at runtime and pass your character as a preamble. This keeps persona separate from code and makes it hot‑swappable.

  • Loads character.json

  • Builds an Alith preamble from bio/lore/style

  • Generates a tweet in your voice

  • Provides postTweet (manual) and autoTweet (cron) helpers

// controller/twitterController.js
const { initializeTwitterClient } = require('../config/twitter');

const fs = require('fs');
const path = require('path');

// Load character data
const characterData = JSON.parse(
  fs.readFileSync(path.join(__dirname, '../character.json'), 'utf8')
);

// alith function with our character 
const alithfunction = async (username = "") => {
  try {
    const { Agent, LLM } = await import('alith');

    const preamble = [
      `You are ${characterData.name}.`,
      characterData.bio?.join(' ') || '',
      characterData.lore ? `Lore: ${characterData.lore.join(' ')}` : '',
      characterData.adjectives ? `Traits: ${characterData.adjectives.join(', ')}` : '',
      characterData.style?.post ? `Style for posts: ${characterData.style.post.join(' ')}` : '',
    ].filter(Boolean).join('\n');

    const model = LLM.from_model_name(process.env.LLM_MODEL || 'gpt-4o-mini');
    const agent = Agent.new('twitter_agent', model).preamble(preamble);

    const prompt = [
      `Write one tweet in ${characterData.name}'s voice.`,
      username ? `Optionally greet @${username}.` : '',
      `<=240 chars, no code blocks, hashtags only if essential.`
    ].join(' ');

    const chat = agent.chat();
    const result = await chat.user(prompt).complete();
    const text = (result?.content || '').toString().trim();

    if (!text) throw new Error('Empty tweet from agent');
    return text.slice(0, 240);
  } catch (err) {
    // Fallback to examples if Alith/model is unavailable
    const examples = characterData.postExamples || [];
    const base = examples[Math.floor(Math.random() * examples.length)] || 'Hello from my agent!';
    return username ? `${base} @${username}`.slice(0, 240) : base.slice(0, 240);
  }
};

const generateQuirkyMessage = async (username) => {
  return await alithfunction(username);
};

let twitterClient = null;

// New function for cron job - posts tweet without requiring request/response
const postTweetCron = async () => {
  try {
    console.log('Cron job: Starting tweet posting...');

    // Initialize Twitter client if not already initialized
    if (!twitterClient) {
      console.log('Cron job: Initializing Twitter client...');
      twitterClient = await initializeTwitterClient();
    }

    // Generate message for cron job (you can customize this)
    const message = await generateQuirkyMessage('cron');
    
    console.log('Cron job: Posting tweet with message:', message);

    // Send the tweet
    const tweetResult = await twitterClient.sendTweet(message);
    console.log('Cron job: Tweet result:', tweetResult);

    // Log success
    const tweetId = tweetResult.id || tweetResult.id_str;
    if (tweetId) {
      const tweetUrl = `https://twitter.com/${process.env.TWITTER_USERNAME}/status/${tweetId}`;
      console.log('Cron job: Tweet posted successfully:', tweetUrl);
    } else {
      console.log('Cron job: Tweet posted but no ID received');
    }

    return { success: true, message: 'Tweet posted via cron job' };

  } catch (error) {
    console.error('Cron job: Error in postTweetCron:', error);
    
    if (error.message.includes('authentication')) {
      twitterClient = null;
    }
    
    throw error;
  }
};

const postTweet = async (req, res) => {
  console.log('Received request body:', req.body);

  try {
    const { username, address } = req.body;

    console.log('Processing username:', username);

    // Initialize Twitter client if not already initialized
    if (!twitterClient) {
      console.log('Initializing Twitter client...');
      twitterClient = await initializeTwitterClient();
    }

    // Remove @ symbol if included
    const cleanUsername = username.replace('@', '');
    const message = await generateQuirkyMessage(cleanUsername);

    console.log('Posting tweet with message:', message);

    try {
      // Send the tweet
      const tweetResult = await twitterClient.sendTweet(message);
      console.log('Tweet result:', tweetResult);

      // Instead of fetching tweet details again, construct URL from the initial response
      // Most Twitter API responses include either an id or id_str field
      const tweetId = tweetResult.id || tweetResult.id_str;
      console.log(tweetId)
      
      if (!tweetId) {
        console.log('Tweet posted but no ID received:', tweetResult);
        return res.status(200).json({
          success: true,
          message: 'Tweet posted successfully',
          tweetUrl: `https://twitter.com/${process.env.TWITTER_USERNAME}/`, // Fallback URL
          tweetText: message,
          updatedScore: 0
        });
      }

      const tweetUrl = `https://twitter.com/${process.env.TWITTER_USERNAME}/status/${tweetId}`;
      console.log('Constructed tweet URL:', tweetUrl);

      return res.status(200).json({
        success: true,
        message: 'Tweet posted successfully',
        tweetUrl,
        tweetText: message,
        updatedScore: 0
      });

    } catch (tweetError) {
      console.error('Error with tweet operation:', tweetError);
      throw tweetError;
    }

  } catch (error) {
    console.error('Error in postTweet:', error);
    
    if (error.message.includes('authentication')) {
      twitterClient = null;
    }

    return res.status(500).json({
      error: 'Failed to post tweet',
      details: error.message
    });
  }
};



module.exports = {
  postTweet,
  postTweetCron

};

Environment

# .env

TWITTER_USERNAME=username
TWITTER_PASSWORD=password
TWITTER_EMAIL=email

# Alith / LLM
LLM_MODEL=gpt-4o-mini
ALITH_API_KEY=your_key_if_required   # only if your Alith setup needs it

Install deps:

npm i alith node-cron
# or pnpm add alith node-cron

Step 3 — Auto‑Tweet via Cron (uses your Digital Twin)

We schedule the agent to post automatically (e.g., at minute 5 every hour).

services/cronService.js

const cron = require('node-cron');
const { postTweetCron } = require('../controller/twitterController');

class CronService {
  constructor() {
    this.isRunning = false;
  }

  start() {
    if (this.isRunning) {
      console.log('Cron service is already running');
      return;
    }

    console.log('Starting cron service...');
    
    // Schedule tweet posting every 1 minute
    cron.schedule('* * * * *', async () => {
      console.log('Cron job triggered: Posting tweet...');
      try {
        await postTweetCron();
        console.log('Tweet posted successfully via cron job');
      } catch (error) {
        console.error('Error in cron job tweet posting:', error);
      }
    }, {
      scheduled: true,
      timezone: "UTC"
    });

    this.isRunning = true;
    console.log('Cron service started successfully. Tweets will be posted every minute.');
  }

  stop() {
    if (!this.isRunning) {
      console.log('Cron service is not running');
      return;
    }

    console.log('Stopping cron service...');
    cron.getTasks().forEach(task => task.stop());
    this.isRunning = false;
    console.log('Cron service stopped');
  }

  getStatus() {
    return {
      isRunning: this.isRunning,
      nextRun: this.isRunning ? 'Every minute' : 'Not scheduled'
    };
  }
}

module.exports = CronService; 

index.js (ESM)

import express from "express";
import cors from "cors";


import dotenv from 'dotenv';
dotenv.config();
import twitterRoutes from './routes/twitterRoutes.js';
import CronService from './services/cronService.js';

const app = express();
const cronService = new CronService();

app.use(cors({
  origin: '*', // Allow all origins
}));


app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// Debug middleware to log requests
app.use((req, res, next) => {
  console.log('Received request:', {
    method: req.method,
    path: req.path,
    body: req.body,
    headers: req.headers
  });
  next();
});

app.use('/api', twitterRoutes);

// Add cron status endpoint
app.get('/api/cron/status', (req, res) => {
  const status = cronService.getStatus();
  res.json(status);
});

// Add cron control endpoints (optional - for manual control)
app.post('/api/cron/start', (req, res) => {
  cronService.start();
  res.json({ message: 'Cron service started' });
});

app.post('/api/cron/stop', (req, res) => {
  cronService.stop();
  res.json({ message: 'Cron service stopped' });
});


// Error handling middleware
app.use((err, req, res, next) => {
  console.error('Server error:', err);
  res.status(500).json({
    error: 'Internal server error',
    details: err.message
  });
});

app.use(express.json({ limit: '10mb' })); 
app.use(express.urlencoded({ limit: '10mb', extended: true }));




const port = process.env.PORT || 3005;
app.listen(port, () => {
  console.log(`Server started on port ${port}`);
  
  // Start the cron service
  cronService.start();
});

Route

// routes/twitterRoutes.js
const express = require('express');
const router = express.Router();
const { postTweet } = require('../controller/twitterController');

router.post('/tweet', postTweet);
module.exports = router;

Manual test

curl -X POST http://localhost:3000/tweet \
  -H "Content-Type: application/json" \
  -d '{"username":"someone"}'

npm run dev

Updating Your Twin

  • Re‑run npx tweets2character any time you want a fresh persona.

  • Replace character.json and restart your server.

  • The agent will immediately pick up the new style/preamble.


Tips & Gotchas

  • CommonJS + ESM: Alith is ESM, your project is CJS → use dynamic import inside functions (shown above).

  • Length control: We trim to 240 chars to be safe with links/quoted tweets.

  • Fallbacks: If the model isn’t reachable, we fall back to postExamples from your character file.

  • Safety: Add your own guardrails (e.g., profanity, duplicates, rate limits) before posting.


Last updated