diff --git a/apps/web/package.json b/apps/web/package.json index a076bf5..d602faa 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -17,6 +17,7 @@ "react": "19.2.4", "react-dom": "19.2.4", "stripe": "^22.2.0", + "twitter-api-v2": "^1.29.0", "zod": "^3.23.0" }, "devDependencies": { diff --git a/apps/web/src/lib/payment.ts b/apps/web/src/lib/payment.ts index 6d33d00..292da4e 100644 --- a/apps/web/src/lib/payment.ts +++ b/apps/web/src/lib/payment.ts @@ -189,3 +189,58 @@ export async function releasePayment( }, }); } + +export async function executePayout( + tx: Prisma.TransactionClient, + taskId: string, + developerWallet: string, + amount: number, + currency: string, + idempotencyKey: string +) { + const existing = await tx.ledgerEntry.findUnique({ + where: { idempotency_key: idempotencyKey }, + }); + if (existing) { + if (existing.response_status === "SUCCESS") return existing; + throw new Error(`Previous executePayout failed for idempotencyKey: ${idempotencyKey}`); + } + + let transferId = `mock_transfer_${taskId}`; + + // If Stripe is configured and wallet looks like a Stripe Connect Account + if (stripe && developerWallet.startsWith("acct_") && !ALLOW_MCP_CLAIM_WITHOUT_STRIPE) { + try { + const transfer = await stripe.transfers.create({ + amount: amount, + currency: currency.toLowerCase(), + destination: developerWallet, + description: `Bounty Payout for Task ${taskId}`, + }, { idempotencyKey }); + transferId = transfer.id; + } catch (error: any) { + await tx.ledgerEntry.create({ + data: { + task_id: taskId, + phase: "PAYOUT", + idempotency_key: idempotencyKey, + stripe_object_id: null, + response_status: "FAILED", + http_status: error.statusCode || 500, + }, + }); + throw error; + } + } + + return await tx.ledgerEntry.create({ + data: { + task_id: taskId, + phase: "PAYOUT", + idempotency_key: idempotencyKey, + stripe_object_id: transferId, + response_status: "SUCCESS", + http_status: 200, + }, + }); +} diff --git a/apps/web/src/lib/x-broadcaster.ts b/apps/web/src/lib/x-broadcaster.ts index 4c2bf0c..49628b4 100644 --- a/apps/web/src/lib/x-broadcaster.ts +++ b/apps/web/src/lib/x-broadcaster.ts @@ -1,8 +1,5 @@ -/** - * X (Twitter) FOMO Broadcaster - * 用於發送病毒式行銷推文,製造錯失恐懼 (FOMO)。 - */ import { sendTrafficAlert } from "./traffic-alert"; +import { TwitterApi } from "twitter-api-v2"; export type FomoEventType = "HIGH_VALUE_BOUNTY" | "SPEED_RUN" | "A2A_SUBCONTRACT"; @@ -31,9 +28,14 @@ function generateTweetText(event: FomoEvent): string { export async function broadcastFomoEvent(event: FomoEvent) { const text = generateTweetText(event); - const bearerToken = process.env.TWITTER_BEARER_TOKEN; + + // Use App Key/Secret + Access Token/Secret for User Context Posting (OAuth 1.0a) + const appKey = process.env.TWITTER_API_KEY; + const appSecret = process.env.TWITTER_API_SECRET; + const accessToken = process.env.TWITTER_ACCESS_TOKEN; + const accessSecret = process.env.TWITTER_ACCESS_SECRET; - if (!bearerToken) { + if (!appKey || !appSecret || !accessToken || !accessSecret) { // Mock Mode: Log to console and send to internal traffic alert console.log(`[X Broadcaster Mock] Would tweet: "${text}"`); void sendTrafficAlert({ @@ -48,23 +50,16 @@ export async function broadcastFomoEvent(event: FomoEvent) { } try { - // 簡單的 Fetch 實作,針對 Twitter V2 API - const response = await fetch("https://api.twitter.com/2/tweets", { - method: "POST", - headers: { - "Authorization": `Bearer ${bearerToken}`, - "Content-Type": "application/json", - }, - body: JSON.stringify({ text }), + const client = new TwitterApi({ + appKey, + appSecret, + accessToken, + accessSecret, }); - if (!response.ok) { - const errText = await response.text(); - console.error("[X Broadcaster] API Error:", errText); - } else { - console.log("[X Broadcaster] Successfully posted tweet."); - } + const response = await client.v2.tweet(text); + console.log("[X Broadcaster] Successfully posted tweet. Tweet ID:", response.data.id); } catch (err) { - console.error("[X Broadcaster] Network Error:", err); + console.error("[X Broadcaster] Twitter API Error:", err); } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b6e6743..6dce6d4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -66,6 +66,9 @@ importers: stripe: specifier: ^22.2.0 version: 22.2.0(@types/node@20.19.42) + twitter-api-v2: + specifier: ^1.29.0 + version: 1.29.0 zod: specifier: ^3.23.0 version: 3.25.76 @@ -2921,6 +2924,9 @@ packages: engines: {node: '>=18.0.0'} hasBin: true + twitter-api-v2@1.29.0: + resolution: {integrity: sha512-v473q5bwme4N+DWSg6qY+JCvfg1nSJRWwui3HUALafxfqCvVkKiYmS/5x/pVeJwTmyeBxexMbzHwnzrH4h6oYQ==} + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -4630,8 +4636,8 @@ snapshots: '@next/eslint-plugin-next': 16.2.7 eslint: 9.39.4(jiti@2.7.0) eslint-import-resolver-node: 0.3.10 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.7.0)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4(jiti@2.7.0)) eslint-plugin-react: 7.37.5(eslint@9.39.4(jiti@2.7.0)) eslint-plugin-react-hooks: 7.1.1(eslint@9.39.4(jiti@2.7.0)) @@ -4653,7 +4659,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.7.0)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3 @@ -4664,22 +4670,22 @@ snapshots: tinyglobby: 0.2.17 unrs-resolver: 1.12.2 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.13.0(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)): + eslint-module-utils@2.13.0(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) eslint: 9.39.4(jiti@2.7.0) eslint-import-resolver-node: 0.3.10 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.7.0)) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -4690,7 +4696,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.4(jiti@2.7.0) eslint-import-resolver-node: 0.3.10 - eslint-module-utils: 2.13.0(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)) + eslint-module-utils: 2.13.0(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)) hasown: 2.0.4 is-core-module: 2.16.2 is-glob: 4.0.3 @@ -6016,6 +6022,8 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + twitter-api-v2@1.29.0: {} + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 diff --git a/scripts/approve_and_payout.ts b/scripts/approve_and_payout.ts new file mode 100644 index 0000000..37a79a1 --- /dev/null +++ b/scripts/approve_and_payout.ts @@ -0,0 +1,53 @@ +import { PrismaClient, TaskStatus } from "@agent-bounty/contracts/node_modules/@prisma/client"; +import { capturePayment, executePayout } from "../apps/web/src/lib/payment"; + +const prisma = new PrismaClient(); + +async function main() { + const taskId = process.argv[2]; + if (!taskId) { + console.error("Please provide a Task ID: pnpm dlx tsx scripts/approve_and_payout.ts "); + process.exit(1); + } + + console.log(`🔍 Approving Task: ${taskId}...`); + + const task = await prisma.task.findUnique({ + where: { id: taskId }, + include: { claims: { where: { status: "PENDING_REVIEW" }, orderBy: { created_at: 'desc' }, take: 1 } } + }); + + if (!task || task.claims.length === 0) { + console.error("Task not found or no claim in PENDING_REVIEW status."); + process.exit(1); + } + + const claim = task.claims[0]; + + await prisma.$transaction(async (tx) => { + // 1. Mark Claim and Task as COMPLETED + await tx.claim.update({ + where: { id: claim.id }, + data: { status: TaskStatus.COMPLETED } + }); + await tx.task.update({ + where: { id: task.id }, + data: { status: TaskStatus.COMPLETED } + }); + + // 2. Capture funds from the task creator + console.log("💰 Capturing funds..."); + const captureKey = `capture-${task.id}-${Date.now()}`; + await capturePayment(tx, task.id, captureKey); + + // 3. Execute Payout to the Agent's developer wallet + console.log(`🏦 Routing payout to agent wallet: ${claim.developer_wallet}...`); + const payoutKey = `payout-${claim.id}-${Date.now()}`; + // Deduct routing fee conceptually, or pay the full held amount if the fee was deducted at sub-task creation + await executePayout(tx, task.id, claim.developer_wallet, claim.held_amount, claim.held_currency, payoutKey); + }); + + console.log("🎉 Payout Successful! The agent has been paid."); +} + +main().catch(console.error).finally(() => prisma.$disconnect()); diff --git a/scripts/seed_tasks.ts b/scripts/seed_tasks.ts new file mode 100644 index 0000000..b23c6b7 --- /dev/null +++ b/scripts/seed_tasks.ts @@ -0,0 +1,43 @@ +import { PrismaClient, TaskStatus } from "@agent-bounty/contracts/node_modules/@prisma/client"; +import { broadcastFomoEvent } from "../apps/web/src/lib/x-broadcaster"; +import crypto from "crypto"; + +const prisma = new PrismaClient(); + +async function main() { + console.log("🌱 [Seed] Generating High Value Bait Tasks..."); + + const task = await prisma.task.create({ + data: { + title: "Implement OIDC SSO Integration for Enterprise Customers", + description: "We need an experienced developer/agent to implement OIDC based Single Sign-On (SSO) with Okta and Auth0. Must include automated integration tests and documentation. Security is critical.", + status: TaskStatus.OPEN, + difficulty: "HARD", + reward_amount: 50000, // $500.00 + reward_currency: "USD", + required_stack: ["TypeScript", "Next.js", "OIDC", "Auth0"], + scope_clarity_score: 0.9, + acceptance_criteria: { + tests: [ + "Auth flow completes successfully via Okta mock", + "Token verification works properly" + ] + }, + is_priority: true, + stripe_payment_intent_id: "promo_free_bounty_intent" // Bypass Stripe Auth Hold + } + }); + + console.log(`✅ Created High Value Task: ${task.id}`); + + // Trigger FOMO + await broadcastFomoEvent({ + type: "HIGH_VALUE_BOUNTY", + taskId: task.id, + amountFormatted: "$500" + }); + + console.log("🚀 [Seed] Done!"); +} + +main().catch(console.error).finally(() => prisma.$disconnect());