DEV Community

I Built a Serverless X (Twitter) Quote Bot with AWS Lambda + S3 + DynamoDB

I wanted to build something simple but real: a bot that posts a quote to my X account every day — reliably — without me logging in and copy-pasting.

This post walks through the exact architecture and setup I used:

  • S3 for storing quotes (quotes.csv)
  • DynamoDB for tracking which quote is next
  • Secrets Manager for storing X API credentials
  • AWS Lambda for the posting logic
  • EventBridge (optional) to schedule it daily

✅ Result: a serverless bot that posts one quote per run.


Architecture

Flow:

  1. Lambda is triggered (manual test or daily schedule)
  2. Lambda reads quotes.csv from S3
  3. Lambda checks DynamoDB to get the next quote index
  4. Lambda posts the quote to X via API (OAuth 1.0a)
  5. Lambda updates DynamoDB (next_index = next_index + 1)

Prerequisites

  • An AWS account (I used ap-south-1 (Mumbai))
  • An X Developer App with Read and Write permissions enabled
  • Python 3.11 Lambda runtime
  • A basic CSV file with quotes

Step 1: Create your quotes file (CSV)

Create a file named quotes.csv with a header named quote (one quote per line):

quote
Start where you are. Use what you have. Do what you can.
Small steps every day add up to big changes.
Your future needs you—your past doesn’t.
Breathe. This moment is enough.
Progress, not perfection.
You don’t have to see the whole staircase—just take the next step.
Be kind to yourself; you’re learning.
Consistency beats intensity when it’s done daily.
One good decision today can change your tomorrow.
You are capable of more than you think.
Enter fullscreen mode Exit fullscreen mode

Step 2: Upload the CSV to S3

Create an S3 bucket (private) and upload quotes.csv.

Example:

  • Region: ap-south-1 (Mumbai)
  • Bucket: har-vmat-xbot-ap-south-1
  • Key: quotes.csv

Steps:

  1. Go to Amazon S3 → Buckets → Create bucket
  2. Choose your region (Mumbai / ap-south-1)
  3. Keep Block all public access enabled
  4. Create the bucket
  5. Open the bucket → Upload → select quotes.csv → Upload

Step 3: Create DynamoDB table for state

We’ll use DynamoDB to track which quote should be posted next, so we don’t need to modify the CSV file.

Create a DynamoDB table:

  • Table name: x_quote_bot_state
  • Partition key: pk (String)

Steps:

  1. Go to DynamoDB → Tables → Create table
  2. Enter x_quote_bot_state
  3. Partition key: pk (String)
  4. Create table

Now insert the initial state item:

  • pk = har_vmat
  • next_index = 0

Steps:

  1. Open the table → Explore table items
  2. Click Create item
  3. Add attributes:
    • pk (String): har_vmat
    • next_index (Number): 0
  4. Save


Step 4: Store X credentials in AWS Secrets Manager

Store your X OAuth 1.0a credentials in AWS Secrets Manager.

Create a secret:

  • Secret name: x-bot-credentials
  • Secret type: Other type of secret

Add these 4 key/value pairs:

  • consumer_key
  • consumer_secret
  • access_token
  • access_token_secret

Steps:

  1. Go to AWS Secrets Manager → Store a new secret
  2. Choose Other type of secret
  3. Add 4 rows for the keys above and paste the values from your X Developer app
  4. Next → name it x-bot-credentials → store

⚠️ Do not commit these keys to GitHub or hardcode them in Lambda.


Step 5: Create a Lambda Layer for dependencies

Lambda doesn’t include requests-oauthlib by default, so we create a layer containing:

  • requests
  • requests-oauthlib

In AWS CloudShell, run:

mkdir -p x-layer/python
pip3 install -t x-layer/python requests requests-oauthlib
cd x-layer
zip -r x-requests-oauthlib-layer.zip python
ls -lh x-requests-oauthlib-layer.zip
Enter fullscreen mode Exit fullscreen mode

Step 6: Create the Lambda function

Create a Lambda function:

  • Name: x-quote-bot
  • Runtime: Python 3.11
  • Architecture: x86_64

Steps:

  1. Go to AWS Lambda → Functions → Create function
  2. Choose Author from scratch
  3. Function name: x-quote-bot
  4. Runtime: Python 3.11
  5. Architecture: x86_64
  6. Under Permissions, choose Use an existing role and select your role (example: x-quote-bot-lambda-role)
  7. Click Create function


Step 6.1: IAM permissions for the Lambda role

Your Lambda execution role must be able to:

  • Read the CSV from S3
  • Read the secret from Secrets Manager
  • Read/write the DynamoDB state item
  • Write logs to CloudWatch

At minimum, allow these actions:

  • s3:GetObject on arn:aws:s3:::har-vmat-xbot-ap-south-1/quotes.csv
  • secretsmanager:GetSecretValue on the secret x-bot-credentials
  • dynamodb:GetItem and dynamodb:PutItem on table x_quote_bot_state
  • CloudWatch logs permissions (usually via AWSLambdaBasicExecutionRole)


