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:
- Lambda is triggered (manual test or daily schedule)
- Lambda reads
quotes.csvfrom S3 - Lambda checks DynamoDB to get the next quote index
- Lambda posts the quote to X via API (OAuth 1.0a)
- 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.
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:
- Go to Amazon S3 → Buckets → Create bucket
- Choose your region (Mumbai / ap-south-1)
- Keep Block all public access enabled
- Create the bucket
- 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:
- Go to DynamoDB → Tables → Create table
- Enter
x_quote_bot_state - Partition key:
pk(String) - Create table
Now insert the initial state item:
-
pk=har_vmat -
next_index=0
Steps:
- Open the table → Explore table items
- Click Create item
- Add attributes:
-
pk(String):har_vmat -
next_index(Number):0
-
- 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_keyconsumer_secretaccess_tokenaccess_token_secret
Steps:
- Go to AWS Secrets Manager → Store a new secret
- Choose Other type of secret
- Add 4 rows for the keys above and paste the values from your X Developer app
- 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:
requestsrequests-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
Step 6: Create the Lambda function
Create a Lambda function:
-
Name:
x-quote-bot - Runtime: Python 3.11
- Architecture: x86_64
Steps:
- Go to AWS Lambda → Functions → Create function
- Choose Author from scratch
- Function name:
x-quote-bot - Runtime: Python 3.11
- Architecture: x86_64
- Under Permissions, choose Use an existing role and select your role (example:
x-quote-bot-lambda-role) - 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:GetObjectonarn:aws:s3:::har-vmat-xbot-ap-south-1/quotes.csv -
secretsmanager:GetSecretValueon the secretx-bot-credentials -
dynamodb:GetItemanddynamodb:PutItemon tablex_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:
- Open your function → scroll to Layers
- Click Add a layer
- Choose Custom layers
- Select your layer (example:
x-requests-oauthlib) and pick the latest version - 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}
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:
- Open AWS Lambda → Functions →
x-quote-bot - Go to the Test tab
-
Create a new test event:
- Event name:
test - Event JSON:
{}
- Event name:
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"
}
}
}
- A new post appears on your X profile
- DynamoDB item
pk = har_vmatupdates:-
next_indexincrements (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)