feat(core): Stripe Connect Payouts & Twitter API Integration
Some checks failed
Deploy to 110 WOOO Server / deploy (push) Failing after 7s
Some checks failed
Deploy to 110 WOOO Server / deploy (push) Failing after 7s
This commit is contained in:
@@ -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": {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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
24
pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
53
scripts/approve_and_payout.ts
Normal file
53
scripts/approve_and_payout.ts
Normal 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
43
scripts/seed_tasks.ts
Normal 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());
|
||||
Reference in New Issue
Block a user