Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# Operation Whiskers

Classified
Classified
1 change: 1 addition & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const eslintConfig = defineConfig([
"out/**",
"build/**",
"next-env.d.ts",
"src/generated/**",
]),
]);

Expand Down
57 changes: 57 additions & 0 deletions prisma/migrations/20251208_add_wagering/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
-- CreateEnum
CREATE TYPE "WagerStatus" AS ENUM ('open', 'active', 'pending_verification', 'resolved', 'cancelled');

-- CreateEnum
CREATE TYPE "VerificationType" AS ENUM ('subjective', 'deadline', 'photo_proof');

-- CreateTable
CREATE TABLE "Wager" (
"id" TEXT NOT NULL,
"conversationId" TEXT NOT NULL,
"creatorPhone" TEXT NOT NULL,
"title" TEXT NOT NULL,
"condition" TEXT NOT NULL,
"sides" TEXT[],
"verificationType" "VerificationType" NOT NULL,
"verificationConfig" JSONB,
"status" "WagerStatus" NOT NULL DEFAULT 'open',
"deadline" TIMESTAMP(3),
"result" TEXT,
"resolvedAt" TIMESTAMP(3),
"proofUrl" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,

CONSTRAINT "Wager_pkey" PRIMARY KEY ("id")
);

-- CreateTable
CREATE TABLE "WagerPosition" (
"id" TEXT NOT NULL,
"wagerId" TEXT NOT NULL,
"phone" TEXT NOT NULL,
"side" TEXT NOT NULL,
"amount" DECIMAL(10,2) NOT NULL,
"matchedAmount" DECIMAL(10,2) NOT NULL DEFAULT 0,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,

CONSTRAINT "WagerPosition_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE INDEX "Wager_conversationId_status_idx" ON "Wager"("conversationId", "status");

-- CreateIndex
CREATE INDEX "Wager_deadline_idx" ON "Wager"("deadline");

-- CreateIndex
CREATE INDEX "WagerPosition_wagerId_idx" ON "WagerPosition"("wagerId");

-- CreateIndex
CREATE INDEX "WagerPosition_phone_idx" ON "WagerPosition"("phone");

-- AddForeignKey
ALTER TABLE "WagerPosition" ADD CONSTRAINT "WagerPosition_wagerId_fkey" FOREIGN KEY ("wagerId") REFERENCES "Wager"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- Add wageringEnabled flag to GroupChatSettings
ALTER TABLE "GroupChatSettings" ADD COLUMN "wageringEnabled" BOOLEAN NOT NULL DEFAULT false;
64 changes: 64 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,9 @@ model GroupChatSettings {
// Users can describe how they want the AI to behave via natural language
customPrompt String? @db.Text

// Wagering feature toggle - enables/disables wagering functionality for this group
wageringEnabled Boolean @default(false)

createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

Expand All @@ -362,3 +365,64 @@ model FeatureRequest {
@@index([createdAt])
@@index([phoneNumber])
}

// ============================================
// Group Chat Wagering
// ============================================

enum WagerStatus {
open // Market open, accepting positions (may have unmatched bets)
active // At least some amount matched, wager in progress
pending_verification // Deadline passed, awaiting resolution
resolved // Winner declared, payouts calculated
cancelled // Cancelled before any matching
}

enum VerificationType {
subjective // Group vote or honor system
deadline // Auto-resolves when time expires
photo_proof // Requires photo evidence
}

model Wager {
id String @id @default(cuid())
conversationId String // Group chat ID
creatorPhone String // Who proposed (doesn't have to bet)

title String // "Pizza Arrival"
condition String // "Arrives before 7:45 PM"
sides String[] // e.g. ["YES", "NO"] or ["UNDER", "OVER"]

verificationType VerificationType
verificationConfig Json? // { deadline?: string, ... }

status WagerStatus @default(open)
deadline DateTime? // When betting closes / event happens
result String? // Winning side
resolvedAt DateTime?
proofUrl String?

positions WagerPosition[]

createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

@@index([conversationId, status])
@@index([deadline])
}

model WagerPosition {
id String @id @default(cuid())
wagerId String
wager Wager @relation(fields: [wagerId], references: [id], onDelete: Cascade)

phone String // Who placed this position
side String // Which side they're on
amount Decimal @db.Decimal(10, 2) // Total amount wagered
matchedAmount Decimal @db.Decimal(10, 2) @default(0) // Amount matched against other side

createdAt DateTime @default(now())

@@index([wagerId])
@@index([phone])
}
2 changes: 2 additions & 0 deletions src/ai/agents/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ export function defaultBuildContext(context: ConversationContext): string {
systemState: context.systemState,
groupParticipants: context.groupParticipants,
sender: context.sender,
groupChatCustomPrompt: context.groupChatCustomPrompt,
wageringEnabled: context.wageringEnabled,
});
}

Expand Down
4 changes: 4 additions & 0 deletions src/ai/agents/systemPrompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ export function buildConversationContextPrompt(context: {
groupParticipants?: GroupParticipantInfo[] | null;
sender?: string; // The authenticated sender (for group chats)
groupChatCustomPrompt?: string | null; // Custom behavior prompt for this group chat
wageringEnabled?: boolean; // Whether wagering/betting is enabled for this group chat
}): string {
const parts: string[] = [];

Expand Down Expand Up @@ -225,6 +226,9 @@ export function buildConversationContextPrompt(context: {
parts.push(
"- GROUP CHAT ETIQUETTE: Do NOT spam. Respond in 1 message (max 2). Often a reaction is better than a message. You do NOT need to respond to everything.",
);
if (context.wageringEnabled) {
parts.push("- Wagering/betting is enabled for this group chat. Use the relevant tools if participants are discussing bets or wagers.");
}

// Custom group chat behavior (if configured)
if (context.groupChatCustomPrompt) {
Expand Down
6 changes: 4 additions & 2 deletions src/ai/respondToMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
createRequestResearchTool,
createScheduledJobTools,
createUpdateUserContextTool,
createWagerTools,
} from "@/src/ai/tools";
import { hasActiveConnections } from "@/src/ai/tools/helpers";
import type { ConversationContext } from "@/src/db/conversation";
Expand Down Expand Up @@ -89,10 +90,11 @@ export async function respondToMessage(
createGenerateConnectionLinkTool(context);
}

// Add group chat-only tools (settings management)
// Add group chat-only tools (settings management, wagering)
if (context.isGroup) {
const groupTools = createGroupChatSettingsTools(context);
Object.assign(contextBoundTools, groupTools);
const wagerTools = createWagerTools(context);
Object.assign(contextBoundTools, groupTools, wagerTools);
}

// Conditionally merge integration tools only if user has connections
Expand Down
2 changes: 2 additions & 0 deletions src/ai/tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
* - Create and manage scheduled jobs
* - Configure group chat settings
* - Record feature requests from users
* - Orchestrate wagers/bets in group chats
*
* Security: User-identity-sensitive tools are context-bound factories.
* The phone number is taken from authenticated context, not user input,
Expand All @@ -25,6 +26,7 @@ export { createRequestFeatureTool } from "./requestFeature";
export { createRequestResearchTool } from "./requestResearch";
export { createUpdateUserContextTool } from "./updateUserContext";
export { createScheduledJobTools } from "./scheduledJob";
export { createWagerTools } from "./wager";

// Static tools (no context needed - Claude can see images directly)
export { createImageTool } from "./createImage";
Expand Down
Loading