From 9ef184bd0ea87a7e7a9c84561a0afd069f2a7323 Mon Sep 17 00:00:00 2001
From: Christopher Freytes <Cfreytez@gmail.com>
Date: Wed, 25 Dec 2024 04:27:11 +0000
Subject: [PATCH] Add: Client Reddit Files

---
 packages/client-reddit/.npmignore             |   6 +
 packages/client-reddit/eslint.config.mjs      |   3 +
 packages/client-reddit/package.json           |  22 ++
 packages/client-reddit/src/actions/comment.ts |  49 ++++
 packages/client-reddit/src/actions/post.ts    |  88 ++++++
 packages/client-reddit/src/actions/vote.ts    |  49 ++++
 .../client-reddit/src/clients/redditClient.ts |  52 ++++
 .../src/clients/redditPostClient.ts           | 259 ++++++++++++++++++
 packages/client-reddit/src/index.ts           |  28 ++
 .../src/providers/redditProvider.ts           | 123 +++++++++
 packages/client-reddit/src/types/index.ts     |  18 ++
 packages/client-reddit/tsconfig.json          |  13 +
 packages/client-reddit/tsup.config.ts         |  21 ++
 13 files changed, 731 insertions(+)
 create mode 100644 packages/client-reddit/.npmignore
 create mode 100644 packages/client-reddit/eslint.config.mjs
 create mode 100644 packages/client-reddit/package.json
 create mode 100644 packages/client-reddit/src/actions/comment.ts
 create mode 100644 packages/client-reddit/src/actions/post.ts
 create mode 100644 packages/client-reddit/src/actions/vote.ts
 create mode 100644 packages/client-reddit/src/clients/redditClient.ts
 create mode 100644 packages/client-reddit/src/clients/redditPostClient.ts
 create mode 100644 packages/client-reddit/src/index.ts
 create mode 100644 packages/client-reddit/src/providers/redditProvider.ts
 create mode 100644 packages/client-reddit/src/types/index.ts
 create mode 100644 packages/client-reddit/tsconfig.json
 create mode 100644 packages/client-reddit/tsup.config.ts

