feat: close wallet proposal revenue loop
All checks were successful
CI and Production Smoke / smoke (push) Successful in 7s

This commit is contained in:
OG T
2026-06-11 15:38:00 +08:00
parent d9ac59e3dd
commit 9e366f8954
9 changed files with 690 additions and 278 deletions

View File

@@ -1,6 +1,6 @@
"use client";
import { useEffect, useState } from "react";
import { useCallback, useEffect, useState } from "react";
type TreasuryStats = {
available?: {
@@ -16,9 +16,12 @@ type TreasuryStats = {
fiat?: number;
};
recent_transactions?: Array<{
id?: string;
type: string;
source: string;
amount: number;
currency?: string;
direction?: "in" | "out";
date: string;
}>;
};
@@ -31,26 +34,86 @@ export default function TreasuryDashboard() {
const [withdrawType, setWithdrawType] = useState<"CRYPTO" | "FIAT">("CRYPTO");
const [destination, setDestination] = useState("");
const [message, setMessage] = useState("");
const [walletTaskId, setWalletTaskId] = useState("");
const [walletReference, setWalletReference] = useState("");
const [walletAmountUsd, setWalletAmountUsd] = useState("");
const [walletNote, setWalletNote] = useState("");
const [verifyingWallet, setVerifyingWallet] = useState(false);
const [walletMessage, setWalletMessage] = useState("");
const cents = (value: number | undefined) => value ?? 0;
useEffect(() => {
const loadStats = async () => {
try {
const res = await fetch("/api/admin/treasury/stats");
const data = await res.json().catch(() => ({}));
if (!res.ok) {
throw new Error(data.error || `Treasury stats request failed (${res.status})`);
}
setStats(data);
} catch (error) {
setLoadError(error instanceof Error ? error.message : "Unable to load treasury stats.");
} finally {
setLoading(false);
const loadStats = useCallback(async () => {
try {
const res = await fetch("/api/admin/treasury/stats");
const data = await res.json().catch(() => ({}));
if (!res.ok) {
throw new Error(data.error || `Treasury stats request failed (${res.status})`);
}
};
void loadStats();
setStats(data);
setLoadError("");
} catch (error) {
setLoadError(error instanceof Error ? error.message : "Unable to load treasury stats.");
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
const timer = window.setTimeout(() => {
void loadStats();
}, 0);
return () => window.clearTimeout(timer);
}, [loadStats]);
const handleVerifyWalletReceipt = async () => {
if (!walletTaskId.trim() || !walletReference.trim()) {
setWalletMessage("Proposal ID and wallet reference are required.");
return;
}
const amountCents = walletAmountUsd.trim()
? Math.round(Number.parseFloat(walletAmountUsd.replace(/[,$]/g, "")) * 100)
: undefined;
if (walletAmountUsd.trim() && (!amountCents || amountCents <= 0)) {
setWalletMessage("Amount must be a positive number.");
return;
}
setVerifyingWallet(true);
setWalletMessage("");
try {
const res = await fetch("/api/admin/proposals/wallet/verify", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
task_id: walletTaskId.trim(),
payment_reference: walletReference.trim(),
amount_cents: amountCents,
verification_note: walletNote.trim(),
}),
});
const data = await res.json().catch(() => ({}));
if (!res.ok || !data.success) {
throw new Error(data.error || `Wallet verification failed (${res.status})`);
}
setWalletMessage(
`Wallet receipt ${data.duplicate ? "was already recorded" : "verified"}; affiliate pending ${(data.affiliate_fee_cents / 100).toFixed(2)}.`
);
setWalletTaskId("");
setWalletReference("");
setWalletAmountUsd("");
setWalletNote("");
await loadStats();
} catch (error) {
setWalletMessage(error instanceof Error ? error.message : "Wallet verification failed.");
} finally {
setVerifyingWallet(false);
}
};
const handleWithdraw = async () => {
if (!stats) {
setMessage("⚠️ Treasury stats are unavailable.");
@@ -176,6 +239,68 @@ export default function TreasuryDashboard() {
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Wallet receipt verification */}
<div className="lg:col-span-2 bg-[#111] border border-[#333] rounded-2xl p-8">
<h2 className="text-2xl font-semibold mb-6 flex items-center gap-3">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M20 7H4"></path><path d="M4 7l2-3h12l2 3"></path><path d="M5 7v11h14V7"></path><path d="M9 11h6"></path></svg>
Verify USDC Proposal Receipt
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div>
<label className="block text-sm text-gray-400 mb-2">Proposal ID</label>
<input
type="text"
value={walletTaskId}
onChange={(e) => setWalletTaskId(e.target.value)}
className="w-full bg-[#0a0a0a] border border-[#333] rounded-xl px-4 py-3 text-white font-mono focus:outline-none focus:border-emerald-500 transition-colors"
/>
</div>
<div>
<label className="block text-sm text-gray-400 mb-2">Tx hash or receipt reference</label>
<input
type="text"
value={walletReference}
onChange={(e) => setWalletReference(e.target.value)}
className="w-full bg-[#0a0a0a] border border-[#333] rounded-xl px-4 py-3 text-white font-mono focus:outline-none focus:border-emerald-500 transition-colors"
/>
</div>
<div>
<label className="block text-sm text-gray-400 mb-2">Amount override (USD/USDC)</label>
<input
type="text"
value={walletAmountUsd}
onChange={(e) => setWalletAmountUsd(e.target.value)}
placeholder="Leave blank to use proposal package"
className="w-full bg-[#0a0a0a] border border-[#333] rounded-xl px-4 py-3 text-white font-mono focus:outline-none focus:border-emerald-500 transition-colors"
/>
</div>
<div>
<label className="block text-sm text-gray-400 mb-2">Verification note</label>
<input
type="text"
value={walletNote}
onChange={(e) => setWalletNote(e.target.value)}
className="w-full bg-[#0a0a0a] border border-[#333] rounded-xl px-4 py-3 text-white focus:outline-none focus:border-emerald-500 transition-colors"
/>
</div>
</div>
<button
onClick={handleVerifyWalletReceipt}
disabled={verifyingWallet}
className="w-full py-4 rounded-xl font-bold text-lg bg-emerald-600 hover:bg-emerald-500 disabled:opacity-50 disabled:cursor-not-allowed transition-all"
>
{verifyingWallet ? "Verifying Receipt..." : "Mark Wallet Proposal Fee Verified"}
</button>
{walletMessage && (
<div className={`mt-6 p-4 rounded-xl ${walletMessage.includes("failed") || walletMessage.includes("required") || walletMessage.includes("positive") ? 'bg-red-900/30 text-red-400 border border-red-500/30' : 'bg-green-900/30 text-green-400 border border-green-500/30'} font-mono text-sm`}>
{walletMessage}
</div>
)}
</div>
{/* Withdrawal Panel */}
<div className="lg:col-span-2 bg-[#111] border border-[#333] rounded-2xl p-8">
<h2 className="text-2xl font-semibold mb-6 flex items-center gap-3">
@@ -238,17 +363,24 @@ export default function TreasuryDashboard() {
<h3 className="text-lg font-semibold text-gray-300 mb-6">Recent Cashflows</h3>
<div className="space-y-4">
{stats.recent_transactions?.map((tx, idx) => (
<div key={idx} className="flex justify-between items-center border-b border-[#222] pb-4 last:border-0">
<div key={tx.id || idx} className="flex justify-between items-center border-b border-[#222] pb-4 last:border-0">
<div>
<p className="text-sm text-gray-300">{tx.type.replace('_', ' ')}</p>
<p className="text-xs text-gray-500 font-mono mt-1">{tx.source}</p>
</div>
<div className="text-right">
<p className="text-sm font-mono text-green-400">+${(tx.amount / 100).toFixed(2)}</p>
<p className={`text-sm font-mono ${tx.direction === "out" ? "text-amber-300" : "text-green-400"}`}>
{tx.direction === "out" ? "-" : "+"}${(tx.amount / 100).toFixed(2)} {tx.currency || "USD"}
</p>
<p className="text-xs text-gray-500 mt-1">{new Date(tx.date).toLocaleTimeString()}</p>
</div>
</div>
))}
{!stats.recent_transactions?.length ? (
<div className="rounded-xl border border-dashed border-[#333] p-4 text-sm text-gray-500">
No verified cashflows yet.
</div>
) : null}
</div>
</div>

View File

@@ -0,0 +1,112 @@
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { adminUnauthorizedResponse, isAdminRequestAuthorized } from "@/lib/admin-auth";
import { recordDemandProposalFeeCaptured } from "@/lib/proposal-payment-ledger";
import { sanitizeAgentId } from "@/lib/a2a-growth";
function asRecord(value: unknown) {
return typeof value === "object" && value !== null ? (value as Record<string, unknown>) : {};
}
function stringValue(value: unknown) {
return typeof value === "string" ? value.trim() : "";
}
function positiveInteger(value: unknown) {
const parsed = typeof value === "number" ? value : Number.parseInt(String(value || ""), 10);
if (!Number.isFinite(parsed) || parsed <= 0) return 0;
return Math.floor(parsed);
}
function normalizePaymentReference(value: string) {
return value.trim().replace(/\s+/g, "").slice(0, 180);
}
export async function POST(request: NextRequest) {
if (!isAdminRequestAuthorized(request)) {
return adminUnauthorizedResponse();
}
try {
const body = asRecord(await request.json().catch(() => ({})));
const taskId = stringValue(body.task_id || body.taskId);
const paymentReference = normalizePaymentReference(
stringValue(body.payment_reference || body.tx_hash || body.txHash)
);
const overrideAmountCents = positiveInteger(body.amount_cents || body.amountCents);
const verificationNote = stringValue(body.verification_note || body.note);
if (!taskId) {
return NextResponse.json({ error: "task_id is required" }, { status: 400 });
}
if (!paymentReference) {
return NextResponse.json({ error: "payment_reference or tx_hash is required" }, { status: 400 });
}
const task = await prisma.task.findUnique({
where: { id: taskId },
select: {
id: true,
title: true,
referred_by_agent: true,
scout_id: true,
},
});
if (!task) {
return NextResponse.json({ error: "Proposal task not found" }, { status: 404 });
}
const intakeEvent = await prisma.auditEvent.findFirst({
where: {
action: "DEMAND_PROPOSAL_INTAKE_CREATED",
entityType: "TASK",
entityId: task.id,
},
orderBy: { createdAt: "desc" },
select: { metadata: true },
});
const metadata = asRecord(intakeEvent?.metadata);
const feeCents = overrideAmountCents || positiveInteger(metadata.proposal_fee_cents);
if (!feeCents) {
return NextResponse.json({
error: "Unable to resolve proposal fee amount. Provide amount_cents.",
}, { status: 400 });
}
const referralAgent = sanitizeAgentId(
task.referred_by_agent || task.scout_id || stringValue(metadata.referral_agent)
);
const result = await recordDemandProposalFeeCaptured({
taskId: task.id,
referralAgent: referralAgent || null,
proposalFeeCents: feeCents,
paymentProvider: "wallet",
paymentReference: `wallet:${paymentReference}`,
idempotencyKey: `proposal-wallet-fee:${paymentReference}`,
responseStatus: "verified",
packageId: stringValue(metadata.package_id) || null,
source: stringValue(metadata.source) || "wallet-verification",
campaign: stringValue(metadata.campaign) || null,
verificationNote: verificationNote || null,
});
return NextResponse.json({
success: true,
duplicate: result.duplicate,
task_id: result.task_id,
ledger_entry_id: result.ledger_entry_id,
referral_agent: referralAgent || null,
fee_cents: feeCents,
affiliate_fee_cents: result.affiliate_fee_cents,
payment_reference: paymentReference,
});
} catch (error: unknown) {
console.error("[Wallet Proposal Verification Error]", error);
return NextResponse.json({ error: "Internal Server Error" }, { status: 500 });
}
}

View File

@@ -1,18 +1,6 @@
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { adminUnauthorizedResponse, isAdminRequestAuthorized } from "@/lib/admin-auth";
function getWithdrawalMetadata(metadata: unknown) {
if (typeof metadata !== "object" || metadata === null) {
return null;
}
const value = metadata as { currency?: unknown; amount?: unknown };
return {
currency: typeof value.currency === "string" ? value.currency : null,
amount: typeof value.amount === "number" ? value.amount : 0,
};
}
import { getTreasurySnapshot } from "@/lib/treasury-snapshot";
export async function GET(request: NextRequest) {
if (!isAdminRequestAuthorized(request)) {
@@ -20,65 +8,7 @@ export async function GET(request: NextRequest) {
}
try {
const allTasks = await prisma.task.findMany({
select: { reward_amount: true, reward_currency: true, status: true }
});
let totalGmvUsdc = 0;
let totalGmvFiat = 0;
let totalUsdcRevenue = 0;
let totalFiatRevenue = 0;
for (const t of allTasks) {
if (t.reward_currency === "USDC") {
totalGmvUsdc += t.reward_amount;
if (t.status === "COMPLETED") totalUsdcRevenue += t.reward_amount * 0.05;
} else {
totalGmvFiat += t.reward_amount;
if (t.status === "COMPLETED") totalFiatRevenue += t.reward_amount * 0.05;
}
}
const arbitrations = await prisma.arbitration.count();
totalUsdcRevenue += arbitrations * 5000; // 50 USDC court fee
// Get previous withdrawals
const withdrawals = await prisma.auditEvent.findMany({
where: { action: "TREASURY_WITHDRAWAL" }
});
let withdrawnUsdc = 0;
let withdrawnFiat = 0;
for (const w of withdrawals) {
const data = getWithdrawalMetadata(w.metadata);
if (!data) {
continue;
}
if (data.currency === "USDC") {
withdrawnUsdc += data.amount;
} else {
withdrawnFiat += data.amount;
}
}
const availableUsdc = totalUsdcRevenue - withdrawnUsdc;
const availableFiat = totalFiatRevenue - withdrawnFiat;
// Provide some fake recent history to make it look active
const recentTransactions = [
{ id: "tx-1", type: "FEE_COLLECTION", amount: 2500, currency: "USDC", source: "Task #epic-build-2", date: new Date().toISOString() },
{ id: "tx-2", type: "ARBITRATION_FEE", amount: 5000, currency: "USDC", source: "Arbitration #arb-091", date: new Date(Date.now() - 3600000).toISOString() },
{ id: "tx-3", type: "SANDBOX_COMPUTE", amount: 50, currency: "USD", source: "Sandbox VM-1928", date: new Date(Date.now() - 7200000).toISOString() },
];
return NextResponse.json({
gmv: { usdc: totalGmvUsdc, fiat: totalGmvFiat },
revenue: { usdc: totalUsdcRevenue, fiat: totalFiatRevenue },
withdrawn: { usdc: withdrawnUsdc, fiat: withdrawnFiat },
available: { usdc: availableUsdc, fiat: availableFiat },
recent_transactions: recentTransactions
});
return NextResponse.json(await getTreasurySnapshot());
} catch (error: unknown) {
console.error("[Treasury Stats Error]", error);

View File

@@ -1,18 +1,7 @@
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { adminUnauthorizedResponse, isAdminRequestAuthorized } from "@/lib/admin-auth";
function getWithdrawalMetadata(metadata: unknown) {
if (typeof metadata !== "object" || metadata === null) {
return null;
}
const value = metadata as { currency?: unknown; amount?: unknown };
return {
currency: typeof value.currency === "string" ? value.currency : null,
amount: typeof value.amount === "number" ? value.amount : 0,
};
}
import { getTreasurySnapshot } from "@/lib/treasury-snapshot";
export async function POST(request: NextRequest) {
if (!isAdminRequestAuthorized(request)) {
@@ -27,72 +16,40 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ error: "Missing required fields" }, { status: 400 });
}
// Calculate actual platform balance to ensure sufficient funds
const completedTasks = await prisma.task.findMany({
where: { status: "COMPLETED" },
select: { reward_amount: true, reward_currency: true }
});
let totalUsdcRevenue = 0;
let totalFiatRevenue = 0;
for (const t of completedTasks) {
const feeCut = t.reward_amount * 0.05; // 5% platform fee
if (t.reward_currency === "USDC") {
totalUsdcRevenue += feeCut;
} else {
totalFiatRevenue += feeCut;
}
const amountCents = Math.floor(Number(amount));
if (!Number.isFinite(amountCents) || amountCents <= 0) {
return NextResponse.json({ error: "Invalid amount" }, { status: 400 });
}
// Add revenue from Arbitrations (simulated)
const arbitrations = await prisma.arbitration.count();
totalUsdcRevenue += arbitrations * 5000; // 50 USDC court fee per arbitration
const snapshot = await getTreasurySnapshot();
// Get previous withdrawals
const withdrawals = await prisma.auditEvent.findMany({
where: { action: "TREASURY_WITHDRAWAL" }
});
for (const w of withdrawals) {
const data = getWithdrawalMetadata(w.metadata);
if (!data) continue;
if (data.currency === "USDC") totalUsdcRevenue -= data.amount;
else totalFiatRevenue -= data.amount;
}
// Validate balance
if (type === "CRYPTO" && amount > totalUsdcRevenue) {
if (type === "CRYPTO" && amountCents > snapshot.available.usdc) {
return NextResponse.json({ error: "Insufficient USDC balance in Treasury" }, { status: 400 });
}
if (type === "FIAT" && amount > totalFiatRevenue) {
if (type === "FIAT" && amountCents > snapshot.available.fiat) {
return NextResponse.json({ error: "Insufficient Fiat balance in Treasury" }, { status: 400 });
}
// Simulate blockchain/stripe transfer latency
await new Promise(resolve => setTimeout(resolve, 2000));
// Record the withdrawal
await prisma.auditEvent.create({
data: {
actorType: "ADMIN",
actorId: "admin",
action: "TREASURY_WITHDRAWAL",
action: "TREASURY_WITHDRAWAL_REQUESTED",
entityType: "TREASURY",
entityId: "platform-treasury",
metadata: {
destination,
type,
amount,
amount: amountCents,
currency: type === "CRYPTO" ? "USDC" : "USD",
txHash: type === "CRYPTO" ? "0x" + Math.random().toString(16).substring(2, 64) : `pi_${Math.random().toString(36).substring(2, 16)}`
status: "PENDING_OPERATOR_EXECUTION",
}
}
});
return NextResponse.json({
success: true,
message: `Successfully transferred ${(amount / 100).toFixed(2)} ${type === "CRYPTO" ? "USDC" : "USD"} to ${destination}`
message: `Withdrawal request recorded for ${(amountCents / 100).toFixed(2)} ${type === "CRYPTO" ? "USDC" : "USD"} to ${destination}. Execute the actual transfer from the treasury wallet or Stripe dashboard, then record the completed withdrawal.`
});
} catch (error: unknown) {

View File

@@ -5,6 +5,7 @@ import { TaskStatus } from "@agent-bounty/contracts";
import { broadcastFomoEvent } from "@/lib/x-broadcaster";
import { sanitizeAgentId } from "@/lib/a2a-growth";
import { resolveA2aTrafficContext } from "@/lib/a2a-traffic";
import { recordDemandProposalFeeCaptured } from "@/lib/proposal-payment-ledger";
const stripe = process.env.STRIPE_SECRET_KEY ? new Stripe(process.env.STRIPE_SECRET_KEY, {
apiVersion: "2026-05-27.dahlia",
@@ -21,7 +22,6 @@ async function handleDemandProposalFee(session: Stripe.Checkout.Session, request
const referralAgent = sanitizeAgentId(metadata.referral_agent);
const proposalFeeCents = Number(metadata.proposal_fee_cents || session.amount_total || 0);
const paymentIntentId = getStripeObjectId(session.payment_intent);
const idempotencyKey = `proposal-fee:${session.id}`;
const trafficContext = referralAgent
? resolveA2aTrafficContext(requestHeaders, referralAgent)
: null;
@@ -37,137 +37,30 @@ async function handleDemandProposalFee(session: Stripe.Checkout.Session, request
return;
}
const existingCapture = await prisma.ledgerEntry.findUnique({
where: { idempotency_key: idempotencyKey },
const result = await recordDemandProposalFeeCaptured({
taskId: task.id,
referralAgent,
proposalFeeCents,
paymentProvider: "stripe",
paymentReference: paymentIntentId || session.id,
idempotencyKey: `proposal-fee:${session.id}`,
responseStatus: "captured",
packageId: metadata.package_id || null,
source: metadata.source || null,
campaign: metadata.campaign || null,
trafficContext,
});
if (existingCapture) {
await prisma.task.update({
where: { id: task.id },
data: { stripe_checkout_session_id: session.id },
});
if (result.duplicate) {
console.log(`[Webhook] Duplicate proposal fee event ignored for session: ${session.id}`);
return;
}
await prisma.$transaction(async (tx) => {
await tx.ledgerEntry.create({
data: {
task_id: task.id,
phase: "proposal_fee_captured",
idempotency_key: idempotencyKey,
stripe_object_id: session.id,
response_status: "captured",
http_status: 200,
},
});
await tx.task.update({
where: { id: task.id },
data: {
stripe_checkout_session_id: session.id,
stripe_payment_intent_id: paymentIntentId,
status: TaskStatus.DRAFT,
},
});
if (referralAgent) {
await tx.agentProfile.upsert({
where: { agent_id: referralAgent },
update: {
discovery_source: "DEMAND_PROPOSAL_PAID_REFERRAL",
},
create: {
agent_id: referralAgent,
type: "SCOUT",
status: "PENDING",
discovery_source: "DEMAND_PROPOSAL_PAID_REFERRAL",
capabilities: {
growth_referral: true,
source: metadata.source || "stripe-webhook",
},
},
});
await tx.scoutReputation.upsert({
where: { scout_id: referralAgent },
update: {
successful_conversions: { increment: 1 },
},
create: {
scout_id: referralAgent,
successful_conversions: 1,
},
});
const existingAffiliate = await tx.affiliateLedger.findFirst({
where: {
scout_id: referralAgent,
task_id: task.id,
},
});
if (!existingAffiliate && proposalFeeCents > 0) {
await tx.affiliateLedger.create({
data: {
scout_id: referralAgent,
task_id: task.id,
amount: Math.floor(proposalFeeCents * 0.1),
currency: "USD",
status: "PENDING",
},
});
}
}
await tx.auditEvent.create({
data: {
actorType: "SYSTEM",
actorId: "stripe-webhook",
action: "DEMAND_PROPOSAL_FEE_CAPTURED",
entityType: "TASK",
entityId: task.id,
metadata: {
stripe_session_id: session.id,
stripe_payment_intent_id: paymentIntentId,
fee_cents: proposalFeeCents,
package_id: metadata.package_id || null,
referral_agent: referralAgent || null,
affiliate_fee_cents: referralAgent ? Math.floor(proposalFeeCents * 0.1) : 0,
source: metadata.source || null,
campaign: metadata.campaign || null,
},
},
});
if (referralAgent && trafficContext) {
await tx.auditEvent.create({
data: {
actorType: trafficContext.actor_type,
actorId: trafficContext.actor_id,
action: "EXTERNAL_DEMAND_PROPOSAL_FEE_CAPTURED",
entityType: "TASK",
entityId: task.id,
reason: "referred_demand_proposal_payment_captured",
metadata: {
stripe_session_id: session.id,
stripe_payment_intent_id: paymentIntentId,
fee_cents: proposalFeeCents,
package_id: metadata.package_id || null,
referral_agent: referralAgent,
affiliate_fee_cents: Math.floor(proposalFeeCents * 0.1),
source: metadata.source || null,
campaign: metadata.campaign || null,
request_id: trafficContext.request_id,
source_ip: trafficContext.source_ip,
user_agent: trafficContext.user_agent,
is_public_ip: trafficContext.is_public_ip,
surface: "stripe/webhook",
request_actor_headers: trafficContext.request_actor_headers,
response_status: 200,
response_summary: "demand_proposal_fee_captured",
},
},
});
}
});
console.log(`[Webhook] Proposal fee captured for task ${task.id}. Payment Intent: ${paymentIntentId}`);
}

View File

@@ -41,6 +41,7 @@ export default async function ProposalSuccessPage({ searchParams }: { searchPara
})
: null;
const stripeCaptured = payment === "stripe" && Boolean(task?.stripe_payment_intent_id);
const walletCaptured = payment === "wallet" && Boolean(task?.stripe_payment_intent_id?.startsWith("wallet:"));
return (
<main className="min-h-screen bg-zinc-950 px-5 py-10 text-zinc-100">
@@ -55,7 +56,9 @@ export default async function ProposalSuccessPage({ searchParams }: { searchPara
<div>
<p className="text-sm font-medium text-emerald-200">
{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`}
</dd>
</div>
{walletCaptured ? (
<div>
<dt className="text-emerald-100/70">Receipt</dt>
<dd className="mt-1 break-all text-emerald-50">{task?.stripe_payment_intent_id?.replace(/^wallet:/, "")}</dd>
</div>
) : null}
</dl>
{TREASURY_USDC_ADDRESS ? (
<div className="mt-3 inline-flex items-center gap-2 rounded-md border border-emerald-300/30 px-3 py-2 text-xs leading-5 text-emerald-100">

View File

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

View File

@@ -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<string, string | number | boolean | null>;
};
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,
};
}

View File

@@ -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<string, unknown>) : {};
}
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<string, unknown>): 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,
};
}