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
Request your archive
Get it from X/Twitter: Settings → Download an archive.
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
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