feat(core): Stripe Connect Payouts & Twitter API Integration
Some checks failed
Deploy to 110 WOOO Server / deploy (push) Failing after 7s

This commit is contained in:
OG T
2026-06-08 10:27:57 +08:00
parent d03519c71c
commit a6201007aa
6 changed files with 184 additions and 29 deletions

View File

@@ -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": {

View File

@@ -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,
},
});
}

View File

@@ -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);
}
}

24
pnpm-lock.yaml generated
View File

@@ -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

View File

@@ -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 <task_id>");
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());

43
scripts/seed_tasks.ts Normal file
View File

@@ -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());