# Project export: Truth AI Agent

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: TreeHacks 2025
- Tagline: A decentralized argument resolution agent powered by autonomous, verifiable AI judgment.
- Devpost: https://devpost.com/software/rhetai
- GitHub: https://github.com/owenkrause/truth-ai
- Video: https://www.youtube.com/embed/YVwyPml0RzM?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 1 GitHub contributor(s) — owenkrause (13 commits)

## Devpost submission (written by the team)

### Inspiration

I was inspired by the visual novel "Ace Attorney," the story of a defense attorney who investigates crimes, gathers evidence, and dramatically battles prosecutors in court. I created Truth AI to bring truth back to the internet. With the increase in popularity of AI agents and lucrative social media monetization programs (ie X / Twitter), content has hyper tumbled into an unoriginal spew of click begging slop. Although firms are profiting from the increase in traffic, soon we will reach a point where interaction with a real human on the internet will be a rare occurrence, if you can even recognize it. Truth AI agents takes a stab at helping elevate the quality of content by discouraging low value content while educating users on better practices.

### What it does

Truth AI agents can be deployed on any social media platform. They are powered by autonomous and verifiable AI compute to deliver high quality, unbiased, untampered opinions. By using AI to analyze content, platforms can efficiently eliminate or demonetize content that doesn't meet criteria. Incorporating the AVS allows for transparency. Any user can view the prompt that was used to analyze the content. This ensures that platforms are not injecting their own biases into the AI.

### How we built it

It utilizes Opacity, an Eigenlayer AVS, to make off-chain OpenAI API calls. Thus, any decision the agent makes is a consensus and can be verified; it is impossible for an entity to corrupt the opinion of the agent. The agent lives on an express server with only 1 endpoint necessary to connect to a frontend. The agent can be customized with txt files that the arguments are injected into.

### What's next

for Truth AI Truth AI agents raise the dilemma of whether AI can truly be unbiased. Since we are relying on the AI to pass judgement, it is critical that it is carried out impartially. But given that AI models are trained on datasets created by humans, who carry inherent biases, is it realistic to expect these systems to achieve a level of impartiality that transcends human judgment?

## README (from the GitHub repository)

# Virtual Judge powered by Eigenlayer AVS

An agent that moderates content on social media platforms through consensus-based AI decision making.

<img src="ace-attorney.jpg" width="300" alt="banner">

## Problem

Traditional dispute resolution and argument evaluation systems are:
- Centralized and prone to bias
- Lack transparency in decision-making
- Time-consuming and expensive
- Not easily accessible to everyone


