For the complete documentation index, see llms.txt. This page is also available as Markdown.
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
// 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
};
# .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
npm i alith node-cron
# or pnpm add alith node-cron
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;
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();
});