Step 6.2: Attach the Lambda Layer

Attach your dependency layer (created earlier) to the function:

  1. Open your function → scroll to Layers
  2. Click Add a layer
  3. Choose Custom layers
  4. Select your layer (example: x-requests-oauthlib) and pick the latest version
  5. Click Add

After attaching, you should see it listed with:

  • Compatible runtime: python3.11
  • Compatible architecture: x86_64


Step 6.3: Add environment variables

Go to Configuration → Environment variables → Edit and add:

  • BUCKET = har-vmat-xbot-ap-south-1
  • KEY = quotes.csv
  • SECRET_ID = x-bot-credentials
  • TABLE = x_quote_bot_state
  • PK_VALUE = har_vmat

Click Save.


Step 7: Add the Lambda code (CSV from S3 → post to X → update DynamoDB)

Open Code → lambda_function.py, replace the contents with the code below, then click Deploy.

import os, json, csv, io, datetime
import boto3
import requests
from requests_oauthlib import OAuth1

s3 = boto3.client("s3")
ddb = boto3.resource("dynamodb")
secrets = boto3.client("secretsmanager")

BUCKET = os.environ["BUCKET"]
KEY = os.environ["KEY"]
SECRET_ID = os.environ["SECRET_ID"]
TABLE = os.environ["TABLE"]
PK_VALUE = os.environ.get("PK_VALUE", "har_vmat")

X_CREATE_POST_URL = "https://api.x.com/2/tweets"

def get_secret():
    resp = secrets.get_secret_value(SecretId=SECRET_ID)
    return json.loads(resp["SecretString"])

def load_quotes_from_s3():
    obj = s3.get_object(Bucket=BUCKET, Key=KEY)
    data = obj["Body"].read()
    try:
        raw = data.decode("utf-8-sig")
    except UnicodeDecodeError:
        raw = data.decode("cp1252")

    f = io.StringIO(raw)
    reader = csv.DictReader(f)

    quotes = []
    for row in reader:
        q = (row.get("quote") or "").strip()
        if q:
            quotes.append(q)
    return quotes

def get_next_index(table, n_quotes):
    resp = table.get_item(Key={"pk": PK_VALUE})
    idx = int(resp.get("Item", {}).get("next_index", 0))
    return idx % max(n_quotes, 1)

def set_next_index(table, next_idx):
    table.put_item(Item={
        "pk": PK_VALUE,
        "next_index": int(next_idx),
        "updated_at": datetime.datetime.utcnow().isoformat() + "Z"
    })

def post_to_x(creds, text):
    auth = OAuth1(
        creds["consumer_key"],
        creds["consumer_secret"],
        creds["access_token"],
        creds["access_token_secret"],
    )
    payload = {"text": text}
    r = requests.post(X_CREATE_POST_URL, auth=auth, json=payload, timeout=20)
    if r.status_code >= 300:
        raise RuntimeError(f"X post failed: {r.status_code} {r.text}")
    return r.json()

def lambda_handler(event, context):
    creds = get_secret()
    quotes = load_quotes_from_s3()
    if not quotes:
        return {"status": "no_quotes"}

    table = ddb.Table(TABLE)
    idx = get_next_index(table, len(quotes))

    text = " ".join(quotes[idx].split())
    if len(text) > 280:
        text = text[:277] + ""

    resp = post_to_x(creds, text)
    set_next_index(table, idx + 1)

    return {"status": "posted", "index": idx, "tweet": resp}
Enter fullscreen mode Exit fullscreen mode

Step 8: Test it

Now that your Lambda code, environment variables, IAM role, and layer are all set, it’s time to test an end-to-end run.

Steps:

  1. Open AWS Lambda → Functions → x-quote-bot
  2. Go to the Test tab
  3. Create a new test event:

    • Event name: test
    • Event JSON: {}
  4. Click Test

Expected results:

  • Lambda returns something like:
{
  "status": "posted",
  "index": 0,
  "tweet": {
    "data": {
      "text": "Start where you are. Use what you have. Do what you can.",
      "id": "YOUR_TWEET_ID"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode
  • A new post appears on your X profile
  • DynamoDB item pk = har_vmat updates:
    • next_index increments (e.g., 0 → 1)


Conclusion

That’s it — you now have a working, serverless X quote bot powered by AWS:

  • S3 stores your content (quotes.csv)
  • DynamoDB tracks which quote is next
  • Secrets Manager securely stores your X credentials
  • Lambda posts to X using OAuth 1.0a
  • EventBridge runs it on a daily schedule (optional but recommended)

The best part: once it’s set up, it’s boring (in a good way). It just runs.

If you’re building your own version, I’d recommend starting simple (like this), and only then adding improvements like randomization, richer content types, or alerting.


Top comments (0)