## Solution
- Autonomous and verifiable argument evaluation through Eigenlayer AVS
- Utilizes [Opacity](https://www.opacity.network/) for off-chain AI powered decision making
- Integratable with any platform

## Impact
Many platforms are simply overwhelmed by the number of bots and with recent monetization programs becoming popular (ie X) creators have resorted to prioritizing content that drives clicks, rather than crafting the highest quality posts. By using AI to analyze content, platforms can effeciently eliminate or demonitize content that doesn't meet criteria. Incorporating the AVS allows for transparency. Any user can view the prompt that was used to analyze the content. This ensures that platforms are not injecting their own biases into the filtering.

## Tech Stack
- Frontend: Next.js, TailwindCSS
- Backend: Express.js
- AI Integration: Opacity AVS and OpenAI
- Decentralization: Eigenlayer

<br />
Built with ❤️ for TreeHacks 2025


## Detected evidence (automated analysis)

Indexed codebase: 28 recognized source files, 51 KB.
- CSS (language) — detected in the code
- Express (technology) — detected in the code
- JavaScript (language) — detected in the code
- Next.js (technology) — detected in the code
- React (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code
- OpenAI (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (49 of 49)

```
.env.example
.gitignore
apps/discord/.env.example
apps/discord/.gitignore
apps/discord/commands/judge.js
apps/discord/deploy-commands.js
apps/discord/eslint.config.js
apps/discord/events/commandHandler.js
apps/discord/events/ready.js
apps/discord/index.js
apps/discord/package.json
apps/twitter/.env.example
apps/twitter/.gitignore
apps/twitter/client.ts
apps/twitter/index.ts
apps/twitter/package.json
apps/twitter/reply.ts
apps/twitter/tokens.example.txt
apps/twitter/types.ts
apps/twitter/utils.ts
apps/web/.gitignore
apps/web/components.json
apps/web/eslint.config.mjs
apps/web/next.config.ts
apps/web/package.json
apps/web/postcss.config.mjs
apps/web/README.md
apps/web/src/app/globals.css
apps/web/src/app/layout.tsx
apps/web/src/app/page.tsx
apps/web/src/components/ui/button.tsx
apps/web/src/components/ui/card.tsx
apps/web/src/components/ui/form.tsx
apps/web/src/components/ui/label.tsx
apps/web/src/components/ui/textarea.tsx
apps/web/src/lib/api.ts
apps/web/src/lib/utils.ts
apps/web/tailwind.config.ts
apps/web/tsconfig.json
package.json
README.md
src/agent/createAgent.ts
src/agent/degen.txt
src/agent/legal.txt
src/agent/normal.txt
src/agent/troll.txt
src/lib/api.ts
src/server.ts
tsconfig.json
```

### Dependencies

- apps/discord/package.json: discord.js@^14.18.0, dotenv@^16.4.7, fs@^0.0.1-security
- apps/twitter/package.json: dotenv@^16.4.7, fs@^0.0.1-security
- apps/web/package.json: @eslint/eslintrc@^3, @hookform/resolvers@^4.1.0, @layr-labs/agentkit-eigenda@^0.1.1, @layr-labs/agentkit-opacity@^0.1.1, @radix-ui/react-label@^2.1.2, @radix-ui/react-slot@^1.1.2, @types/node@^20.17.19, @types/react@^19.0.8, @types/react-dom@^19.0.3, class-variance-authority@^0.7.1, clsx@^2.1.1, dotenv@^16.4.7, eslint@^9, eslint-config-next@15.1.7, face-api.js@^0.22.2, lucide-react@^0.475.0, next@15.1.7, postcss@^8, react@^19.0.0, react-dom@^19.0.0, react-hook-form@^7.54.2, tailwind-merge@^3.0.1, tailwindcss@^3.4.1, tailwindcss-animate@^1.0.7, typescript@^5.7.3, zod@^3.24.2
- package.json: @eslint/js@^9.20.0, @layr-labs/agentkit@latest, @layr-labs/agentkit-eigenda@latest, @layr-labs/agentkit-opacity@latest, @layr-labs/agentkit-witnesschain@latest, @types/cors@^2.8.17, @types/express@^4.17.21, @types/node@^20.11.24, @typescript-eslint/eslint-plugin@^7.1.0, @typescript-eslint/parser@^7.1.0, axios@^1.7.9, cors@^2.8.5, discord.js@^14.18.0, dotenv@^16.4.7, eigenda-sdk@latest, eslint@^8.57.1, express@^4.18.3, fs@^0.0.1-security, prettier@^3.2.5, tsx@^4.19.2, typescript@^5.3.3

### Recent commits (newest first)

- Update README.md
- final fix
- feat: completed twitter integration.
- feat: added character files and fixed some bugs
- fix small bug
- feat: move frontends into apps and add discord app
- update readme
- feat: input form for arguments and ai response
- feat: input form for arguments and ai response
- forge install: forge-std
- chore: forge init
- feat: frontend for argument results
- forge install: forge-std
- chore: forge init

## Key source files (fetched from GitHub, selected and truncated for size)

### package.json

```
{
  "name": "virtual-judge",
  "version": "1.0.0",
  "description": "A ready-to-use scaffold for building verifiable AI agents using Layr Labs' AgentKit adapters",
  "main": "dist/index.js",
  "type": "module",
  "scripts": {
    "dev": "tsx watch src/server.ts",
    "build": "tsc",
    "start": "node dist/index.js",
    "example": "ts-node src/index.ts",
    "lint": "eslint . --ext .ts",
    "format": "prettier --write \"src/**/*.ts\""
  },
  "dependencies": {
    "@layr-labs/agentkit": "latest",
    "@layr-labs/agentkit-eigenda": "latest",
    "@layr-labs/agentkit-opacity": "latest",
    "@layr-labs/agentkit-witnesschain": "latest",
    "cors": "^2.8.5",
    "discord.js": "^14.18.0",
    "dotenv": "^16.4.7",
    "eigenda-sdk": "latest",
    "express": "^4.18.3",
    "fs": "^0.0.1-security"
  },
  "devDependencies": {
    "@eslint/js": "^9.20.0",
    "@types/cors": "^2.8.17",
    "@types/express": "^4.17.21",
    "@types/node": "^20.11.24",
    "@typescript-eslint/eslint-plugin": "^7.1.0",
    "@typescript-eslint/parser": "^7.1.0",
    "axios": "^1.7.9",
    "eslint": "^8.57.1",
    "prettier": "^3.2.5",
    "tsx": "^4.19.2",
    "typescript": "^5.3.3"
  }
}

```

### apps/twitter/package.json

```
{
  "name": "twitter",
  "version": "1.0.0",
  "main": "index.js",
  "type": "module",
  "scripts": {
    "start": "tsx watch index.ts",
    "build": "tsc"
  },
  "dependencies": {
    "dotenv": "^16.4.7",
    "fs": "^0.0.1-security"
  }
}

```

### apps/discord/package.json

```
{
  "name": "virtual-judge",
  "version": "1.0.0",
  "main": "index.js",
  "type": "module",
  "scripts": {
    "start": "node index.js",
    "deploy": "node deploy-commands.js"
  },
  "dependencies": {
    "discord.js": "^14.18.0",
    "dotenv": "^16.4.7",
    "fs": "^0.0.1-security"
  }
}

```

### apps/web/package.json

```
{
  "name": "@judge/webapp",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev --turbopack",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "@hookform/resolvers": "^4.1.0",
    "@layr-labs/agentkit-eigenda": "^0.1.1",
    "@layr-labs/agentkit-opacity": "^0.1.1",
    "@radix-ui/react-label": "^2.1.2",
    "@radix-ui/react-slot": "^1.1.2",
    "class-variance-authority": "^0.7.1",
    "clsx": "^2.1.1",
    "dotenv": "^16.4.7",
    "face-api.js": "^0.22.2",
    "lucide-react": "^0.475.0",
    "next": "15.1.7",
    "react": "^19.0.0",
    "react-dom": "^19.0.0",
    "react-hook-form": "^7.54.2",
    "tailwind-merge": "^3.0.1",
    "tailwindcss-animate": "^1.0.7",
    "zod": "^3.24.2"
  },
  "devDependencies": {
    "@eslint/eslintrc": "^3",
    "@types/node": "^20.17.19",
    "@types/react": "^19.0.8",
    "@types/react-dom": "^19.0.3",
    "eslint": "^9",
    "eslint-config-next": "15.1.7",
    "postcss": "^8",
    "tailwindcss": "^3.4.1",
    "typescript": "^5.7.3"
  }
}

```

### src/server.ts

```typescript
import express from "express";
import cors from "cors";
import { createAgent } from "./agent/createAgent.ts";
import dotenv from "dotenv";

dotenv.config();

const app = express();
const port = process.env.PORT || 3000;

let agent: Awaited<ReturnType<typeof createAgent>>;

// Initialize the agent
createAgent().then(a => {
  agent = a;
  console.log("Agent initialized successfully");
}).catch(error => {
  console.error("Failed to initialize agent:", error);
  process.exit(1);
});

// Middleware
app.use(cors());
app.use(express.json());

// Health check endpoint
app.get("/health", (req, res) => {
  res.json({ status: "ok", agentReady: !!agent });
});

// Generate verifiable text endpoint
app.post("/api/generate", async (req, res) => {
  try {
    if (!agent) {
      return res.status(503).json({ error: "Agent not ready" });
    }

    const { characterType, args } = req.body;
    
    if (!characterType) {
      return res.status(400).json({ error: "Prompt is required" });
    }
    if (!args) {
      return res.status(400).json({ error: "Two arguments are required" });
    }

    const result = await agent.generateVerifiableText(characterType, args);
    console.log("Generation result:", result);
    res.json(result);
  } catch (error) {
    console.error("Error generating text:", error);
    res.status(500).json({ 
      error: "Failed to generate text",
      details: error instanceof Error ? error.message : String(error)
    });
  }
});

// Start server
app.listen(port, () => {
  console.log(`🚀 Server running at http://localhost:${port}`);
  console.log("Available endpoints:");
  console.log("  GET  /health");
  console.log("  POST /api/generate");
}); 
```

### apps/twitter/index.ts

```typescript
import { initializeMonitor } from "./client";

(async () => {
  try {
    await initializeMonitor();
  } catch (error) {
    console.error("Monitor error:", error);
    process.exit(1);
  }
})();
```

### apps/discord/index.js

```javascript
import dotenv from "dotenv";
import fs from "fs";
import { Client, Events, GatewayIntentBits, Collection } from "discord.js";

dotenv.config()

const client = new Client({ 
  intents: [
    GatewayIntentBits.Guilds,
    GatewayIntentBits.GuildMessages,
    GatewayIntentBits.MessageContent
  ] 
});

client.commands = new Collection();
const commandFiles = fs
  .readdirSync("./commands")
  .filter((file) => file.endsWith(".js"));

for (const file of commandFiles) {
  const command = await import(`./commands/${file}`);
  client.commands.set(command.default.data.name, command.default);
}

const eventFiles = fs
  .readdirSync("./events")
  .filter((file) => file.endsWith(".js"));

for (const file of eventFiles) {
  const event = await import(`./events/${file}`);
  if (event.default.once) {
    client.once(event.default.name, (...args) =>
      event.default.execute(...args)
    );
  } else {
    client.on(event.default.name, (...args) =>
      event.default.execute(...args)
    );
  }
}

client.login(process.env.DISCORD_TOKEN);
```

### apps/web/src/app/layout.tsx

```typescript
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";

const geistSans = Geist({
  variable: "--font-geist-sans",
  subsets: ["latin"],
});

const geistMono = Geist_Mono({
  variable: "--font-geist-mono",
  subsets: ["latin"],
});

export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body
        className={`${geistSans.variable} ${geistMono.variable} antialiased`}
      >
        {children}
      </body>
    </html>
  );
}