diff --git a/packages/client-reddit/.npmignore b/packages/client-reddit/.npmignore
new file mode 100644
index 00000000000..078562eceab
--- /dev/null
+++ b/packages/client-reddit/.npmignore
@@ -0,0 +1,6 @@
+*
+
+!dist/**
+!package.json
+!readme.md
+!tsup.config.ts
\ No newline at end of file
diff --git a/packages/client-reddit/eslint.config.mjs b/packages/client-reddit/eslint.config.mjs
new file mode 100644
index 00000000000..92fe5bbebef
--- /dev/null
+++ b/packages/client-reddit/eslint.config.mjs
@@ -0,0 +1,3 @@
+import eslintGlobalConfig from "../../eslint.config.mjs";
+
+export default [...eslintGlobalConfig];
diff --git a/packages/client-reddit/package.json b/packages/client-reddit/package.json
new file mode 100644
index 00000000000..710c139a742
--- /dev/null
+++ b/packages/client-reddit/package.json
@@ -0,0 +1,22 @@
+{
+  "name": "@ai16z/client-reddit",
+  "version": "0.1.0",
+  "main": "dist/index.js",
+  "type": "module",
+  "types": "dist/index.d.ts",
+  "dependencies": {
+    "@ai16z/eliza": "workspace:*",
+    "snoowrap": "^1.23.0"
+  },
+  "devDependencies": {
+    "@types/node": "^20.0.0",
+    "tsup": "8.3.5",
+    "vitest": "^2.1.4"
+  },
+  "scripts": {
+    "build": "tsup --format esm --dts",
+    "dev": "tsup --format esm --dts --watch",
+    "test": "vitest run",
+    "test:watch": "vitest watch"
+  }
+}
diff --git a/packages/client-reddit/src/actions/comment.ts b/packages/client-reddit/src/actions/comment.ts
new file mode 100644
index 00000000000..fe11bc27625
--- /dev/null
+++ b/packages/client-reddit/src/actions/comment.ts
@@ -0,0 +1,49 @@
+import { Action, IAgentRuntime, Memory } from "@ai16z/eliza";
+
+export const createComment: Action = {
+    name: "CREATE_REDDIT_COMMENT",
+    similes: ["COMMENT_ON_REDDIT", "REPLY_ON_REDDIT"],
+    validate: async (runtime: IAgentRuntime, message: Memory) => {
+        const hasCredentials = !!runtime.getSetting("REDDIT_CLIENT_ID") &&
+                             !!runtime.getSetting("REDDIT_CLIENT_SECRET") &&
+                             !!runtime.getSetting("REDDIT_REFRESH_TOKEN");
+        return hasCredentials;
+    },
+    handler: async (
+        runtime: IAgentRuntime,
+        message: Memory,
+        state: any,
+        options: any
+    ) => {
+        const { reddit } = await runtime.getProvider("redditProvider");
+
+        // Extract post ID and comment content from message
+        const postId = options.postId; // This should be a fullname (t3_postid)
+        const content = message.content.text;
+
+        try {
+            await reddit.getSubmission(postId).reply(content);
+            return true;
+        } catch (error) {
+            console.error("Failed to create Reddit comment:", error);
+            return false;
+        }
+    },
+    examples: [
+        [
+            {
+                user: "{{user1}}",
+                content: {
+                    text: "Comment on this Reddit post: t3_abc123 with: Great post!"
+                },
+            },
+            {
+                user: "{{agentName}}",
+                content: {
+                    text: "I'll add that comment to the Reddit post",
+                    action: "CREATE_REDDIT_COMMENT",
+                },
+            },
+        ],
+    ],
+};
diff --git a/packages/client-reddit/src/actions/post.ts b/packages/client-reddit/src/actions/post.ts
new file mode 100644
index 00000000000..bd3d53c597a
--- /dev/null
+++ b/packages/client-reddit/src/actions/post.ts
@@ -0,0 +1,88 @@
+import { Action, IAgentRuntime, Memory } from "@ai16z/eliza";
+import { RedditPost } from "../types";
+
+export const createPost: Action = {
+    name: "CREATE_REDDIT_POST",
+    similes: ["POST_TO_REDDIT", "SUBMIT_REDDIT_POST"],
+    validate: async (runtime: IAgentRuntime, message: Memory) => {
+        const hasCredentials = !!runtime.getSetting("REDDIT_CLIENT_ID") &&
+                             !!runtime.getSetting("REDDIT_CLIENT_SECRET") &&
+                             !!runtime.getSetting("REDDIT_REFRESH_TOKEN");
+        return hasCredentials;
+    },
+    handler: async (
+        runtime: IAgentRuntime,
+        message: Memory,
+        state: any,
+        options: any
+    ) => {
+        const { reddit } = await runtime.getProvider("redditProvider");
+
+        // Parse the subreddit and content from the message
+        // Expected format: "Post to r/subreddit: Title | Content"
+        const messageText = message.content.text;
+        const match = messageText.match(/Post to r\/(\w+):\s*([^|]+)\|(.*)/i);
+        
+        if (!match) {
+            throw new Error("Invalid post format. Use: Post to r/subreddit: Title | Content");
+        }
+
+        const [_, subreddit, title, content] = match;
+
+        try {
+            const post = await reddit.submitSelfpost({
+                subredditName: subreddit.trim(),
+                title: title.trim(),
+                text: content.trim()
+            });
+
+            return {
+                success: true,
+                data: {
+                    id: post.id,
+                    url: post.url,
+                    subreddit: post.subreddit.display_name,
+                    title: post.title
+                }
+            };
+        } catch (error) {
+            console.error("Failed to create Reddit post:", error);
+            return {
+                success: false,
+                error: error.message
+            };
+        }
+    },
+    examples: [
+        [
+            {
+                user: "{{user1}}",
+                content: {
+                    text: "Post to r/test: My First Post | This is the content of my post"
+                },
+            },
+            {
+                user: "{{agentName}}",
+                content: {
+                    text: "I'll create that post on r/test for you",
+                    action: "CREATE_REDDIT_POST",
+                },
+            },
+        ],
+        [
+            {
+                user: "{{user1}}",
+                content: {
+                    text: "Post to r/AskReddit: What's your favorite book? | I'm curious to know what books everyone loves and why."
+                },
+            },
+            {
+                user: "{{agentName}}",
+                content: {
+                    text: "Creating your post on r/AskReddit",
+                    action: "CREATE_REDDIT_POST",
+                },
+            },
+        ],
+    ],
+};
diff --git a/packages/client-reddit/src/actions/vote.ts b/packages/client-reddit/src/actions/vote.ts
new file mode 100644
index 00000000000..ff4dbe040d7
--- /dev/null
+++ b/packages/client-reddit/src/actions/vote.ts
@@ -0,0 +1,49 @@
+import { Action, IAgentRuntime, Memory } from "@ai16z/eliza";
+
+export const vote: Action = {
+    name: "REDDIT_VOTE",
+    similes: ["UPVOTE_ON_REDDIT", "DOWNVOTE_ON_REDDIT"],
+    validate: async (runtime: IAgentRuntime, message: Memory) => {
+        const hasCredentials = !!runtime.getSetting("REDDIT_CLIENT_ID") &&
+                             !!runtime.getSetting("REDDIT_CLIENT_SECRET") &&
+                             !!runtime.getSetting("REDDIT_REFRESH_TOKEN");
+        return hasCredentials;
+    },
+    handler: async (
+        runtime: IAgentRuntime,
+        message: Memory,
+        state: any,
+        options: any
+    ) => {
+        const { reddit } = await runtime.getProvider("redditProvider");
+
+        // Extract target ID and vote direction
+        const targetId = options.targetId; // fullname of post/comment
+        const direction = options.direction; // 1 for upvote, -1 for downvote
+
+        try {
+            await reddit.getSubmission(targetId).upvote(); // or downvote()
+            return true;
+        } catch (error) {
+            console.error("Failed to vote on Reddit:", error);
+            return false;
+        }
+    },
+    examples: [
+        [
+            {
+                user: "{{user1}}",
+                content: {
+                    text: "Upvote this Reddit post: t3_abc123"
+                },
+            },
+            {
+                user: "{{agentName}}",
+                content: {
+                    text: "I'll upvote that post for you",
+                    action: "REDDIT_VOTE",
+                },
+            },
+        ],
+    ],
+};
diff --git a/packages/client-reddit/src/clients/redditClient.ts b/packages/client-reddit/src/clients/redditClient.ts
new file mode 100644
index 00000000000..153b05507a4
--- /dev/null
+++ b/packages/client-reddit/src/clients/redditClient.ts
@@ -0,0 +1,52 @@
+import { IAgentRuntime, elizaLogger } from "@ai16z/eliza";
+import { RedditProvider } from "../providers/redditProvider";
+import { RedditPostClient } from "./redditPostClient";
+import Snoowrap from "snoowrap";
+
+export class RedditClient {
+    provider: RedditProvider;
+    postClient: RedditPostClient;
+    runtime: IAgentRuntime;
+
+    constructor(runtime: IAgentRuntime) {
+        this.runtime = runtime;
+        const reddit = new Snoowrap({
+            userAgent: runtime.getSetting("REDDIT_USER_AGENT"),
+            clientId: runtime.getSetting("REDDIT_CLIENT_ID"),
+            clientSecret: runtime.getSetting("REDDIT_CLIENT_SECRET"),
+            refreshToken: runtime.getSetting("REDDIT_REFRESH_TOKEN")
+        });
+        this.provider = new RedditProvider(runtime, reddit);
+        this.postClient = new RedditPostClient(runtime, this.provider);
+    }
+
+    async start() {
+        elizaLogger.info("Starting Reddit client");
+        await this.provider.start();
+
+        const autoPost = this.runtime.getSetting("REDDIT_AUTO_POST") === "true";
+        if (autoPost) {
+            elizaLogger.info("Auto-posting enabled for Reddit");
+            await this.postClient.start(true);
+        }
+    }
+
+    async stop() {
+        elizaLogger.info("Stopping Reddit client");
+        await this.postClient.stop();
+    }
+
+    async submitPost(subreddit: string, title: string, content: string) {
+        try {
+            const post = await this.provider.submitSelfpost({
+                subredditName: subreddit,
+                title,
+                text: content
+            });
+            return post;
+        } catch (error) {
+            elizaLogger.error("Error submitting Reddit post:", error);
+            throw error;
+        }
+    }
+}
\ No newline at end of file
diff --git a/packages/client-reddit/src/clients/redditPostClient.ts b/packages/client-reddit/src/clients/redditPostClient.ts
new file mode 100644
index 00000000000..eb6b9bf752d
--- /dev/null
+++ b/packages/client-reddit/src/clients/redditPostClient.ts
@@ -0,0 +1,259 @@
+import { IAgentRuntime, elizaLogger, generateText, ModelClass, composeContext } from "@ai16z/eliza";
+import { RedditProvider } from "../providers/redditProvider";
+
+const redditPostTemplate = `
+# Areas of Expertise
+{{knowledge}}
+
+# About {{agentName}}:
+{{bio}}
+{{lore}}
+{{topics}}
+
+{{providers}}
+
+{{characterPostExamples}}
+
+{{postDirections}}
+
+# Task: Generate a Reddit post in the voice and style of {{agentName}}.
+Write a post for r/{{subreddit}} that is {{adjective}} about {{topic}}.
+Title should be brief, engaging, and use only basic alphanumeric characters.
+Content should be 2-4 sentences, natural and conversational. No markdown, emojis, or special characters.
+
+Format your response as:
+Title: <your title>
+Content: <your content>`;
+
+export class RedditPostClient {
+    runtime: IAgentRuntime;
+    reddit: RedditProvider;
+    private stopProcessing: boolean = false;
+
+    constructor(runtime: IAgentRuntime, reddit: RedditProvider) {
+        this.runtime = runtime;
+        this.reddit = reddit;
+    }
+
+    async start(postImmediately: boolean = false) {
+        if (postImmediately) {
+            await this.generateNewPost();
+        }
+
+        this.startPostingLoop();
+    }
+
+    private async startPostingLoop() {
+        while (!this.stopProcessing) {
+            try {
+                const lastPost = await this.runtime.cacheManager.get<{
+                    timestamp: number;
+                }>("reddit/lastPost");
+
+                const lastPostTimestamp = lastPost?.timestamp ?? 0;
+                const minMinutes = parseInt(this.runtime.getSetting("POST_INTERVAL_MIN")) || 90;
+                const maxMinutes = parseInt(this.runtime.getSetting("POST_INTERVAL_MAX")) || 180;
+                const randomMinutes = Math.floor(Math.random() * (maxMinutes - minMinutes + 1)) + minMinutes;
+                const delay = randomMinutes * 60 * 1000;
+
+                if (Date.now() > lastPostTimestamp + delay) {
+                    await this.generateNewPost();
+                }
+
+                elizaLogger.log(`Next Reddit post scheduled in ${randomMinutes} minutes`);
+                await new Promise(resolve => setTimeout(resolve, delay));
+            } catch (error) {
+                elizaLogger.error("Error in Reddit posting loop:", error);
+                await new Promise(resolve => setTimeout(resolve, 5 * 60 * 1000)); // Wait 5 minutes on error
+            }
+        }
+    }
+
+    private async submitWithRateLimit(subreddit: string, title: string, content: string) {
+        elizaLogger.debug("Applying rate limit before submission");
+        await new Promise(resolve => setTimeout(resolve, 2000));
+
+        try {
+            // Validate content lengths
+            if (title.length === 0 || title.length > 300) {
+                throw new Error(`Title length must be between 1-300 characters (current: ${title.length})`);
+            }
+            if (content.length > 40000) {
+                throw new Error(`Content exceeds maximum length of 40,000 characters (current: ${content.length})`);
+            }
+
+            // Clean up the content
+            const cleanTitle = title
+                .replace(/[^\w\s-]/g, '') // Remove all non-word chars except spaces and hyphens
+                .replace(/\s+/g, ' ')     // Replace multiple spaces with single space
+                .trim();
+
+            const cleanContent = content
+                .replace(/\*[^*]*\*/g, '')  // Remove markdown asterisks and content between them
+                .replace(/[^\w\s.,!?-]/g, '') // Keep only basic punctuation
+                .replace(/\s+/g, ' ')       // Replace multiple spaces with single space
+                .trim();
+
+            // Validate after cleaning
+            if (!cleanTitle || !cleanContent) {
+                throw new Error("Title or content is empty after cleaning");
+            }
+
+            // Log submission attempt
+            elizaLogger.info("Attempting Reddit submission:", {
+                subreddit,
+                cleanTitle,
+                titleLength: cleanTitle.length,
+                contentLength: cleanContent.length,
+                userAgent: this.reddit.reddit.userAgent,
+                hasAuth: !!this.reddit.reddit.accessToken
+            });
+
+            const post = await this.reddit.submitSelfpost({
+                subredditName: subreddit,
+                title: cleanTitle,
+                text: cleanContent
+            });
+
+            elizaLogger.info("Post submitted successfully:", {
+                url: `https://reddit.com${post.permalink}`,
+                subreddit: post.subreddit_name_prefixed,
+                title: post.title,
+                upvotes: post.score
+            });
+
+            return post;
+        } catch (error) {
+            const errorDetails = error.response?.body || error.message;
+            elizaLogger.error("Reddit API submission error:", {
+                error: {
+                    name: error.name,
+                    message: error.message,
+                    details: errorDetails,
+                    status: error.statusCode,
+                    stack: error.stack
+                },
+                submission: {
+                    subreddit,
+                    titleLength: title.length,
+                    contentLength: content.length,
+                    hasAuth: !!this.reddit.reddit.accessToken,
+                    userAgent: this.reddit.reddit.userAgent
+                }
+            });
+            throw error;
+        }
+    }
+
+    private async generateNewPost() {
+        elizaLogger.info("=== Starting Reddit Post Generation ===");
+
+        try {
+            // Log settings check
+            elizaLogger.debug("Checking Reddit credentials:", {
+                hasClientId: !!this.runtime.getSetting("REDDIT_CLIENT_ID"),
+                hasClientSecret: !!this.runtime.getSetting("REDDIT_CLIENT_SECRET"),
+                hasRefreshToken: !!this.runtime.getSetting("REDDIT_REFRESH_TOKEN")
+            });
+
+            // Subreddit selection
+            const subreddits = (this.runtime.getSetting("REDDIT_SUBREDDITS") || "test").split(",");
+            const subreddit = subreddits[Math.floor(Math.random() * subreddits.length)].trim();
+            elizaLogger.info(`Selected subreddit: r/${subreddit}`);
+
+            // State composition
+            elizaLogger.debug("Composing state for post generation");
+            const state = await this.runtime.composeState(
+                {
+                    userId: this.runtime.agentId,
+                    roomId: `reddit-${this.runtime.agentId}`,
+                    content: { text: "", action: "POST" }
+                },
+                { subreddit }
+            );
+            elizaLogger.debug("State composed successfully", { state });
+
+            // Context creation
+            elizaLogger.debug("Creating context with template");
+            const context = composeContext({
+                state,
+                template: redditPostTemplate
+            });
+            elizaLogger.debug("Context created successfully");
+
+            // Text generation
+            elizaLogger.info("Generating post content...");
+            const response = await generateText({
+                runtime: this.runtime,
+                context,
+                modelClass: ModelClass.MEDIUM
+            });
+            elizaLogger.debug("Raw generated response:", response);
+
+            // Response parsing
+            const titleMatch = response.match(/Title:\s*(.*)/i);
+            const contentMatch = response.match(/Content:\s*(.*)/is);
+
+            if (!titleMatch || !contentMatch) {
+                elizaLogger.error("Failed to parse post content:", {
+                    response,
+                    hasTitleMatch: !!titleMatch,
+                    hasContentMatch: !!contentMatch
+                });
+                return;
+            }
+
+            const title = titleMatch[1].trim();
+            const content = contentMatch[1].trim();
+            elizaLogger.info("Parsed post content:", {
+                title,
+                content,
+                titleLength: title.length,
+                contentLength: content.length
+            });
+
+            // Dry run check
+            if (this.runtime.getSetting("REDDIT_DRY_RUN") === "true") {
+                elizaLogger.info(`[DRY RUN] Would post to r/${subreddit}:`, {
+                    title,
+                    content
+                });
+                return;
+            }
+
+            // Post submission
+            elizaLogger.info(`Attempting to submit post to r/${subreddit}`);
+            try {
+                const post = await this.submitWithRateLimit(subreddit, title, content);
+
+                // Cache update
+                elizaLogger.debug("Updating last post cache");
+                await this.runtime.cacheManager.set("reddit/lastPost", {
+                    id: post.id,
+                    timestamp: Date.now()
+                });
+
+                elizaLogger.success(`Successfully posted to Reddit: ${post.url}`);
+            } catch (error) {
+                // ... existing error handling ...
+            }
+
+        } catch (error) {
+            elizaLogger.error("Error in Reddit post generation:", {
+                error: error instanceof Error ? {
+                    name: error.name,
+                    message: error.message,
+                    stack: error.stack
+                } : error,
+                runtime: {
+                    agentId: this.runtime.agentId,
+                    hasRedditProvider: !!this.reddit
+                }
+            });
+        }
+    }
+
+    async stop() {
+        this.stopProcessing = true;
+    }
+}
\ No newline at end of file
diff --git a/packages/client-reddit/src/index.ts b/packages/client-reddit/src/index.ts
new file mode 100644
index 00000000000..1a73d52dc2b
--- /dev/null
+++ b/packages/client-reddit/src/index.ts
@@ -0,0 +1,28 @@
+import { Plugin, Client } from "@ai16z/eliza";
+import { createPost } from "./actions/post";
+import { createComment } from "./actions/comment";
+import { vote } from "./actions/vote";
+import { redditProvider } from "./providers/redditProvider";
+import { RedditClient } from "./clients/redditClient";
+
+export const RedditClientInterface: Client = {
+    async start(runtime) {
+        const client = new RedditClient(runtime);
+        await client.start();
+        return client;
+    },
+    async stop(runtime) {
+        // Cleanup logic
+    }
+};
+
+export const redditPlugin: Plugin = {
+    name: "reddit",
+    description: "Reddit Plugin for Eliza - Interact with Reddit posts, comments and voting",
+    actions: [createPost, createComment, vote],
+    providers: [redditProvider],
+    evaluators: [],
+    clients: [RedditClientInterface]
+};
+
+export default redditPlugin;
diff --git a/packages/client-reddit/src/providers/redditProvider.ts b/packages/client-reddit/src/providers/redditProvider.ts
new file mode 100644
index 00000000000..cdc3d3b8f68
--- /dev/null
+++ b/packages/client-reddit/src/providers/redditProvider.ts
@@ -0,0 +1,123 @@
+import { Provider, IAgentRuntime, elizaLogger } from "@ai16z/eliza";
+import Snoowrap from "snoowrap";
+import { RedditPostClient } from "../clients/redditPostClient";
+
+export class RedditProvider {
+    postClient: RedditPostClient;
+    runtime: IAgentRuntime;
+    reddit: Snoowrap;
+
+    constructor(runtime: IAgentRuntime, reddit?: Snoowrap) {
+        this.runtime = runtime;
+        if (reddit) {
+            this.reddit = reddit;
+        } else {
+            // Use the provided user agent or build one
+            const userAgent = this.runtime.getSetting("REDDIT_USER_AGENT");
+
+            this.reddit = new Snoowrap({
+                userAgent,
+                clientId: runtime.getSetting("REDDIT_CLIENT_ID"),
+                clientSecret: runtime.getSetting("REDDIT_CLIENT_SECRET"),
+                refreshToken: runtime.getSetting("REDDIT_REFRESH_TOKEN"),
+                accessToken: this.runtime.getSetting("REDDIT_ACCESS_TOKEN"),
+                // Add these options for better auth handling
+                endpointDomain: 'oauth.reddit.com',
+                requestDelay: 1000,
+                continueAfterRatelimitError: true,
+                retryErrorCodes: [502, 503, 504, 522],
+                maxRetryAttempts: 3
+            });
+
+            // Configure additional options
+            this.reddit.config({
+                debug: true,
+                proxies: false,
+                requestTimeout: 30000
+            });
+        }
+        this.postClient = new RedditPostClient(runtime, this);
+    }
+
+    async start() {
+        try {
+            elizaLogger.info("Starting Reddit authentication with:", {
+                userAgent: this.reddit.userAgent,
+                clientIdExists: !!this.runtime.getSetting("REDDIT_CLIENT_ID"),
+                secretExists: !!this.runtime.getSetting("REDDIT_CLIENT_SECRET"),
+                tokenExists: !!this.runtime.getSetting("REDDIT_REFRESH_TOKEN"),
+                accessTokenExists: !!this.runtime.getSetting("REDDIT_ACCESS_TOKEN")
+            });
+
+            // Test authentication
+            const me = await this.reddit.getMe();
+            elizaLogger.info("Reddit authentication successful", {
+                username: me.name,
+                karma: me.link_karma + me.comment_karma
+            });
+
+            // Start the post client after successful auth
+            const postImmediately = this.runtime.getSetting("POST_IMMEDIATELY") === "true";
+            await this.postClient.start(postImmediately);
+
+        } catch (error) {
+            elizaLogger.error("Reddit authentication failed", {
+                error: error.message,
+                response: error.response?.body,
+                statusCode: error.statusCode,
+                headers: error.response?.headers
+            });
+            throw error;
+        }
+    }
+
+    async submitSelfpost({ subredditName, title, text }: {
+        subredditName: string;
+        title: string;
+        text: string;
+    }) {
+        try {
+            elizaLogger.info(`Attempting to submit post to r/${subredditName}:`, {
+                title,
+                contentLength: text.length
+            });
+
+            const subreddit = await this.reddit.getSubreddit(subredditName);
+            const post = await subreddit.submitSelfpost({ title, text });
+
+            elizaLogger.success(`Successfully posted to Reddit:`, {
+                url: `https://reddit.com${post.permalink}`,
+                subreddit: subredditName,
+                title: post.title,
+                upvotes: post.score
+            });
+
+            return post;
+        } catch (error) {
+            elizaLogger.error("Failed to submit post", {
+                error: error.message,
+                subreddit: subredditName,
+                isAuthenticated: !!this.reddit.accessToken
+            });
+            throw error;
+        }
+    }
+}
+
+export const redditProvider: Provider = {
+    provide: async (runtime: IAgentRuntime) => {
+        const userAgent = runtime.getSetting("REDDIT_USER_AGENT");
+
+        const reddit = new Snoowrap({
+            userAgent,
+            clientId: runtime.getSetting("REDDIT_CLIENT_ID"),
+            clientSecret: runtime.getSetting("REDDIT_CLIENT_SECRET"),
+            refreshToken: runtime.getSetting("REDDIT_REFRESH_TOKEN"),
+            endpointDomain: 'oauth.reddit.com'
+        });
+
+        const provider = new RedditProvider(runtime, reddit);
+        await provider.start();
+        return { reddit: provider };
+    }
+};
diff --git a/packages/client-reddit/src/types/index.ts b/packages/client-reddit/src/types/index.ts
new file mode 100644
index 00000000000..bcca1ad8a59
--- /dev/null
+++ b/packages/client-reddit/src/types/index.ts
@@ -0,0 +1,18 @@
+export interface RedditPost {
+    id: string;
+    subreddit: string;
+    title: string;
+    content: string;
+    author: string;
+    score: number;
+    created: Date;
+}
+
+export interface RedditComment {
+    id: string;
+    postId: string;
+    content: string;
+    author: string;
+    score: number;
+    created: Date;
+}
diff --git a/packages/client-reddit/tsconfig.json b/packages/client-reddit/tsconfig.json
new file mode 100644
index 00000000000..834c4dce269
--- /dev/null
+++ b/packages/client-reddit/tsconfig.json
@@ -0,0 +1,13 @@
+{
+    "extends": "../core/tsconfig.json",
+    "compilerOptions": {
+        "outDir": "dist",
+        "rootDir": "src",
+        "types": [
+            "node"
+        ]
+    },
+    "include": [
+        "src/**/*.ts"
+    ]
+}
\ No newline at end of file
diff --git a/packages/client-reddit/tsup.config.ts b/packages/client-reddit/tsup.config.ts
new file mode 100644
index 00000000000..b5e4388b214
--- /dev/null
+++ b/packages/client-reddit/tsup.config.ts
@@ -0,0 +1,21 @@
+import { defineConfig } from "tsup";
+
+export default defineConfig({
+    entry: ["src/index.ts"],
+    outDir: "dist",
+    sourcemap: true,
+    clean: true,
+    format: ["esm"], // Ensure you're targeting CommonJS
+    external: [
+        "dotenv", // Externalize dotenv to prevent bundling
+        "fs", // Externalize fs to use Node.js built-in module
+        "path", // Externalize other built-ins if necessary
+        "@reflink/reflink",
+        "@node-llama-cpp",
+        "https",
+        "http",
+        "agentkeepalive",
+        "zod",
+        // Add other modules you want to externalize
+    ],
+});