@@ -55,7 +56,9 @@ export default async function ProposalSuccessPage({ searchParams }: { searchPara
{payment === "wallet"
- ? "待 USDC 轉帳確認"
+ ? walletCaptured
+ ? "USDC 付款已確認"
+ : "待 USDC 轉帳確認"
: stripeCaptured
? "提案付款已確認"
: "付款返回成功,等待 webhook 入帳確認"}
@@ -122,6 +125,12 @@ export default async function ProposalSuccessPage({ searchParams }: { searchPara
{TREASURY_USDC_ADDRESS || `${TREASURY_WALLET_LABEL} is not configured yet`}
+ {walletCaptured ? (
+
+
Receipt
+ {task?.stripe_payment_intent_id?.replace(/^wallet:/, "")}
+
+ ) : null}
{TREASURY_USDC_ADDRESS ? (
diff --git a/apps/web/src/lib/payment.ts b/apps/web/src/lib/payment.ts
index 7e8663f..a17c681 100644
--- a/apps/web/src/lib/payment.ts
+++ b/apps/web/src/lib/payment.ts
@@ -211,13 +211,10 @@ export async function executePayout(
where: { idempotency_key: idempotencyKey },
});
if (existing) {
- if (existing.response_status === "SUCCESS") return existing;
+ if (existing.response_status === "SUCCESS" || existing.response_status.startsWith("PENDING_")) 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({
@@ -226,7 +223,17 @@ export async function executePayout(
destination: developerWallet,
description: `Bounty Payout for Task ${taskId}`,
}, { idempotencyKey });
- transferId = transfer.id;
+
+ return await tx.ledgerEntry.create({
+ data: {
+ task_id: taskId,
+ phase: "PAYOUT",
+ idempotency_key: idempotencyKey,
+ stripe_object_id: transfer.id,
+ response_status: "SUCCESS",
+ http_status: 200,
+ },
+ });
} catch (error: unknown) {
await tx.ledgerEntry.create({
data: {
@@ -247,8 +254,10 @@ export async function executePayout(
task_id: taskId,
phase: "PAYOUT",
idempotency_key: idempotencyKey,
- stripe_object_id: transferId,
- response_status: "SUCCESS",
+ stripe_object_id: developerWallet,
+ response_status: developerWallet.startsWith("acct_")
+ ? "PENDING_STRIPE_CONNECT_TRANSFER"
+ : "PENDING_MANUAL_WALLET_TRANSFER",
http_status: 200,
},
});
diff --git a/apps/web/src/lib/proposal-payment-ledger.ts b/apps/web/src/lib/proposal-payment-ledger.ts
new file mode 100644
index 0000000..b2f115f
--- /dev/null
+++ b/apps/web/src/lib/proposal-payment-ledger.ts
@@ -0,0 +1,212 @@
+import { prisma } from "@/lib/prisma";
+import { asA2aAgentActorId } from "@/lib/a2a-traffic";
+import { Prisma } from "../../prisma/generated/client";
+import { TaskStatus } from "@agent-bounty/contracts";
+
+type ProposalFeeTrafficContext = {
+ actor_type: "AGENT" | "USER";
+ actor_id: string;
+ request_id: string;
+ source_ip: string;
+ user_agent: string;
+ is_public_ip: boolean;
+ request_actor_headers?: Record;
+};
+
+export type ProposalFeeCaptureInput = {
+ taskId: string;
+ referralAgent?: string | null;
+ proposalFeeCents: number;
+ paymentProvider: "stripe" | "wallet";
+ paymentReference: string;
+ idempotencyKey: string;
+ responseStatus?: "captured" | "verified";
+ packageId?: string | null;
+ source?: string | null;
+ campaign?: string | null;
+ verificationNote?: string | null;
+ trafficContext?: ProposalFeeTrafficContext | null;
+};
+
+function positiveCents(value: number) {
+ if (!Number.isFinite(value)) return 0;
+ return Math.max(0, Math.floor(value));
+}
+
+function externalTrafficContextForReferral(referralAgent: string): ProposalFeeTrafficContext {
+ return {
+ actor_type: "AGENT",
+ actor_id: asA2aAgentActorId(referralAgent),
+ request_id: `admin-wallet-${Date.now()}`,
+ source_ip: "admin-verification",
+ user_agent: "vibework-admin-wallet-verifier",
+ is_public_ip: false,
+ request_actor_headers: {},
+ };
+}
+
+export async function recordDemandProposalFeeCaptured(input: ProposalFeeCaptureInput) {
+ const proposalFeeCents = positiveCents(input.proposalFeeCents);
+ const affiliateFeeCents = input.referralAgent ? Math.floor(proposalFeeCents * 0.1) : 0;
+ const responseStatus = input.responseStatus || (input.paymentProvider === "wallet" ? "verified" : "captured");
+ const trafficContext =
+ input.referralAgent && input.paymentProvider === "wallet" && !input.trafficContext
+ ? externalTrafficContextForReferral(input.referralAgent)
+ : input.trafficContext;
+
+ const task = await prisma.task.findUnique({
+ where: { id: input.taskId },
+ select: { id: true },
+ });
+
+ if (!task) {
+ throw new Error(`Proposal task not found: ${input.taskId}`);
+ }
+
+ const existingCapture = await prisma.ledgerEntry.findUnique({
+ where: { idempotency_key: input.idempotencyKey },
+ });
+
+ if (existingCapture) {
+ return {
+ task_id: input.taskId,
+ duplicate: true,
+ ledger_entry_id: existingCapture.id,
+ affiliate_fee_cents: affiliateFeeCents,
+ };
+ }
+
+ const result = await prisma.$transaction(async (tx: Prisma.TransactionClient) => {
+ const ledger = await tx.ledgerEntry.create({
+ data: {
+ task_id: input.taskId,
+ phase: "proposal_fee_captured",
+ idempotency_key: input.idempotencyKey,
+ stripe_object_id: input.paymentReference,
+ response_status: responseStatus,
+ http_status: 200,
+ },
+ });
+
+ await tx.task.update({
+ where: { id: input.taskId },
+ data: {
+ stripe_payment_intent_id: input.paymentReference,
+ status: TaskStatus.DRAFT,
+ },
+ });
+
+ if (input.referralAgent) {
+ await tx.agentProfile.upsert({
+ where: { agent_id: input.referralAgent },
+ update: {
+ discovery_source: "DEMAND_PROPOSAL_PAID_REFERRAL",
+ },
+ create: {
+ agent_id: input.referralAgent,
+ type: "SCOUT",
+ status: "PENDING",
+ discovery_source: "DEMAND_PROPOSAL_PAID_REFERRAL",
+ capabilities: {
+ growth_referral: true,
+ source: input.source || input.paymentProvider,
+ },
+ },
+ });
+
+ await tx.scoutReputation.upsert({
+ where: { scout_id: input.referralAgent },
+ update: {
+ successful_conversions: { increment: 1 },
+ },
+ create: {
+ scout_id: input.referralAgent,
+ successful_conversions: 1,
+ },
+ });
+
+ const existingAffiliate = await tx.affiliateLedger.findFirst({
+ where: {
+ scout_id: input.referralAgent,
+ task_id: input.taskId,
+ },
+ });
+
+ if (!existingAffiliate && affiliateFeeCents > 0) {
+ await tx.affiliateLedger.create({
+ data: {
+ scout_id: input.referralAgent,
+ task_id: input.taskId,
+ amount: affiliateFeeCents,
+ currency: input.paymentProvider === "wallet" ? "USDC" : "USD",
+ status: "PENDING",
+ },
+ });
+ }
+ }
+
+ await tx.auditEvent.create({
+ data: {
+ actorType: "SYSTEM",
+ actorId: input.paymentProvider === "wallet" ? "wallet-verifier" : "stripe-webhook",
+ action: "DEMAND_PROPOSAL_FEE_CAPTURED",
+ entityType: "TASK",
+ entityId: input.taskId,
+ metadata: {
+ payment_provider: input.paymentProvider,
+ payment_reference: input.paymentReference,
+ fee_cents: proposalFeeCents,
+ currency: input.paymentProvider === "wallet" ? "USDC" : "USD",
+ package_id: input.packageId || null,
+ referral_agent: input.referralAgent || null,
+ affiliate_fee_cents: affiliateFeeCents,
+ source: input.source || null,
+ campaign: input.campaign || null,
+ verification_note: input.verificationNote || null,
+ },
+ },
+ });
+
+ if (input.referralAgent && trafficContext) {
+ await tx.auditEvent.create({
+ data: {
+ actorType: trafficContext.actor_type,
+ actorId: trafficContext.actor_id,
+ action: "EXTERNAL_DEMAND_PROPOSAL_FEE_CAPTURED",
+ entityType: "TASK",
+ entityId: input.taskId,
+ reason: "referred_demand_proposal_payment_captured",
+ metadata: {
+ payment_provider: input.paymentProvider,
+ payment_reference: input.paymentReference,
+ fee_cents: proposalFeeCents,
+ currency: input.paymentProvider === "wallet" ? "USDC" : "USD",
+ package_id: input.packageId || null,
+ referral_agent: input.referralAgent,
+ affiliate_fee_cents: affiliateFeeCents,
+ source: input.source || null,
+ campaign: input.campaign || null,
+ verification_note: input.verificationNote || null,
+ request_id: trafficContext.request_id,
+ source_ip: trafficContext.source_ip,
+ user_agent: trafficContext.user_agent,
+ is_public_ip: trafficContext.is_public_ip,
+ surface: input.paymentProvider === "wallet" ? "admin/wallet-verification" : "stripe/webhook",
+ request_actor_headers: trafficContext.request_actor_headers || {},
+ response_status: 200,
+ response_summary: "demand_proposal_fee_captured",
+ },
+ },
+ });
+ }
+
+ return ledger;
+ });
+
+ return {
+ task_id: input.taskId,
+ duplicate: false,
+ ledger_entry_id: result.id,
+ affiliate_fee_cents: affiliateFeeCents,
+ };
+}
diff --git a/apps/web/src/lib/treasury-snapshot.ts b/apps/web/src/lib/treasury-snapshot.ts
new file mode 100644
index 0000000..f06a2d2
--- /dev/null
+++ b/apps/web/src/lib/treasury-snapshot.ts
@@ -0,0 +1,158 @@
+import { prisma } from "@/lib/prisma";
+
+type Bucket = {
+ usdc: number;
+ fiat: number;
+};
+
+function emptyBucket(): Bucket {
+ return { usdc: 0, fiat: 0 };
+}
+
+function asRecord(value: unknown) {
+ return typeof value === "object" && value !== null ? (value as Record) : {};
+}
+
+function numberValue(value: unknown) {
+ const parsed = typeof value === "number" ? value : Number.parseInt(String(value || ""), 10);
+ return Number.isFinite(parsed) ? Math.floor(parsed) : 0;
+}
+
+function stringValue(value: unknown) {
+ return typeof value === "string" ? value : "";
+}
+
+function bucketKeyFromMetadata(metadata: Record): keyof Bucket {
+ const currency = stringValue(metadata.currency).toUpperCase();
+ const paymentProvider = stringValue(metadata.payment_provider).toLowerCase();
+ const withdrawalType = stringValue(metadata.type).toUpperCase();
+ if (currency === "USDC" || paymentProvider === "wallet" || withdrawalType === "CRYPTO") {
+ return "usdc";
+ }
+ return "fiat";
+}
+
+function addToBucket(bucket: Bucket, key: keyof Bucket, amount: number) {
+ bucket[key] += amount;
+}
+
+function subtractBucket(left: Bucket, right: Bucket): Bucket {
+ return {
+ usdc: left.usdc - right.usdc,
+ fiat: left.fiat - right.fiat,
+ };
+}
+
+export async function getTreasurySnapshot() {
+ const [
+ allTasks,
+ completedTasks,
+ arbitrationCount,
+ proposalFeeEvents,
+ withdrawalEvents,
+ recentAuditEvents,
+ ] = await Promise.all([
+ prisma.task.findMany({
+ select: { reward_amount: true, reward_currency: true },
+ }),
+ prisma.task.findMany({
+ where: { status: "COMPLETED" },
+ select: { reward_amount: true, reward_currency: true },
+ }),
+ prisma.arbitration.count(),
+ prisma.auditEvent.findMany({
+ where: { action: "DEMAND_PROPOSAL_FEE_CAPTURED" },
+ select: { id: true, entityId: true, metadata: true, createdAt: true },
+ orderBy: { createdAt: "desc" },
+ }),
+ prisma.auditEvent.findMany({
+ where: { action: "TREASURY_WITHDRAWAL" },
+ select: { metadata: true },
+ }),
+ prisma.auditEvent.findMany({
+ where: {
+ action: {
+ in: [
+ "DEMAND_PROPOSAL_FEE_CAPTURED",
+ "TREASURY_WITHDRAWAL",
+ "TREASURY_WITHDRAWAL_REQUESTED",
+ ],
+ },
+ },
+ select: { id: true, action: true, entityId: true, metadata: true, createdAt: true },
+ orderBy: { createdAt: "desc" },
+ take: 20,
+ }),
+ ]);
+
+ const gmv = emptyBucket();
+ const taskPlatformFees = emptyBucket();
+ const proposalFeeRevenue = emptyBucket();
+ const arbitrationRevenue = emptyBucket();
+ const withdrawn = emptyBucket();
+
+ for (const task of allTasks) {
+ addToBucket(gmv, task.reward_currency === "USDC" ? "usdc" : "fiat", task.reward_amount);
+ }
+
+ for (const task of completedTasks) {
+ addToBucket(taskPlatformFees, task.reward_currency === "USDC" ? "usdc" : "fiat", Math.floor(task.reward_amount * 0.05));
+ }
+
+ addToBucket(arbitrationRevenue, "usdc", arbitrationCount * 5000);
+
+ for (const event of proposalFeeEvents) {
+ const metadata = asRecord(event.metadata);
+ const amount = numberValue(metadata.fee_cents);
+ if (amount <= 0) continue;
+ addToBucket(proposalFeeRevenue, bucketKeyFromMetadata(metadata), amount);
+ }
+
+ for (const event of withdrawalEvents) {
+ const metadata = asRecord(event.metadata);
+ const amount = numberValue(metadata.amount);
+ if (amount <= 0) continue;
+ addToBucket(withdrawn, bucketKeyFromMetadata(metadata), amount);
+ }
+
+ const revenue = {
+ usdc: taskPlatformFees.usdc + proposalFeeRevenue.usdc + arbitrationRevenue.usdc,
+ fiat: taskPlatformFees.fiat + proposalFeeRevenue.fiat + arbitrationRevenue.fiat,
+ };
+
+ const recentTransactions = recentAuditEvents.map((event) => {
+ const metadata = asRecord(event.metadata);
+ const isOutflow = event.action.startsWith("TREASURY_WITHDRAWAL");
+ const amount = numberValue(metadata.amount) || numberValue(metadata.fee_cents);
+ const currency = bucketKeyFromMetadata(metadata) === "usdc" ? "USDC" : "USD";
+ const provider = stringValue(metadata.payment_provider);
+ const reference = stringValue(metadata.payment_reference) || stringValue(metadata.txHash) || stringValue(metadata.destination);
+ const source =
+ event.action === "DEMAND_PROPOSAL_FEE_CAPTURED"
+ ? `Proposal ${event.entityId}`
+ : reference || "Treasury";
+
+ return {
+ id: event.id,
+ type: event.action === "DEMAND_PROPOSAL_FEE_CAPTURED"
+ ? `${provider || "proposal"}_proposal_fee`
+ : event.action.toLowerCase(),
+ source,
+ amount,
+ currency,
+ direction: isOutflow ? "out" : "in",
+ date: event.createdAt.toISOString(),
+ };
+ });
+
+ return {
+ gmv,
+ revenue,
+ withdrawn,
+ available: subtractBucket(revenue, withdrawn),
+ proposal_fee_revenue: proposalFeeRevenue,
+ task_platform_fee_revenue: taskPlatformFees,
+ arbitration_fee_revenue: arbitrationRevenue,
+ recent_transactions: recentTransactions,
+ };
+}