```

### apps/web/src/app/page.tsx

```typescript
"use client"

import { useState, useEffect } from "react"
import Image from "next/image"
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { z } from "zod"
import { api } from "@/lib/api";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Textarea } from "@/components/ui/textarea"
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"

const CHARACTER = "normal";

const formSchema = z.object({
  person1Argument: z.string().min(1, {
    message: "Argument cannot be empty",
  }),
  person2Argument: z.string().min(1, {
    message: "Argument cannot be empty",
  }),
})

export default function Home() {
  const [scene, setScene] = useState<0 | 1 | 2>(0);
  const [currentSprite, setCurrentSprite] = useState<"normal" | "talk">("normal");
  const [showForm, setShowForm] = useState(true);
  const [args, setArguments] = useState<{
    person1: string;
    person2: string;
  } | null>(null);
  const [judgeResponse, setJudgeResponse] = useState<string | null>(null);
  const [isLoading, setIsLoading] = useState(false);
  const [isTransitioning, setIsTransitioning] = useState(false);

  // Generate scene texts based on form values and API response
  const getSceneTexts = () => {
    if (!args) return ["Please submit your arguments first.", "", ""];
    
    return [
      `Person 1: ${args.person1}`,
      `Person 2: ${args.person2}`,
      judgeResponse ? 
        `Judge: ${judgeResponse}` : 
        isLoading ? 
          "Judge: Deliberating..." :
          "Judge: After hearing both arguments, I will now make my decision..."
    ];
  };

  const handleSceneTransition = async () => {
    if (isTransitioning) return;
    
    setIsTransitioning(true);
    setCurrentSprite("talk");
    
    // Talk animation
    await new Promise(resolve => setTimeout(resolve, 500));
    setCurrentSprite("normal");
    
    // Wait for sprite to return to normal before changing scene
    await new Promise(resolve => setTimeout(resolve, 100));
    setScene(((scene + 1) % 3) as 0 | 1 | 2);
    
    setIsTransitioning(false);
  };

  const SceneSelector = ({ type }: { type: 0 | 1 | 2 }) => {
    switch(type) {
      case 0:
        return (
          <div className="relative w-[960px] h-[640px]">
            <Image className="absolute" src="/defense_background.jpg" alt="court background" width={960} height={640} />
            <Image className="absolute" src="/defense_stand.png" alt="court background" width={960} height={640} />
            {currentSprite === "normal" && (
              <Image className="absolute" src="/defense_smirk.gif" alt="" width={960} height={640} />
            )}
            {currentSprite === "talk" && (
              <Image className="absolute" src="/defense_talk.gif" alt="court background" width={960} height={640} />
            )}
          </div>
        )
      case 1:
        return (
          <div className="relative w-[960px] h-[640px]">
            <Image className="absolute" src="/prosecutor_background.jpg" alt="court background" width={960} height={640} />
            <Image className="absolute" src="/prosecutor_stand.png" alt="court background" width={960} height={640} />
            {currentSprite === "normal" && (
              <Image className="absolute" src="/prosecutor_normal.gif" alt="" width={960} height={640} />
            )}
            {currentSprite === "talk" && (
              <Image className="absolute" src="/prosecutor_talk.gif" alt="court background" width={960} height={640} />
            )}
          </div>
        )
      case 2:
        return (
          <div className="relative w-[960px] h-[640px]">
            <Image className="absolute" src="/judge_background.jpg" alt="court background" width={960} height={640} />
            <Image className="absolute" src="/judge_stand.png" alt="court background" width={960} height={640} />
            {currentSprite === "normal" && (
              <Image className="absolute" src="/judge_normal.gif" alt="court background" width={960} height={640} />
            )}
            {currentSprite === "talk" && (
              <Image className="absolute" src="/judge_talk.gif" alt="court background" width={960} height={640} />
            )}
          </div>
        )
    }
  }

  const TextOverlay = ({ text }: { text: string }) => {
    const [displayedText, setDisplayedText] = useState("");
    const [currentIndex, setCurrentIndex] = useState(0);
    const [isCompleted, setIsCompleted] = useState(false);

    useEffect(() => {
      setDisplayedText("");
      setCurrentIndex(0);
      setIsCompleted(false);
    }, [text]);

    useEffect(() => {
      if (currentIndex < text.length) {
        const timer = setTimeout(() => {
          setDisplayedText(prev => prev + text[currentIndex]);
          setCurrentIndex(prev => prev + 1);
        }, 25);
  
        return () => clearTimeout(timer);
      } else {
        setIsCompleted(true);
        if (scene === 2 && !judgeResponse && !isLoading) {
          handleJudgeDecision();
        }
      }
    }, [currentIndex, text, scene, judgeResponse, isLoading]);

    return (
      <div 
        className={`absolute bottom-0 w-full p-6 bg-black/80 
          ${isCompleted && scene !== 2 && !isTransitioning ? "cursor-pointer" : ""}`}
        onClick={() => {
          if (isCompleted && scene !== 2 && !isTransitioning) {
            handleSceneTransition();
          }
        }}
      >
        <div className="max-w-3xl mx-auto">
          <p className="inline text-white font-mono text-lg leading-relaxed">
            {displayedText}
          </p>
          {isCompleted && scene !== 2 && !isTransitioning && (
            <div className="inline text-gray-400 mt-2 animate-pulse">
              &emsp;(click)
            </div>
          )}
        </div>
      </div>
    );
  }

  const
[truncated — 4505 more characters]
```

### apps/web/next.config.ts

```typescript
import type { NextConfig } from "next";

const nextConfig: NextConfig = {};

export default nextConfig;

```

[20 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]