The Bren Tipping System is a Next.js-based application that enables users to tip each other using a points-based system integrated with Farcaster and other platforms. The system manages user allowances, tracks transactions, and maintains a leaderboard system.
- Frontend: Next.js, React, TypeScript, Tailwind CSS
- Backend: Next.js API Routes
- Database: PostgreSQL with Prisma ORM
- Authentication: NextAuth.js with SIWE (Sign-in with Ethereum)
- Points System: Stack Protocol
- Social Platform: Neynar API (Farcaster)
- Additional Integrations: Telegram, Slack
- Webhooks: Process incoming cast notifications from Farcaster
- Points Management: Track user points and allowances via Stack Protocol
- User Management: Handle user creation, authentication, and profile data
- Tip Processing: Validate and execute tip transactions
- Bot Replies: Automated responses to user interactions
Create a .env file with the following variables:
# Database
POSTGRES_PRISMA_URL=postgresql://username:password@host:port/database
POSTGRES_URL_NON_POOLING=postgresql://username:password@host:port/database
# Neynar (Farcaster API)
NEYNAR_API_KEY=your_neynar_api_key
SIGNER_UUID=your_signer_uuid
# Stack Protocol (Points System)
STACK_API_KEY=your_stack_api_key
STACK_POINT_SYSTEM_ID=your_point_system_id
# NextAuth
NEXTAUTH_URL=https://your-domain.com
NEXTAUTH_SECRET=your_nextauth_secret
# External APIs
DUNE_API_KEY=your_dune_api_key
AIRSTACK_API_KEY=your_airstack_api_key
ALCHEMY_API_KEY=your_alchemy_api_key
# Telegram Bot
TELEGRAM_BOT_TOKEN=your_telegram_bot_token
WEBHOOK_URL=https://your-domain.com/api/telegramWebhook
# Base URL
BASE_URL=https://your-domain.com
NEXT_PUBLIC_BASE_URL=https://your-domain.com
# Node Environment
NODE_ENV=productionThe application uses several key database models:
model User {
id String @id @default(cuid())
name String?
email String? @unique
walletAddress String? @unique
fid Int? @unique
tgUsername String? @unique
isAllowanceGiven Boolean?
allowanceGivenAt DateTime? @default(now())
farcasterDetails FarcasterDetails?
telegramDetails TelegramDetails?
sentTransactions Transaction[] @relation("FromUser")
receivedTransactions Transaction[] @relation("ToUser")
userRankings UserRankings?
weeklyPoints WeeklyPoints[]
pointEvents PointEvent[]
}model Transaction {
id String @id @default(uuid())
fromUserId String
toUserId String
amount Float
value String
platform Platform
createdAt DateTime @default(now())
castHash String? @unique
parentCastHash String? @unique
text String?
link String?
fromUser User @relation("FromUser", fields: [fromUserId], references: [id])
toUser User @relation("ToUser", fields: [toUserId], references: [id])
}model PointEvent {
id String @id @default(uuid())
userId String
event Event
amount Float?
points Int
platform Platform
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id])
additionalData Json?
}The system uses webhooks from multiple platforms to process tip transactions:
The system registers webhooks with Neynar to receive notifications about cast activities:
// src/scripts/setWebhook.ts
const webhook = await neynar.publishWebhook("cast.created", {
url: `${process.env.BASE_URL}/api/newWebHook`,
subscription: {
cast_created: {
author_fids: [670648] // Bot FID
}
}
});The system sets up Telegram webhooks to receive bot messages:
// src/pages/api/telegramWebhook.ts
export async function setWebhook() {
const response = await fetch(`https://api.telegram.org/bot${BOT_TOKEN}/setWebhook`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
url: WEBHOOK_URL,
allowed_updates: ["message", "message_reaction"]
}),
});
}- Tipping: Users can tip by mentioning the bot with format: "10 $bren @username"
- Commands: Support for /connectwallet, /checkallowance, /checkpoints, /values
- Group Integration: Bot can be added to Telegram groups
- Reactions: Bot reacts with 👍 to successful tips
Entry Point: /api/newWebHook
- Receives webhook notifications from Neynar
- Validates the incoming data
- Queues the webhook for processing
Processing: /api/process-webhook
The main webhook processing happens in webHookProcessing.ts:
-
Cast Validation
// Extract tip amount using regex const amountFromText = message.match(/\$?\s*(\d+)\s*\$?\s*bren\b/i); if (amountFromText?.[1]) { tipAmount = parseInt(amountFromText[1].replace(/\$/, '')); } // Extract hashtag value const hashtagMatch = message.match(/#(\w+)/); if (hashtagMatch?.[1]) { hashtagValue = hashtagMatch[1]; }
-
User Validation
- Check if user exists in database
- For new users, validate eligibility via whitelist
- Create user record if eligible
-
Allowance Check
const currentAllowance = await getUserCurrentAllowance(walletAddress); const weeklySpent = await getWeeklySpentAmount(userId); const allowanceLeft = currentAllowance - weeklySpent;
-
Tip Processing
- Validate recipient
- Execute transaction
- Update database records
- Post bot reply
Entry Point: /api/telegramWebhook
- Receives updates from Telegram Bot API
- Handles different message types (commands, tips, group additions)
Message Processing:
- Command Handling: Routes to
/api/processCommandfor commands like/connectwallet - Tip Processing: Routes to
/api/processTGTipfor tip messages
// Tip parsing for Telegram
function parseTipMessage(text: string, replyToMessage: any): { amount: number, recipient: string } | null {
const match = text.match(/(?=.*\d)(?=.*\$bren)(?=.*@(\w+)).*/i);
const amountMatch = text.match(/(\d+)/);
const amount = amountMatch ? parseInt(amountMatch[0], 10) : 0;
let recipient = match[1];
// If it's a reply, get the recipient from the replied-to message
if (replyToMessage && replyToMessage.from && replyToMessage.from.username) {
recipient = replyToMessage.from.username;
}
return { amount, recipient };
}The system responds to these types of casts:
- Tip Format:
$[amount] bren #[value](e.g., "$100 bren #integrity") - Recipient: Either parent author or mentioned user
- Requirements: User must be whitelisted and have sufficient allowance
The system responds to:
- Tip Format:
[amount] $bren @usernamewhen bot is mentioned (e.g., "@brenisbot 50 $bren @alice") - Commands: Direct messages or commands starting with
/ - Group Events: Bot being added to groups triggers welcome message
The application integrates with Stack Protocol for points management:
// src/server/stack.ts
export const stack = new StackClient({
apiKey: process.env.STACK_API_KEY,
pointSystemId: Number(process.env.STACK_POINT_SYSTEM_ID)
});When users are approved, they receive an initial allowance based on their user type:
// src/pages/api/functions/setAllowance.ts
switch (userType) {
case UserType.ALLIES:
allowancePoints = 500;
break;
case UserType.WHITELISTED:
allowancePoints = 300;
break;
case UserType.POWER_BADGE:
allowancePoints = 300;
break;
case UserType.SPLITTERS:
allowancePoints = 300;
break;
case UserType.FOLLOWER:
allowancePoints = 20;
break;
}
// Track allowance on Stack Protocol
await stack.track("allowance", {
account: walletAddress,
points: allowancePoints
});Every tip transaction creates a PointEvent record:
// Record the point event
await db.pointEvent.create({
data: {
userId: toUser.id,
event: "FARCASTER_TIP",
points: tipAmount,
platform: "FARCASTER",
},
});The system tracks weekly points for leaderboard purposes:
const weekStart = getWeekStart();
await db.weeklyPoints.upsert({
where: {
userId_weekStart_platform: {
userId: fromUser.id,
weekStart,
platform: "FARCASTER",
},
},
update: {
pointsEarned: { increment: tipAmount },
},
create: {
userId: fromUser.id,
weekStart,
pointsEarned: tipAmount,
platform: "FARCASTER",
},
});Rankings are updated for both sender and recipient:
async function updateUserRankings(userId: string, amount: number, isReceived: boolean) {
await db.userRankings.upsert({
where: { userId: userId },
update: {
[isReceived ? 'tipsReceived' : 'tipsSent']: { increment: amount },
[isReceived ? 'tipsReceivedCount' : 'tipsSentCount']: { increment: 1 }
},
create: {
userId: userId,
[isReceived ? 'tipsReceived' : 'tipsSent']: amount,
[isReceived ? 'tipsReceivedCount' : 'tipsSentCount']: 1
}
});
}The system uses Sign-In with Ethereum (SIWE) for wallet-based authentication:
// src/pages/api/auth/[...nextauth].ts
const siwe = new SiweMessage(JSON.parse(credentials.message));
const result = await siwe.verify({
signature: credentials.signature,
domain: nextAuthUrl.host,
nonce: siwe.nonce,
});
if (result.success) {
const user = await db.user.upsert({
where: { walletAddress: siwe.address },
update: {},
create: { walletAddress: siwe.address },
});
}- Each user has a unique
walletAddressfield - Farcaster FID is linked to the wallet address
- Multiple social accounts can link to the same wallet
- Farcaster: Linked via FID and verified Ethereum addresses
- Telegram: Linked via Telegram username and user ID
- Slack: Linked via Slack username
// User can have details across multiple platforms
model User {
farcasterDetails FarcasterDetails?
telegramDetails TelegramDetails?
// Slack linking handled separately
}
model FarcasterDetails {
fid Int?
display_name String?
username String?
pfp String?
type UserType?
}The system categorizes users into different types with varying allowances:
- ALLIES (500 points): PayItForward participants
- SPLITTERS (300 points): Splitter participants
- POWER_BADGE (300 points): Farcaster power badge holders
- WHITELISTED (300 points): Manually whitelisted users
- INVITED (100-200 points): Users invited by existing members
- FOLLOWER (20 points): Users following the Bren channel
// src/pages/api/functions/checkWhiteList.ts
export async function checkWhitelist(
fid: number,
walletAddress: string,
isPowerBadge: boolean
): Promise<UserType | 'NOT_WHITELISTED'>The eligibility system checks:
- Local whitelist database
- Power badge status
- FBI token holdings
- Channel following status
- Invite relationships
POST /api/newWebHook- Receives Neynar webhooksPOST /api/process-webhook- Processes webhook dataPOST /api/webHookProcessing- Core processing logic
GET /api/getUserDetails-db- Get user details from databasePOST /api/createUser-db- Create new userPOST /api/updateUser-db- Update user information
GET /api/points/[walletAddress]- Get user pointsGET /api/getUserStats- Get user statisticsGET /api/rankings- Get leaderboard data
GET /api/whitelist/fids- Check FID whitelistGET /api/whitelist/fbi-token- Check FBI token holdingsGET /api/whitelist/warpcast- Check Warpcast following
POST /api/telegramWebhook- Telegram bot webhookPOST /api/processTGTip- Process Telegram tipsPOST /api/processCommand- Handle Telegram bot commands
POST /api/slackWebhook- Slack bot webhookPOST /api/processSlackTip- Process Slack tips
Automated Replies
The system posts automated replies to tips via botReply.ts:
// Success reply
await botReplySuccess(
castHash,
`Hey @${fromUsername}!\nYou have successfully tipped ${tipAmount} $bren to @${toUsername} for #${hashtagValue}.`,
toFid,
tipAmount,
allowanceLeft
);
// Failure reply
await botReplyFail(
castHash,
`Hey @${fromUsername}!\nYou cannot tip ${tipAmount} $bren.\nAllowance left : ${currentAllowance} $bren`,
"Your tip failed due to insufficient allowance",
currentAllowance
);Features:
- Tipping: Mention the bot with format
@brenisbot [amount] $bren @username - Reactions: Bot reacts with 👍 emoji to successful tips
- Direct Messages: Responds to user DMs
- Group Support: Can be added to Telegram groups
Commands:
/start- Show available commands/connectwallet- Get wallet connection link/checkallowance- Check remaining weekly allowance/checkpoints- Check total points received/values- Display community values
Processing:
// Telegram tip processing with 500 weekly allowance
const tipsSentThisWeek = await db.transaction.aggregate({
where: {
fromUserId: fromUserId,
createdAt: { gte: startOfWeek },
},
_sum: { amount: true },
});
const remainingAllowance = 500 - (tipsSentThisWeek._sum.amount || 0);The system prevents the bot from tipping itself:
const botFid = 670648; // Bren bot FID
if (neynarCast.author.fid === botFid) {
console.log('Bot cannot tip itself');
return;
}- Regex validation for tip amounts and hashtags
- Wallet address verification
- Cast hash uniqueness checking
- Weekly allowance limits per user type
- Transaction deduplication via cast hash
- Bot reply deduplication
- Whitelist-based user approval
- Ethereum address verification requirement
- Platform-specific validation
- Set up PostgreSQL database
- Configure connection strings in
.env - Run Prisma migrations:
npx prisma db push - Generate Prisma client:
npx prisma generate
- Copy environment variables from the example above
- Configure Neynar webhook endpoints
- Set up Stack Protocol point system
- Configure NextAuth secrets
Run the webhook setup script:
npm run setup-webhooknpm install
npm run devnpm run build
npm startThe application includes comprehensive logging for:
- Webhook processing events
- User creation and updates
- Tip transaction processing
- Error tracking and debugging
Key performance queries for monitoring:
- Weekly tip volumes
- User growth metrics
- Platform-specific activity
- Allowance utilization rates
-
Webhook Processing Failures
- Check Neynar API key validity
- Verify webhook URL accessibility
- Review cast format compliance
-
Database Connection Issues
- Verify PostgreSQL connection strings
- Check database permissions
- Ensure Prisma schema is up to date
-
Points System Integration
- Validate Stack API credentials
- Check point system ID configuration
- Monitor API rate limits
-
Authentication Problems
- Verify NextAuth configuration
- Check SIWE message validation
- Ensure proper wallet connection
Enable debug logging by setting:
NODE_ENV=development
DEBUG=trueThis documentation covers the complete architecture and functionality of the Bren Tipping System. For specific implementation details, refer to the source code in the respective files mentioned throughout this document.