# 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

```bash
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

<br>

3. 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

{% code overflow="wrap" %}

```javascript
// 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

};

```

{% endcode %}

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)

<br>

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

services/cronService.js

```javascript
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; 
```

<br>

index.js (ESM)

```javascript
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

```javascript
// 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"}'
```

```bash
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.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.lazai.network/digital-twin/build-your-digital-twin-using-lazai.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
