diff --git a/components/ai/AiAssumptionsBanner.tsx b/components/ai/AiAssumptionsBanner.tsx
new file mode 100644
index 00000000..75c59be5
--- /dev/null
+++ b/components/ai/AiAssumptionsBanner.tsx
@@ -0,0 +1,89 @@
+'use client';
+
+import { useState } from 'react';
+import { Sparkles, X } from 'lucide-react';
+
+import { BoundlessButton } from '@/components/buttons';
+
+export interface AiAssumption {
+ section: string;
+ field: string;
+ note: string;
+}
+
+interface AiAssumptionsBannerProps {
+ assumptions: AiAssumption[];
+ /** Jump to the wizard step that owns an assumption so the organizer can edit. */
+ onReview?: (section: string) => void;
+ className?: string;
+}
+
+/**
+ * Shows the non-obvious choices an AI draft made ("Assumed a single winner…")
+ * so the organizer can see and correct every guess. Shared by the bounty and
+ * hackathon review steps. Dismissible; renders nothing when there's nothing to
+ * surface.
+ */
+export default function AiAssumptionsBanner({
+ assumptions,
+ onReview,
+ className,
+}: AiAssumptionsBannerProps) {
+ const [dismissed, setDismissed] = useState(false);
+ if (dismissed || !assumptions || assumptions.length === 0) return null;
+
+ return (
+
+
+
+
+
+
+
+
+ A few things the AI assumed
+
+
+ Review these guesses and correct anything that doesn't match
+ what you meant.
+
+
+
+
setDismissed(true)}
+ className='text-gray-500 transition-colors hover:text-gray-300'
+ >
+
+
+
+
+
+ {assumptions.map((a, i) => (
+
+ {a.note}
+ {onReview && (
+ onReview(a.section)}
+ >
+ Review
+
+ )}
+
+ ))}
+
+
+ );
+}
diff --git a/components/ai/AiBriefPanel.tsx b/components/ai/AiBriefPanel.tsx
new file mode 100644
index 00000000..9001e9b0
--- /dev/null
+++ b/components/ai/AiBriefPanel.tsx
@@ -0,0 +1,52 @@
+'use client';
+
+import { useState } from 'react';
+import { ChevronDown, FileText } from 'lucide-react';
+
+interface AiBriefPanelProps {
+ /** The brief that produced the AI draft (may include folded-in clarify answers). */
+ brief?: string | null;
+ className?: string;
+}
+
+/**
+ * Collapsible "Your brief" panel shown beside an AI-generated draft on the review
+ * step, so the organizer can compare what they asked for against what the AI
+ * produced. Renders nothing for manually-created drafts. Shared by both wizards.
+ */
+export default function AiBriefPanel({ brief, className }: AiBriefPanelProps) {
+ const [open, setOpen] = useState(false);
+ if (!brief || brief.trim() === '') return null;
+
+ return (
+
+
setOpen(o => !o)}
+ className='flex w-full items-center justify-between gap-2 px-4 py-3 text-left'
+ aria-expanded={open}
+ >
+
+
+ Your brief
+
+
+
+ {open && (
+
+ )}
+
+ );
+}
diff --git a/components/ai/AiClarifyQuestions.tsx b/components/ai/AiClarifyQuestions.tsx
new file mode 100644
index 00000000..26d395f3
--- /dev/null
+++ b/components/ai/AiClarifyQuestions.tsx
@@ -0,0 +1,130 @@
+'use client';
+
+import { useState } from 'react';
+import { Sparkles } from 'lucide-react';
+
+import { BoundlessButton } from '@/components/buttons';
+
+export interface ClarifyQuestionOption {
+ value: string;
+ label: string;
+}
+export interface ClarifyQuestionItem {
+ id: string;
+ question: string;
+ options: ClarifyQuestionOption[];
+}
+
+/** A picked answer, ready to fold into the brief. */
+export interface ClarifyAnswer {
+ question: string;
+ label: string;
+}
+
+interface AiClarifyQuestionsProps {
+ questions: ClarifyQuestionItem[];
+ /** Continue to drafting with the chosen answers (skipped questions omitted). */
+ onSubmit: (answers: ClarifyAnswer[]) => void;
+ /** Draft now without answering (the AI picks sensible defaults). */
+ onSkip: () => void;
+ isSubmitting?: boolean;
+}
+
+/**
+ * Adaptive clarify step shared by the bounty + hackathon generate dialogs.
+ * Shows 1-3 chip questions when the brief was too thin; answers are folded back
+ * into the brief before drafting. Answering is optional — Skip drafts straight
+ * away.
+ */
+export default function AiClarifyQuestions({
+ questions,
+ onSubmit,
+ onSkip,
+ isSubmitting = false,
+}: AiClarifyQuestionsProps) {
+ const [selected, setSelected] = useState>({});
+
+ const handleContinue = () => {
+ const answers: ClarifyAnswer[] = questions
+ .filter(q => selected[q.id])
+ .map(q => ({
+ question: q.question,
+ label:
+ q.options.find(o => o.value === selected[q.id])?.label ??
+ selected[q.id],
+ }));
+ onSubmit(answers);
+ };
+
+ return (
+
+
+
+
+
+
+ A couple of quick questions so the draft matches what you have in
+ mind. Answer what you can — you can skip the rest.
+
+
+
+
+ {questions.map(q => (
+
+
{q.question}
+
+ {q.options.map(o => {
+ const on = selected[q.id] === o.value;
+ return (
+
+ setSelected(prev =>
+ prev[q.id] === o.value
+ ? (() => {
+ const next = { ...prev };
+ delete next[q.id];
+ return next;
+ })()
+ : { ...prev, [q.id]: o.value }
+ )
+ }
+ className={[
+ 'rounded-full border px-3 py-1 text-xs transition-colors',
+ on
+ ? 'border-primary bg-primary/15 text-primary'
+ : 'border-zinc-700 text-zinc-400 hover:border-zinc-600 hover:text-zinc-200',
+ ].join(' ')}
+ >
+ {o.label}
+
+ );
+ })}
+
+
+ ))}
+
+
+
+
+ Skip
+
+ }
+ iconPosition='left'
+ onClick={handleContinue}
+ >
+ Generate draft
+
+
+
+ );
+}
diff --git a/components/ai/AiExampleReference.tsx b/components/ai/AiExampleReference.tsx
new file mode 100644
index 00000000..23a3a20b
--- /dev/null
+++ b/components/ai/AiExampleReference.tsx
@@ -0,0 +1,71 @@
+'use client';
+
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@/components/ui/select';
+
+export interface ExampleItem {
+ id: string;
+ label: string;
+ /** The text fed to the model as a style reference when this item is picked. */
+ example: string;
+}
+
+interface AiExampleReferenceProps {
+ items: ExampleItem[];
+ /** Selected item id, or null for none. */
+ value: string | null;
+ onChange: (id: string | null) => void;
+ /** e.g. "bounty" | "hackathon". */
+ noun: string;
+}
+
+const NONE = '__none__';
+
+/**
+ * Optional "use a past one as a style reference" picker for the Generate
+ * dialogs. The chosen item's gist is passed to the model as `examples[]` so the
+ * draft mirrors the organizer's house style. Renders nothing when there's
+ * nothing to reference. Shared by both wizards.
+ */
+export default function AiExampleReference({
+ items,
+ value,
+ onChange,
+ noun,
+}: AiExampleReferenceProps) {
+ if (items.length === 0) return null;
+
+ return (
+
+
+ Match a past {noun}{' '}
+ (optional)
+
+
onChange(v === NONE ? null : v)}
+ >
+
+
+
+
+ None
+ {items.map(item => (
+
+ {item.label}
+
+ ))}
+
+
+
+ We'll use its style as a reference — your brief still drives the
+ content.
+
+
+ );
+}
diff --git a/components/ai/AiRegenerateControl.tsx b/components/ai/AiRegenerateControl.tsx
new file mode 100644
index 00000000..cfc8cd18
--- /dev/null
+++ b/components/ai/AiRegenerateControl.tsx
@@ -0,0 +1,177 @@
+'use client';
+
+import { useState } from 'react';
+import { Sparkles } from 'lucide-react';
+import { toast } from 'sonner';
+
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from '@/components/ui/popover';
+import { Textarea } from '@/components/ui/textarea';
+import { BoundlessButton } from '@/components/buttons';
+import { ApiError } from '@/lib/api';
+
+type RegenData = Record;
+
+interface AiRegenerateControlProps {
+ /** Render only on AI-generated drafts. */
+ available: boolean;
+ /** True while the regenerate request is in flight. */
+ isRunning: boolean;
+ /** Run the regenerate; resolve with the proposed values (wizard section shape). */
+ onRun: (instructions: string) => Promise;
+ /** Apply the accepted values to the tab's form. */
+ onApply: (data: RegenData) => void;
+ label?: string;
+ /** Optional custom preview of the proposed values. */
+ summarize?: (data: RegenData) => string;
+ /** Pre-fill the instructions box (e.g. after a mode change). */
+ defaultInstruction?: string;
+}
+
+const truncate = (s: string, n = 80): string =>
+ s.length > n ? `${s.slice(0, n)}…` : s;
+
+/** Default compact preview: one line per changed field. */
+function defaultSummary(data: RegenData): string {
+ return Object.entries(data)
+ .map(([k, v]) => {
+ if (Array.isArray(v)) return `${k}: ${v.length} item(s)`;
+ if (v && typeof v === 'object') return `${k}: updated`;
+ return `${k}: ${truncate(String(v ?? ''))}`;
+ })
+ .join('\n');
+}
+
+/**
+ * Steerable, non-destructive "Regenerate with AI" control shared by the bounty
+ * and hackathon wizards. The organizer can add a short instruction ("make the
+ * deadline 3 weeks"), runs it, then **previews** the proposed values and chooses
+ * Apply or Discard — so a regenerate never silently overwrites in-progress edits.
+ */
+export default function AiRegenerateControl({
+ available,
+ isRunning,
+ onRun,
+ onApply,
+ label = 'Regenerate with AI',
+ summarize,
+ defaultInstruction,
+}: AiRegenerateControlProps) {
+ const [open, setOpen] = useState(false);
+ const [instructions, setInstructions] = useState(defaultInstruction ?? '');
+ const [proposed, setProposed] = useState(null);
+
+ if (!available) return null;
+
+ const reset = () => {
+ setProposed(null);
+ setInstructions(defaultInstruction ?? '');
+ };
+
+ const handleRun = async () => {
+ try {
+ const data = await onRun(instructions.trim());
+ if (!data || Object.keys(data).length === 0) {
+ toast.message('No changes were proposed. Try a different instruction.');
+ return;
+ }
+ setProposed(data);
+ } catch (err) {
+ if (err instanceof ApiError) {
+ if (err.status === 503) {
+ toast.error('The AI assistant is busy. Try again in a moment.');
+ return;
+ }
+ toast.error(err.message || 'Could not regenerate this section.');
+ return;
+ }
+ toast.error('Could not regenerate this section.');
+ }
+ };
+
+ const handleApply = () => {
+ if (proposed) onApply(proposed);
+ toast.success('Applied. Review the new values.');
+ setOpen(false);
+ reset();
+ };
+
+ return (
+ {
+ setOpen(next);
+ if (!next) reset();
+ }}
+ >
+
+
+
+ {label}
+
+
+
+ {proposed === null ? (
+
+
+
Regenerate with AI
+
+ Optionally tell the AI what to change.
+
+
+
+ ) : (
+
+
Proposed changes
+
+ {(summarize ?? defaultSummary)(proposed)}
+
+
+
+ Discard
+
+
+ Apply
+
+
+
+ )}
+
+
+ );
+}
diff --git a/components/ai/AiStreamPreview.tsx b/components/ai/AiStreamPreview.tsx
new file mode 100644
index 00000000..e947384a
--- /dev/null
+++ b/components/ai/AiStreamPreview.tsx
@@ -0,0 +1,60 @@
+'use client';
+
+import { Sparkles } from 'lucide-react';
+
+import { BoundlessButton } from '@/components/buttons';
+import LoadingSpinner from '@/components/LoadingSpinner';
+
+export interface PreviewField {
+ label: string;
+ /** Present once the field has streamed in; null shows a shimmer placeholder. */
+ value: string | null;
+}
+
+interface AiStreamPreviewProps {
+ fields: PreviewField[];
+ onCancel: () => void;
+}
+
+/**
+ * Live "draft taking shape" view for the Generate dialogs. Renders each field as
+ * it streams in (shimmer until it arrives) with a Cancel button so the organizer
+ * can bail early if it's heading the wrong way. Shared by both wizards.
+ */
+export default function AiStreamPreview({
+ fields,
+ onCancel,
+}: AiStreamPreviewProps) {
+ return (
+
+
+
+
+
+ Drafting — you can edit everything after.
+
+
+
+
+ {fields.map(f => (
+
+
{f.label}
+ {f.value ? (
+
+ {f.value}
+
+ ) : (
+
+ )}
+
+ ))}
+
+
+
+
+ Cancel
+
+
+
+ );
+}
diff --git a/components/ai/AiUsageNote.tsx b/components/ai/AiUsageNote.tsx
new file mode 100644
index 00000000..40d3e9fa
--- /dev/null
+++ b/components/ai/AiUsageNote.tsx
@@ -0,0 +1,37 @@
+'use client';
+
+import { useAiUsage } from '@/hooks/use-ai-usage';
+
+interface AiUsageNoteProps {
+ organizationId: string;
+ className?: string;
+}
+
+/**
+ * "N of 50 AI generations left this month" + spend, for the Generate dialogs.
+ * Renders nothing for unlimited tiers or before usage loads. Shared by both
+ * wizards. Turns amber when running low.
+ */
+export default function AiUsageNote({
+ organizationId,
+ className,
+}: AiUsageNoteProps) {
+ const { data } = useAiUsage(organizationId);
+ if (!data || data.limit === null || data.remaining === null) return null;
+
+ const cost = Number(data.costUsdThisMonth || '0');
+ const low = data.remaining <= Math.max(3, Math.ceil(data.limit * 0.1));
+
+ return (
+
+ {data.remaining} of {data.limit} AI generations left this month
+ {cost > 0 ? ` · $${cost.toFixed(2)} spent` : ''}
+
+ );
+}
diff --git a/components/organization/bounties/new/GenerateWithAiBountyDialog.tsx b/components/organization/bounties/new/GenerateWithAiBountyDialog.tsx
new file mode 100644
index 00000000..8fcdbeca
--- /dev/null
+++ b/components/organization/bounties/new/GenerateWithAiBountyDialog.tsx
@@ -0,0 +1,455 @@
+'use client';
+
+import { useEffect, useMemo, useRef, useState } from 'react';
+import { useRouter } from 'next/navigation';
+import { useQueryClient } from '@tanstack/react-query';
+import { useForm } from 'react-hook-form';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { z } from 'zod';
+import { toast } from 'sonner';
+import { Sparkles } from 'lucide-react';
+
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from '@/components/ui/dialog';
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from '@/components/ui/form';
+import { Input } from '@/components/ui/input';
+import { Textarea } from '@/components/ui/textarea';
+import { BoundlessButton } from '@/components/buttons';
+import AiClarifyQuestions, {
+ type ClarifyAnswer,
+} from '@/components/ai/AiClarifyQuestions';
+import AiExampleReference, {
+ type ExampleItem,
+} from '@/components/ai/AiExampleReference';
+import AiUsageNote from '@/components/ai/AiUsageNote';
+import AiStreamPreview, {
+ type PreviewField,
+} from '@/components/ai/AiStreamPreview';
+import { streamDraft } from '@/lib/ai/stream-draft';
+import {
+ useClarifyBountyDraft,
+ useOrganizationBounties,
+ bountyKeys,
+ type ClarifyQuestion,
+} from '@/features/bounties';
+
+/** Fold chosen clarify answers back into the brief before drafting. */
+function augmentBrief(brief: string, answers: ClarifyAnswer[]): string {
+ if (answers.length === 0) return brief;
+ const lines = answers.map(a => `- ${a.question} ${a.label}`).join('\n');
+ return `${brief}\n\nAdditional details:\n${lines}`;
+}
+
+const briefSchema = z.object({
+ brief: z
+ .string()
+ .trim()
+ .min(10, 'Add a little more detail (at least 10 characters).')
+ .max(2000, 'Keep the brief under 2000 characters.'),
+ budgetCapUsdc: z
+ .string()
+ .trim()
+ .regex(/^\d+(\.\d+)?$/, 'Enter a number, e.g. 500.'),
+ earliestStart: z
+ .string()
+ .regex(/^\d{4}-\d{2}-\d{2}$/, 'Pick an earliest start date.'),
+});
+
+type BriefForm = z.infer;
+
+/**
+ * Curated starter briefs. Static (no admin CMS yet) — selecting one fills the
+ * brief textarea; the organizer edits freely from there.
+ */
+interface BriefTemplate {
+ id: string;
+ name: string;
+ description: string;
+ brief: string;
+}
+
+const BRIEF_TEMPLATES: BriefTemplate[] = [
+ {
+ id: 'design-single',
+ name: 'Design task',
+ description: 'One winner, paid on the best entry',
+ brief:
+ 'A one-week bounty to design three onboarding illustrations for a Stellar wallet app, in a clean flat style. Pay the single best entry.',
+ },
+ {
+ id: 'dev-issue',
+ name: 'Dev fix',
+ description: 'GitHub issue, vetted applicants',
+ brief:
+ 'A development bounty to fix a flaky integration test in our open-source Soroban SDK. Reviewers should vet a short application before work starts. GitHub issue: https://github.com/example/sdk/issues/42',
+ },
+ {
+ id: 'content-competition',
+ name: 'Content competition',
+ description: 'Multiple winners, ranked',
+ brief:
+ 'A two-week competition for written tutorials explaining how to build on Stellar, aimed at new developers. Reward the top three entries.',
+ },
+];
+
+function todayIso(): string {
+ return new Date().toISOString().slice(0, 10);
+}
+
+function TemplateChip({
+ template,
+ selected,
+ onSelect,
+}: {
+ template: BriefTemplate;
+ selected: boolean;
+ onSelect: () => void;
+}) {
+ return (
+
+ {template.name}
+
+ {template.description}
+
+
+ );
+}
+
+interface GenerateWithAiBountyDialogProps {
+ organizationId: string;
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+}
+
+export default function GenerateWithAiBountyDialog({
+ organizationId,
+ open,
+ onOpenChange,
+}: GenerateWithAiBountyDialogProps) {
+ const router = useRouter();
+ const queryClient = useQueryClient();
+ const clarify = useClarifyBountyDraft(organizationId);
+ const [selectedTemplateId, setSelectedTemplateId] = useState(
+ null
+ );
+ // Live streaming reveal: partial draft snapshots + an abort handle for Cancel.
+ const [streaming, setStreaming] = useState(false);
+ const [partial, setPartial] = useState | null>(null);
+ const abortRef = useRef(null);
+ // Adaptive clarify gate: when the brief is thin we collect a couple of chip
+ // answers before drafting.
+ const [questions, setQuestions] = useState([]);
+ const [pending, setPending] = useState(null);
+ // Optional "match a past bounty" style reference (fed to the model as examples).
+ const [exampleId, setExampleId] = useState(null);
+ const { data: pastBounties } = useOrganizationBounties(organizationId);
+ const exampleItems: ExampleItem[] = useMemo(
+ () =>
+ (pastBounties ?? []).map(b => ({
+ id: b.id,
+ label: b.title || 'Untitled bounty',
+ example: `A past bounty titled "${b.title}"${
+ b.rewardAmount
+ ? ` with a ${b.rewardAmount} ${b.rewardCurrency ?? 'USDC'} reward`
+ : ''
+ }.`,
+ })),
+ [pastBounties]
+ );
+ const exampleText =
+ exampleItems.find(i => i.id === exampleId)?.example ?? null;
+
+ const form = useForm({
+ resolver: zodResolver(briefSchema),
+ defaultValues: { brief: '', budgetCapUsdc: '', earliestStart: todayIso() },
+ });
+
+ const isClarifying = clarify.isPending;
+
+ useEffect(() => {
+ if (!open) {
+ setSelectedTemplateId(null);
+ setQuestions([]);
+ setPending(null);
+ setExampleId(null);
+ setStreaming(false);
+ setPartial(null);
+ }
+ }, [open]);
+
+ const handleClose = (next: boolean) => {
+ if (streaming) return;
+ onOpenChange(next);
+ };
+
+ const cancelStream = () => {
+ abortRef.current?.abort();
+ setStreaming(false);
+ setPartial(null);
+ };
+
+ const handleSelectTemplate = (template: BriefTemplate) => {
+ if (selectedTemplateId === template.id) {
+ setSelectedTemplateId(null);
+ return;
+ }
+ setSelectedTemplateId(template.id);
+ form.setValue('brief', template.brief, { shouldValidate: true });
+ };
+
+ const runGenerate = async (values: BriefForm, answers: ClarifyAnswer[]) => {
+ const ac = new AbortController();
+ abortRef.current = ac;
+ setPartial(null);
+ setStreaming(true);
+ await streamDraft(
+ `/api/organizations/${organizationId}/bounties/draft/from-brief/stream`,
+ {
+ ...values,
+ brief: augmentBrief(values.brief, answers),
+ ...(exampleText ? { examples: [exampleText] } : {}),
+ },
+ ac.signal,
+ {
+ onPartial: setPartial,
+ onDone: ({ draftId, draft }) => {
+ if (draft) {
+ queryClient.setQueryData(
+ bountyKeys.draft(organizationId, draftId),
+ draft
+ );
+ }
+ queryClient.invalidateQueries({
+ queryKey: bountyKeys.drafts(organizationId),
+ });
+ toast.success('Draft generated. Review and publish when ready.');
+ setStreaming(false);
+ onOpenChange(false);
+ router.push(
+ `/organizations/${organizationId}/bounties/drafts/${draftId}`
+ );
+ },
+ onError: e => {
+ setStreaming(false);
+ handleStreamError(e);
+ },
+ }
+ );
+ };
+
+ const onSubmit = async (values: BriefForm) => {
+ // Adaptive gate: ask the AI whether the brief needs clarifying questions.
+ try {
+ const verdict = await clarify.mutateAsync(values.brief);
+ if (!verdict.ready && verdict.questions.length > 0) {
+ setPending(values);
+ setQuestions(verdict.questions);
+ return;
+ }
+ } catch {
+ // Clarify is a soft gate — if it fails, draft from the brief as-is.
+ }
+ await runGenerate(values, []);
+ };
+
+ const handleStreamError = (err: { status?: number; message: string }) => {
+ if (err.status === 503) {
+ toast.error('The AI assistant is busy right now.', {
+ description: 'You can start your bounty manually instead.',
+ action: {
+ label: 'Start manually',
+ onClick: () => {
+ onOpenChange(false);
+ router.push(`/organizations/${organizationId}/bounties/new`);
+ },
+ },
+ });
+ return;
+ }
+ toast.error(err.message || 'Could not generate a draft. Please try again.');
+ };
+
+ // Live preview fields, derived from the streaming partial suggestion.
+ const previewFields: PreviewField[] = [
+ {
+ label: 'Title',
+ value: typeof partial?.title === 'string' ? partial.title : null,
+ },
+ {
+ label: 'Description',
+ value:
+ typeof partial?.description === 'string'
+ ? partial.description.slice(0, 240)
+ : null,
+ },
+ {
+ label: 'Mode',
+ value:
+ partial?.entryType && partial?.claimType
+ ? `${String(partial.entryType)} · ${String(partial.claimType)}`
+ : null,
+ },
+ {
+ label: 'Prizes',
+ value:
+ Array.isArray(partial?.prizeTiers) && partial.prizeTiers.length > 0
+ ? `${partial.prizeTiers.length} tier(s)`
+ : null,
+ },
+ ];
+
+ return (
+
+
+
+
+
+ Generate with AI
+
+
+ Describe your bounty or pick a template below. We'll pick a
+ sensible mode and draft the scope, submission rules, and prize tiers
+ for you to review and edit.
+
+
+
+ {streaming ? (
+
+ ) : questions.length > 0 && pending ? (
+ runGenerate(pending, answers)}
+ onSkip={() => runGenerate(pending, [])}
+ />
+ ) : (
+
+
+ )}
+
+
+ );
+}
diff --git a/components/organization/bounties/new/NewBountyTab.tsx b/components/organization/bounties/new/NewBountyTab.tsx
index 362b860b..aabaee09 100644
--- a/components/organization/bounties/new/NewBountyTab.tsx
+++ b/components/organization/bounties/new/NewBountyTab.tsx
@@ -2,13 +2,38 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
+import { useQuery } from '@tanstack/react-query';
import { toast } from 'sonner';
+import { Sparkles } from 'lucide-react';
import { Tabs, TabsContent } from '@/components/ui/tabs';
+import { BoundlessButton } from '@/components/buttons';
+import AiAssumptionsBanner from '@/components/ai/AiAssumptionsBanner';
+import AiBriefPanel from '@/components/ai/AiBriefPanel';
+import FundingConfirmationModal from '@/components/organization/funding/FundingConfirmationModal';
+import type { FundingSourceItem } from '@/components/organization/funding/FundingConfirmationModal';
+import FundingProgressModal from '@/components/organization/funding/FundingProgressModal';
+import type { FundingMode } from '@/components/organization/funding/types';
+import {
+ useDraft,
+ requestBountyFundingOtp,
+ verifyBountyFundingOtp,
+ type BountyDraftWithAi,
+} from '@/features/bounties';
+import { useTreasuryWallets } from '@/features/treasury';
+import { connectWallet } from '@/lib/wallet/wallet-kit';
+import { getWalletBalanceByAddress } from '@/lib/api/wallet';
+import { formatAddress } from '@/lib/wallet-utils';
+import {
+ getBountyPrizePool,
+ getBountyPlatformFee,
+ getBountyTotalFunding,
+} from '@/lib/utils/bounty-escrow';
import { useBountySteps } from '@/hooks/use-bounty-steps';
import { useBountyDraft } from '@/hooks/use-bounty-draft';
import { useBountyPublish } from '@/hooks/use-bounty-publish';
import { buildMockBountyData } from './mock-data';
+import GenerateWithAiBountyDialog from './GenerateWithAiBountyDialog';
import ScopeTab from './tabs/ScopeTab';
import ModeTab from './tabs/ModeTab';
import SubmissionModelTab from './tabs/SubmissionModelTab';
@@ -35,9 +60,9 @@ interface NewBountyTabProps {
* tabs. The ModeTab feeds the chosen mode into the SubmissionModelTab so its
* conditional fields render correctly.
*
- * Scope / Reward / Review tabs arrive in #600 and the publish + funding flow in
- * #601; their wiring seams (saveSection, draftId, navigateToStep) are in place
- * here. There are intentionally no AI entry points.
+ * Organizer Assist (AI) entry points: a "Start faster with AI" banner on the
+ * empty wizard opens GenerateWithAiBountyDialog (brief -> full draft), and each
+ * AI-generated section offers a per-section "Regenerate with AI" button.
*/
export default function NewBountyTab({
organizationId,
@@ -159,30 +184,185 @@ export default function NewBountyTab({
? { entryType: stepData.mode.entryType, claimType: stepData.mode.claimType }
: undefined;
- // Publish via the shared escrow runner. Defaults to MANAGED (the connected
- // custodial wallet funds + the backend signs); the funding-source picker
- // (external wallet / treasury) is a follow-up.
- const { publish, isPublishing, publishResponse } = useBountyPublish({
- organizationId: derivedOrgId || '',
- stepData,
- draftId,
- fundingMode: 'MANAGED',
+ // ---- Funding (publish) flow: the same modal hackathons use ----
+ const [selectedSourceId, setSelectedSourceId] = useState('external');
+ const [confirmOpen, setConfirmOpen] = useState(false);
+ const [progressOpen, setProgressOpen] = useState(false);
+ const [externalAddress, setExternalAddress] = useState(null);
+ const [connecting, setConnecting] = useState(false);
+
+ const rewardData = stepData.reward;
+ const totalPrizePool = rewardData ? getBountyPrizePool(rewardData) : 0;
+ const platformFee = rewardData ? getBountyPlatformFee(rewardData) : 0;
+ const totalFunding = rewardData ? getBountyTotalFunding(rewardData) : 0;
+
+ // Selectable funding sources: org treasury (managed) wallets + a connected
+ // (external) wallet. Mirrors the hackathon picker.
+ const treasuryQuery = useTreasuryWallets(derivedOrgId);
+ const fundingSources = useMemo(() => {
+ const items: FundingSourceItem[] = [];
+ for (const w of treasuryQuery.data ?? []) {
+ if (w.kind === 'MANAGED' && w.status === 'ACTIVE') {
+ items.push({
+ id: w.id,
+ kind: 'MANAGED_TREASURY',
+ label: `${w.label} (treasury)`,
+ description: `Org treasury wallet · ${formatAddress(w.publicKey, 4)}`,
+ address: w.publicKey,
+ });
+ }
+ }
+ items.push({
+ id: 'external',
+ kind: 'EXTERNAL',
+ label: 'Connected wallet',
+ description: 'You sign the transaction in your own wallet.',
+ address: externalAddress,
+ });
+ return items;
+ }, [treasuryQuery.data, externalAddress]);
+
+ // Default to the org's canonical treasury wallet when one exists, unless the
+ // organizer explicitly picks another.
+ const sourcePickedRef = useRef(false);
+ const defaultTreasuryId = useMemo(() => {
+ const list = treasuryQuery.data ?? [];
+ const def =
+ list.find(
+ w => w.kind === 'MANAGED' && w.status === 'ACTIVE' && w.isDefault
+ ) ?? list.find(w => w.kind === 'MANAGED' && w.status === 'ACTIVE');
+ return def?.id ?? null;
+ }, [treasuryQuery.data]);
+ useEffect(() => {
+ if (!sourcePickedRef.current && defaultTreasuryId) {
+ setSelectedSourceId(defaultTreasuryId);
+ }
+ }, [defaultTreasuryId]);
+
+ const handleSelectSource = useCallback((id: string) => {
+ sourcePickedRef.current = true;
+ setSelectedSourceId(id);
+ }, []);
+
+ // Funding mode + owner derived from the explicit selection id (see the
+ // hackathon note: never key off a list-fallback that could silently flip a
+ // treasury selection to the connected wallet).
+ const isExternalSource = selectedSourceId === 'external';
+ const selectedTreasury = isExternalSource
+ ? undefined
+ : fundingSources.find(
+ s => s.id === selectedSourceId && s.kind === 'MANAGED_TREASURY'
+ );
+ const fundingMode: FundingMode = isExternalSource ? 'EXTERNAL' : 'MANAGED';
+ const treasurySource = selectedTreasury?.address
+ ? { walletId: selectedTreasury.id, address: selectedTreasury.address }
+ : null;
+ const externalOwnerAddress = isExternalSource ? externalAddress : null;
+ const funderAddress = isExternalSource
+ ? externalAddress
+ : (selectedTreasury?.address ?? null);
+
+ const sourceAddress = funderAddress ?? undefined;
+ const sourceBalanceQuery = useQuery({
+ queryKey: ['wallet-balance', sourceAddress],
+ queryFn: () => getWalletBalanceByAddress(sourceAddress as string),
+ enabled: !!sourceAddress,
+ staleTime: 15_000,
});
+ const selectedSourceUsdc = sourceAddress
+ ? Number.parseFloat(sourceBalanceQuery.data?.usdc ?? '')
+ : undefined;
+ const balanceLoading = !!sourceAddress && sourceBalanceQuery.isLoading;
+
+ const { publish, isPublishing, escrowPhase, escrowError, escrowTxHash } =
+ useBountyPublish({
+ organizationId: derivedOrgId || '',
+ stepData,
+ draftId,
+ fundingMode,
+ externalOwnerAddress,
+ treasurySource,
+ });
- // Once the publish finalizes on-chain, leave the wizard for the organizer's
- // bounty list rather than lingering on the (now read-only) review.
- const hasRedirectedRef = useRef(false);
+ const isFundingComplete = escrowPhase === 'completed';
+ const isFundingFailed = escrowPhase === 'failed';
+ const escrowPhaseRef = useRef(escrowPhase);
useEffect(() => {
- if (publishResponse && derivedOrgId && !hasRedirectedRef.current) {
- hasRedirectedRef.current = true;
- router.replace(`/organizations/${derivedOrgId}/bounties`);
+ escrowPhaseRef.current = escrowPhase;
+ }, [escrowPhase]);
+
+ const goToBountyList = useCallback(() => {
+ if (derivedOrgId) router.replace(`/organizations/${derivedOrgId}/bounties`);
+ }, [derivedOrgId, router]);
+
+ const runFunding = useCallback(async () => {
+ try {
+ await publish();
+ updateStepCompletion('review', true);
+ } catch {
+ // Failure surfaced via escrowPhase; the progress modal offers retry.
}
- }, [publishResponse, derivedOrgId, router]);
+ }, [publish, updateStepCompletion]);
+
+ const handleConfirmFunding = async () => {
+ setConfirmOpen(false);
+ setProgressOpen(true);
+ await runFunding();
+ // Pre-flight bailed before the runner started -> nothing to show.
+ if (escrowPhaseRef.current === 'idle') setProgressOpen(false);
+ };
+
+ // Funding step-up (email OTP). Backend decides if required; no-op when off.
+ const requestOtp = useCallback(async () => {
+ if (!derivedOrgId || !draftId) {
+ return { required: false, alreadyVerified: false, sent: false };
+ }
+ const res = await requestBountyFundingOtp(derivedOrgId, draftId);
+ return {
+ required: res.required,
+ alreadyVerified: res.alreadyVerified,
+ sent: res.sent,
+ };
+ }, [derivedOrgId, draftId]);
+
+ const verifyOtp = useCallback(
+ async (code: string) => {
+ if (!derivedOrgId || !draftId) return;
+ await verifyBountyFundingOtp(derivedOrgId, draftId, code);
+ },
+ [derivedOrgId, draftId]
+ );
+
+ const handleConnectExternal = useCallback(async () => {
+ setConnecting(true);
+ try {
+ const { address } = await connectWallet();
+ setExternalAddress(address);
+ } catch (err) {
+ toast.error(
+ err instanceof Error ? err.message : 'Could not connect a wallet.'
+ );
+ } finally {
+ setConnecting(false);
+ }
+ }, []);
// Dev-only convenience: fill every section with valid mock data, persist it
// in one PATCH, and jump to Review. Never rendered in production builds.
const isDev = process.env.NODE_ENV === 'development';
const [isFillingMock, setIsFillingMock] = useState(false);
+ const [aiDialogOpen, setAiDialogOpen] = useState(false);
+ // Offer AI drafting only on a fresh, empty wizard (no draft created and no
+ // scope entered yet) so it never overwrites in-progress work.
+ const showAiBanner = !draftId && !stepData.scope;
+
+ // AI assumptions (from the draft response) surfaced on the review step so the
+ // organizer can see and correct every guess the AI made.
+ const { data: draftRecord } = useDraft(derivedOrgId ?? '', draftId);
+ const aiGenerationRecord = (draftRecord as BountyDraftWithAi | undefined)
+ ?.aiGeneration;
+ const aiAssumptions = aiGenerationRecord?.assumptions ?? [];
+ const aiBrief = aiGenerationRecord?.brief ?? null;
const handleFillMock = useCallback(async () => {
setIsFillingMock(true);
try {
@@ -232,6 +412,44 @@ export default function NewBountyTab({
)}
+ {showAiBanner && derivedOrgId && (
+
+
+
+
+
+
+
+
+ Start faster with AI
+
+
+ Describe your bounty and we'll draft the scope,
+ submission rules, and prizes for you to review and edit.
+
+
+
+
}
+ iconPosition='left'
+ onClick={() => setAiDialogOpen(true)}
+ >
+ Generate with AI
+
+
+
+ )}
+
+ {derivedOrgId && (
+
+ )}
+
@@ -294,20 +512,67 @@ export default function NewBountyTab({
+ {aiBrief && }
+ {aiAssumptions.length > 0 && (
+ navigateToStep(section as StepKey)}
+ className='mb-6'
+ />
+ )}
navigateToStep('resources')}
onSaveDraft={saveDraft}
isSavingDraft={isSavingDraft}
- onPublish={async () => {
- await publish();
- }}
+ onPublish={() => setConfirmOpen(true)}
isLoading={isPublishing}
/>
+
+ {draftId && derivedOrgId && (
+ <>
+
+ {
+ setProgressOpen(false);
+ if (isFundingComplete) goToBountyList();
+ }}
+ onRetry={() => void runFunding()}
+ onSwitchToDraft={() => setProgressOpen(false)}
+ onView={goToBountyList}
+ entityNoun='bounty'
+ />
+ >
+ )}
);
}
diff --git a/components/organization/bounties/new/RegenerateBountySectionButton.tsx b/components/organization/bounties/new/RegenerateBountySectionButton.tsx
new file mode 100644
index 00000000..6ebbefd6
--- /dev/null
+++ b/components/organization/bounties/new/RegenerateBountySectionButton.tsx
@@ -0,0 +1,59 @@
+'use client';
+
+import { useParams, useSearchParams } from 'next/navigation';
+
+import AiRegenerateControl from '@/components/ai/AiRegenerateControl';
+import {
+ useDraft,
+ useRegenerateBountyDraftSection,
+ type BountyDraftRegenSection,
+ type BountyDraftWithAi,
+} from '@/features/bounties';
+
+interface RegenerateBountySectionButtonProps {
+ /** Which AI section to regenerate (description | submission | reward). */
+ section: BountyDraftRegenSection;
+ /** Apply the regenerated values (wizard section shape) to the tab's form. */
+ onApply: (data: Record) => void;
+ label?: string;
+ /** Pre-fill the instructions box (e.g. after a mode change). */
+ defaultInstruction?: string;
+}
+
+/**
+ * Per-section "Regenerate with AI" for the bounty wizard. Derives the org +
+ * draft from the route, renders only on AI-generated drafts, and delegates the
+ * steerable + preview/accept-discard UX to the shared AiRegenerateControl.
+ */
+export default function RegenerateBountySectionButton({
+ section,
+ onApply,
+ label,
+ defaultInstruction,
+}: RegenerateBountySectionButtonProps) {
+ const params = useParams();
+ const searchParams = useSearchParams();
+ const organizationId = (params?.id as string) ?? '';
+ const draftId =
+ (params?.draftId as string) ?? searchParams.get('draftId') ?? '';
+
+ const { data: draft } = useDraft(organizationId, draftId || undefined);
+ const regenerate = useRegenerateBountyDraftSection(organizationId);
+
+ const aiGeneration = (draft as BountyDraftWithAi | undefined)?.aiGeneration;
+
+ return (
+
+ regenerate
+ .mutateAsync({ draftId, body: { section, instructions } })
+ .then(r => r.data as Record)
+ }
+ onApply={onApply}
+ />
+ );
+}
diff --git a/components/organization/bounties/new/tabs/RewardTab.tsx b/components/organization/bounties/new/tabs/RewardTab.tsx
index d2dbb77e..1aaffe1f 100644
--- a/components/organization/bounties/new/tabs/RewardTab.tsx
+++ b/components/organization/bounties/new/tabs/RewardTab.tsx
@@ -30,6 +30,7 @@ import {
} from '@/lib/utils/bounty-escrow';
import { MAX_PRIZE_TIERS, type BountyClaimType } from './schemas/modeSchema';
import { makeRewardSchema, type RewardFormData } from './schemas/rewardSchema';
+import RegenerateBountySectionButton from '../RegenerateBountySectionButton';
interface RewardTabProps {
/** The claim type + winner count chosen in ModeTab; drives the tier count. */
@@ -121,6 +122,33 @@ export default function RewardTab({
return (
+
+ {
+ if (typeof data.rewardCurrency === 'string') {
+ form.setValue('rewardCurrency', data.rewardCurrency, {
+ shouldDirty: true,
+ });
+ }
+ if (Array.isArray(data.prizeTiers)) {
+ replace(
+ (
+ data.prizeTiers as Array<{
+ position: number;
+ amount: string;
+ passMark?: number | null;
+ }>
+ ).map(t => ({
+ position: t.position,
+ amount: String(t.amount),
+ passMark: t.passMark ?? null,
+ }))
+ );
+ }
+ }}
+ />
+
(
-
- Description*
-
+
+
+ Description*
+
+ {
+ if (typeof data.description === 'string') {
+ form.setValue('description', data.description, {
+ shouldValidate: true,
+ shouldDirty: true,
+ });
+ }
+ }}
+ />
+
{
return Number.isNaN(n) ? null : n;
};
+/** Trim an ISO date-time to the `YYYY-MM-DDTHH:mm` a datetime-local input wants. */
+const toDatetimeLocal = (iso: unknown): string | null => {
+ if (typeof iso !== 'string' || iso.trim() === '') return null;
+ const d = new Date(iso);
+ if (Number.isNaN(d.getTime())) return null;
+ return iso.slice(0, 16);
+};
+
export default function SubmissionModelTab({
mode,
category,
@@ -79,6 +91,23 @@ export default function SubmissionModelTab({
// raise it but not drop below it.
const reputationFloor = category ? CATEGORY_REPUTATION_BASELINE[category] : 0;
+ // Detect a mode change since the AI generated this draft, so we can nudge the
+ // organizer to regenerate the (now mode-dependent) submission settings.
+ const routeParams = useParams();
+ const searchParams = useSearchParams();
+ const organizationId = (routeParams?.id as string) ?? '';
+ const draftId =
+ (routeParams?.draftId as string) ?? searchParams.get('draftId') ?? '';
+ const { data: draftRecord } = useDraft(organizationId, draftId || undefined);
+ const generatedMode = (draftRecord as BountyDraftWithAi | undefined)
+ ?.aiGeneration?.generatedMode;
+ const modeChanged = Boolean(
+ generatedMode &&
+ mode &&
+ (generatedMode.entryType !== mode.entryType ||
+ generatedMode.claimType !== mode.claimType)
+ );
+
const form = useForm({
resolver: zodResolver(
makeSubmissionModelSchema(
@@ -154,6 +183,51 @@ export default function SubmissionModelTab({
return (
+ {modeChanged && (
+
+
+ You changed the mode since this was drafted. Regenerate the
+ submission settings so they match the new mode.
+
+
+ )}
+
+ {
+ const deadline = toDatetimeLocal(data.submissionDeadline);
+ if (deadline)
+ form.setValue('submissionDeadline', deadline, {
+ shouldValidate: true,
+ shouldDirty: true,
+ });
+ if ('applicationWindowCloseAt' in data)
+ form.setValue(
+ 'applicationWindowCloseAt',
+ toDatetimeLocal(data.applicationWindowCloseAt)
+ );
+ if (typeof data.maxApplicants === 'number')
+ form.setValue('maxApplicants', data.maxApplicants);
+ if (typeof data.shortlistSize === 'number')
+ form.setValue('shortlistSize', data.shortlistSize);
+ if (
+ typeof data.reputationMinimum === 'number' &&
+ fields.reputationMinimum !== 'hidden'
+ )
+ form.setValue('reputationMinimum', data.reputationMinimum);
+ if (typeof data.applicationCreditCost === 'number')
+ form.setValue(
+ 'applicationCreditCost',
+ data.applicationCreditCost
+ );
+ }}
+ />
+
{/* Submission deadline (always required) */}
Promise;
verifyOtp?: (code: string) => Promise;
+ /** Entity word for the copy, e.g. "hackathon" | "bounty". Defaults to "event". */
+ entityNoun?: string;
}
function sourceIcon(kind: FundingSourceKind) {
@@ -104,6 +106,7 @@ export default function FundingConfirmationModal({
isSubmitting = false,
requestOtp,
verifyOtp,
+ entityNoun = 'event',
}: FundingConfirmationModalProps) {
const selected =
sources.find(s => s.id === selectedSourceId) ?? sources[0] ?? null;
@@ -278,7 +281,7 @@ export default function FundingConfirmationModal({
- Fund & publish hackathon
+ Fund & publish {entityNoun}
Publishing sets aside your prize pool so it is ready to pay
diff --git a/components/organization/hackathons/new/FundingProgressModal.tsx b/components/organization/funding/FundingProgressModal.tsx
similarity index 92%
rename from components/organization/hackathons/new/FundingProgressModal.tsx
rename to components/organization/funding/FundingProgressModal.tsx
index 4ea8f9e4..9d7c8b8f 100644
--- a/components/organization/hackathons/new/FundingProgressModal.tsx
+++ b/components/organization/funding/FundingProgressModal.tsx
@@ -18,8 +18,7 @@ import {
} from '@/components/ui/dialog';
import { BoundlessButton } from '@/components/buttons';
import { getTransactionExplorerUrl } from '@/lib/wallet-utils';
-import type { EscrowRunPhase } from '@/features/hackathons';
-import type { FundingMode } from '@/features/hackathons';
+import type { EscrowRunPhase, FundingMode } from './types';
interface FundingProgressModalProps {
open: boolean;
@@ -32,7 +31,9 @@ interface FundingProgressModalProps {
onClose: () => void;
onRetry?: () => void;
onSwitchToDraft?: () => void;
- onViewHackathon?: () => void;
+ onView?: () => void;
+ /** Entity word for the copy, e.g. "hackathon" | "bounty". Defaults to "event". */
+ entityNoun?: string;
}
interface Step {
@@ -69,10 +70,12 @@ export default function FundingProgressModal({
onClose,
onRetry,
onSwitchToDraft,
- onViewHackathon,
+ onView,
+ entityNoun = 'event',
}: FundingProgressModalProps) {
const steps = fundingMode === 'EXTERNAL' ? EXTERNAL_STEPS : MANAGED_STEPS;
const dismissable = isCompleted || isFailed;
+ const Entity = entityNoun.charAt(0).toUpperCase() + entityNoun.slice(1);
const phaseIndex = steps.findIndex(s => s.phase === phase);
const activeIndex = isCompleted
@@ -103,14 +106,14 @@ export default function FundingProgressModal({
{isCompleted
- ? 'Hackathon published'
+ ? `${Entity} published`
: isFailed
? 'Funding failed'
- : 'Funding your hackathon'}
+ : `Funding your ${entityNoun}`}
{isCompleted
- ? 'Your prize pool is secured and the hackathon is live.'
+ ? `Your prize pool is secured and the ${entityNoun} is live.`
: isFailed
? 'No funds moved. You can retry or move it back to draft.'
: 'Keep this open while we set up your prize pool.'}
@@ -175,9 +178,9 @@ export default function FundingProgressModal({
>
Close
- {onViewHackathon && (
-
- View hackathon
+ {onView && (
+
+ View {entityNoun}
)}
>
diff --git a/components/organization/funding/types.ts b/components/organization/funding/types.ts
new file mode 100644
index 00000000..eefd6017
--- /dev/null
+++ b/components/organization/funding/types.ts
@@ -0,0 +1,19 @@
+/**
+ * Shared types for the organizer funding modals (used by both the hackathon and
+ * bounty publish flows). Structurally identical to the per-feature escrow types
+ * (features/hackathons, features/bounties), so either feature's values pass
+ * here without coupling the shared modals to a specific feature.
+ */
+
+/** Escrow op lifecycle phase, as surfaced by useEscrowOpRunner. */
+export type EscrowRunPhase =
+ | 'idle'
+ | 'starting'
+ | 'signing'
+ | 'submitting'
+ | 'polling'
+ | 'completed'
+ | 'failed';
+
+/** How the escrow is funded: managed (server-signed) or external (wallet-signed). */
+export type FundingMode = 'MANAGED' | 'EXTERNAL';
diff --git a/components/organization/hackathons/new/GenerateWithAiDialog.tsx b/components/organization/hackathons/new/GenerateWithAiDialog.tsx
index 39bd8ea6..a53ad1de 100644
--- a/components/organization/hackathons/new/GenerateWithAiDialog.tsx
+++ b/components/organization/hackathons/new/GenerateWithAiDialog.tsx
@@ -1,7 +1,8 @@
'use client';
-import { useEffect, useState } from 'react';
+import { useEffect, useMemo, useRef, useState } from 'react';
import { useRouter } from 'next/navigation';
+import { useQueryClient } from '@tanstack/react-query';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
@@ -27,13 +28,31 @@ import {
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { BoundlessButton } from '@/components/buttons';
-import LoadingSpinner from '@/components/LoadingSpinner';
-import { ApiError } from '@/lib/api';
-import {
- useGenerateDraftFromBrief,
- useBriefTemplates,
+import AiClarifyQuestions, {
+ type ClarifyAnswer,
+} from '@/components/ai/AiClarifyQuestions';
+import AiExampleReference, {
+ type ExampleItem,
+} from '@/components/ai/AiExampleReference';
+import AiUsageNote from '@/components/ai/AiUsageNote';
+import AiStreamPreview, {
+ type PreviewField,
+} from '@/components/ai/AiStreamPreview';
+import { streamDraft } from '@/lib/ai/stream-draft';
+import { useHackathons } from '@/hooks/use-hackathons';
+import { useClarifyDraft, useBriefTemplates } from '@/features/hackathons';
+import { hackathonKeys } from '@/features/hackathons/api/keys';
+import type {
+ BriefTemplate,
+ HackathonClarifyQuestion,
} from '@/features/hackathons';
-import type { BriefTemplate } from '@/features/hackathons';
+
+/** Fold chosen clarify answers back into the brief before drafting. */
+function augmentBrief(brief: string, answers: ClarifyAnswer[]): string {
+ if (answers.length === 0) return brief;
+ const lines = answers.map(a => `- ${a.question} ${a.label}`).join('\n');
+ return `${brief}\n\nAdditional details:\n${lines}`;
+}
const briefSchema = z.object({
brief: z
@@ -52,14 +71,6 @@ const briefSchema = z.object({
type BriefForm = z.infer;
-const PROGRESS_STEPS = [
- 'Reading your brief…',
- 'Shaping tracks and prizes…',
- 'Drafting judging criteria…',
- 'Laying out the timeline…',
- 'Finalizing your draft…',
-];
-
function todayIso(): string {
return new Date().toISOString().slice(0, 10);
}
@@ -104,38 +115,70 @@ export default function GenerateWithAiDialog({
onOpenChange,
}: GenerateWithAiDialogProps) {
const router = useRouter();
- const generate = useGenerateDraftFromBrief(organizationId);
+ const queryClient = useQueryClient();
+ const clarify = useClarifyDraft(organizationId);
const { data: templates } = useBriefTemplates();
- const [progressStep, setProgressStep] = useState(0);
const [selectedTemplateId, setSelectedTemplateId] = useState(
null
);
+ // Live streaming reveal: partial draft snapshots + an abort handle for Cancel.
+ const [streaming, setStreaming] = useState(false);
+ const [partial, setPartial] = useState | null>(null);
+ const abortRef = useRef(null);
+ // Adaptive clarify gate: when the brief is thin we collect a couple of chip
+ // answers before drafting.
+ const [questions, setQuestions] = useState([]);
+ const [pending, setPending] = useState(null);
+ // Optional "match a past hackathon" style reference (fed to the model as examples).
+ const [exampleId, setExampleId] = useState(null);
+ const { hackathons: pastHackathons } = useHackathons({
+ organizationId,
+ pageSize: 20,
+ });
+ const exampleItems: ExampleItem[] = useMemo(
+ () =>
+ (pastHackathons ?? []).map(h => {
+ const desc = (h.description ?? '').slice(0, 400);
+ return {
+ id: h.id,
+ label: h.name || 'Untitled hackathon',
+ example: `A past hackathon called "${h.name}".${
+ desc ? `\n${desc}` : ''
+ }`,
+ };
+ }),
+ [pastHackathons]
+ );
+ const exampleText =
+ exampleItems.find(i => i.id === exampleId)?.example ?? null;
const form = useForm({
resolver: zodResolver(briefSchema),
defaultValues: { brief: '', budgetCapUsdc: '', earliestStart: todayIso() },
});
- const isGenerating = generate.isPending;
+ const isClarifying = clarify.isPending;
+ // Reset selected template + clarify + stream state when dialog closes.
useEffect(() => {
- if (!isGenerating) {
- setProgressStep(0);
- return;
+ if (!open) {
+ setSelectedTemplateId(null);
+ setQuestions([]);
+ setPending(null);
+ setExampleId(null);
+ setStreaming(false);
+ setPartial(null);
}
- const id = setInterval(() => {
- setProgressStep(step => Math.min(step + 1, PROGRESS_STEPS.length - 1));
- }, 4000);
- return () => clearInterval(id);
- }, [isGenerating]);
-
- // Reset selected template when dialog closes.
- useEffect(() => {
- if (!open) setSelectedTemplateId(null);
}, [open]);
+ const cancelStream = () => {
+ abortRef.current?.abort();
+ setStreaming(false);
+ setPartial(null);
+ };
+
const handleClose = (next: boolean) => {
- if (isGenerating) return;
+ if (streaming) return;
if (!next)
form.reset({ ...form.getValues(), brief: form.getValues().brief });
onOpenChange(next);
@@ -150,51 +193,103 @@ export default function GenerateWithAiDialog({
form.setValue('brief', template.brief, { shouldValidate: true });
};
+ const runGenerate = async (values: BriefForm, answers: ClarifyAnswer[]) => {
+ const ac = new AbortController();
+ abortRef.current = ac;
+ setPartial(null);
+ setStreaming(true);
+ await streamDraft(
+ `/api/organizations/${organizationId}/hackathons/draft/from-brief/stream`,
+ {
+ ...values,
+ brief: augmentBrief(values.brief, answers),
+ ...(exampleText ? { examples: [exampleText] } : {}),
+ },
+ ac.signal,
+ {
+ onPartial: setPartial,
+ onDone: ({ draftId, draft }) => {
+ if (draft) {
+ queryClient.setQueryData(
+ hackathonKeys.draft(organizationId, draftId),
+ draft
+ );
+ }
+ queryClient.invalidateQueries({
+ queryKey: hackathonKeys.drafts(organizationId),
+ });
+ toast.success('Draft generated. Review and publish when ready.');
+ setStreaming(false);
+ onOpenChange(false);
+ router.push(
+ `/organizations/${organizationId}/hackathons/drafts/${draftId}`
+ );
+ },
+ onError: e => {
+ setStreaming(false);
+ handleStreamError(e);
+ },
+ }
+ );
+ };
+
const onSubmit = async (values: BriefForm) => {
+ // Adaptive gate: ask the AI whether the brief needs clarifying questions.
try {
- const result = await generate.mutateAsync(values);
- toast.success('Draft generated. Review and publish when ready.');
- onOpenChange(false);
- router.push(
- `/organizations/${organizationId}/hackathons/drafts/${result.draftId}`
- );
- } catch (err) {
- handleGenerateError(err);
+ const verdict = await clarify.mutateAsync(values.brief);
+ if (!verdict.ready && verdict.questions.length > 0) {
+ setPending(values);
+ setQuestions(verdict.questions);
+ return;
+ }
+ } catch {
+ // Clarify is a soft gate — if it fails, draft from the brief as-is.
}
+ await runGenerate(values, []);
};
- const handleGenerateError = (err: unknown) => {
- if (err instanceof ApiError) {
- if (err.status === 503) {
- toast.error('The AI assistant is busy right now.', {
- description: 'You can start your hackathon manually instead.',
- action: {
- label: 'Start manually',
- onClick: () => {
- onOpenChange(false);
- router.push(`/organizations/${organizationId}/hackathons/new`);
- },
+ const handleStreamError = (err: { status?: number; message: string }) => {
+ if (err.status === 503) {
+ toast.error('The AI assistant is busy right now.', {
+ description: 'You can start your hackathon manually instead.',
+ action: {
+ label: 'Start manually',
+ onClick: () => {
+ onOpenChange(false);
+ router.push(`/organizations/${organizationId}/hackathons/new`);
},
- });
- return;
- }
- if (err.status === 429) {
- toast.error(
- err.message || 'AI usage limit reached for this organization.'
- );
- return;
- }
- const fieldError = err.fieldErrors?.[0];
- toast.error(
- fieldError?.message ||
- err.message ||
- 'Could not generate a draft. Please try again.'
- );
+ },
+ });
return;
}
- toast.error('Could not generate a draft. Please try again.');
+ toast.error(err.message || 'Could not generate a draft. Please try again.');
};
+ // Live preview fields, derived from the streaming partial suggestion.
+ const previewFields: PreviewField[] = [
+ {
+ label: 'Name',
+ value: typeof partial?.name === 'string' ? partial.name : null,
+ },
+ {
+ label: 'Tagline',
+ value: typeof partial?.tagline === 'string' ? partial.tagline : null,
+ },
+ {
+ label: 'Tracks',
+ value: Array.isArray(partial?.tracks)
+ ? `${partial.tracks.length} track(s)`
+ : null,
+ },
+ {
+ label: 'Prizes',
+ value:
+ Array.isArray(partial?.prizeTiers) && partial.prizeTiers.length > 0
+ ? `${partial.prizeTiers.length} tier(s)`
+ : null,
+ },
+ ];
+
const hasTemplates = templates && templates.length > 0;
return (
@@ -212,16 +307,15 @@ export default function GenerateWithAiDialog({
- {isGenerating ? (
-
-
-
- {PROGRESS_STEPS[progressStep]}
-
-
- This usually takes 15–30 seconds.
-
-
+ {streaming ? (
+
+ ) : questions.length > 0 && pending ? (
+ runGenerate(pending, answers)}
+ onSkip={() => runGenerate(pending, [])}
+ />
) : (
@@ -244,6 +338,13 @@ export default function GenerateWithAiDialog({
)}
+
+
+
+
}
iconPosition='left'
>
diff --git a/components/organization/hackathons/new/NewHackathonTab.tsx b/components/organization/hackathons/new/NewHackathonTab.tsx
index 28d12a52..3c638120 100644
--- a/components/organization/hackathons/new/NewHackathonTab.tsx
+++ b/components/organization/hackathons/new/NewHackathonTab.tsx
@@ -17,17 +17,21 @@ import ResourcesTab from './tabs/ResourcesTab';
import JudgingTab from './tabs/JudgingTab';
import CollaborationTab from './tabs/CollaborationTab';
import ReviewTab from './tabs/ReviewTab';
-import FundingConfirmationModal from './FundingConfirmationModal';
-import type { FundingSourceItem } from './FundingConfirmationModal';
-import FundingProgressModal from './FundingProgressModal';
+import FundingConfirmationModal from '@/components/organization/funding/FundingConfirmationModal';
+import type { FundingSourceItem } from '@/components/organization/funding/FundingConfirmationModal';
+import FundingProgressModal from '@/components/organization/funding/FundingProgressModal';
import type { StepKey } from './constants';
import { isStepDataValid } from '@/lib/utils/hackathon-step-validation';
import { usePrizePoolCalculations } from '@/hooks/use-prize-pool-calculations';
import {
requestHackathonFundingOtp,
verifyHackathonFundingOtp,
+ useDraft,
type FundingMode,
+ type HackathonDraftAssumption,
} from '@/features/hackathons';
+import AiAssumptionsBanner from '@/components/ai/AiAssumptionsBanner';
+import AiBriefPanel from '@/components/ai/AiBriefPanel';
import { connectWallet } from '@/lib/wallet/wallet-kit';
import { useTreasuryWallets } from '@/features/treasury';
import { useQuery } from '@tanstack/react-query';
@@ -126,6 +130,22 @@ export default function NewHackathonTab({
router.replace(`${pathname}?${params.toString()}`, { scroll: false });
}, [draftId, initialDraftId, searchParams, pathname, router]);
+ // AI assumptions (from the draft response) surfaced on the review step so the
+ // organizer can see and correct every guess the AI made.
+ const { data: aiDraftRecord } = useDraft(derivedOrgId ?? '', draftId);
+ const aiGenerationRecord = (
+ aiDraftRecord as
+ | {
+ aiGeneration?: {
+ assumptions?: HackathonDraftAssumption[];
+ brief?: string | null;
+ };
+ }
+ | undefined
+ )?.aiGeneration;
+ const aiAssumptions = aiGenerationRecord?.assumptions ?? [];
+ const aiBrief = aiGenerationRecord?.brief ?? null;
+
// Define the callback after hooks are initialized
const onDraftLoaded = useCallback(
(formData: any, firstIncompleteStep: StepKey) => {
@@ -610,6 +630,14 @@ export default function NewHackathonTab({
+ {aiBrief && }
+ {aiAssumptions.length > 0 && (
+ handleEditTab(section as StepKey)}
+ className='mb-6'
+ />
+ )}
setProgressOpen(false)}
- onViewHackathon={handleViewHackathon}
+ onView={handleViewHackathon}
+ entityNoun='hackathon'
/>
>
)}
diff --git a/components/organization/hackathons/new/RegenerateSectionButton.tsx b/components/organization/hackathons/new/RegenerateSectionButton.tsx
index 078748d4..19a67d58 100644
--- a/components/organization/hackathons/new/RegenerateSectionButton.tsx
+++ b/components/organization/hackathons/new/RegenerateSectionButton.tsx
@@ -1,11 +1,8 @@
'use client';
import { useParams, useSearchParams } from 'next/navigation';
-import { Sparkles } from 'lucide-react';
-import { toast } from 'sonner';
-import { BoundlessButton } from '@/components/buttons';
-import { ApiError } from '@/lib/api';
+import AiRegenerateControl from '@/components/ai/AiRegenerateControl';
import {
useDraft,
useRegenerateDraftSection,
@@ -21,15 +18,14 @@ interface RegenerateSectionButtonProps {
}
/**
- * Self-contained "Regenerate with AI" button for a wizard step. Derives the org
- * + draft from the route, and renders only on AI-generated drafts (the server
- * owns the suggestion, surfaced via `draft.aiGeneration`). On success it hands
- * the regenerated section back to the tab to apply to its form.
+ * Per-section "Regenerate with AI" for the hackathon wizard. Derives the org +
+ * draft from the route, renders only on AI-generated drafts, and delegates the
+ * steerable + preview/accept-discard UX to the shared AiRegenerateControl.
*/
export default function RegenerateSectionButton({
section,
onApply,
- label = 'Regenerate with AI',
+ label,
}: RegenerateSectionButtonProps) {
const params = useParams();
const searchParams = useSearchParams();
@@ -40,41 +36,17 @@ export default function RegenerateSectionButton({
const { data: draft } = useDraft(organizationId, draftId || undefined);
const regenerate = useRegenerateDraftSection(organizationId);
- // Available only on AI-generated drafts.
- if (!draftId || !draft?.aiGeneration) return null;
-
- const handleClick = async () => {
- try {
- const result = await regenerate.mutateAsync({
- draftId,
- body: { section },
- });
- onApply(result.data as Record);
- toast.success('Section regenerated. Review the new values.');
- } catch (err) {
- if (err instanceof ApiError) {
- if (err.status === 503) {
- toast.error('The AI assistant is busy. Try again in a moment.');
- return;
- }
- toast.error(err.message || 'Could not regenerate this section.');
- return;
- }
- toast.error('Could not regenerate this section.');
- }
- };
-
return (
-
-
- {label}
-
+
+ regenerate
+ .mutateAsync({ draftId, body: { section, instructions } })
+ .then(r => r.data as Record)
+ }
+ onApply={onApply}
+ />
);
}
diff --git a/features/bounties/api/draft-ai-client.ts b/features/bounties/api/draft-ai-client.ts
new file mode 100644
index 00000000..2948fc05
--- /dev/null
+++ b/features/bounties/api/draft-ai-client.ts
@@ -0,0 +1,69 @@
+/**
+ * Organizer Assist (AI) bounty draft calls. Backs the React Query hooks in
+ * use-draft-ai.ts.
+ *
+ * These two endpoints are new (boundless-nestjs OrganizationBountiesAiController)
+ * and are not yet in the generated OpenAPI `paths`, so the call goes through a
+ * narrow loosely-typed view of `apiClient` until `npm run codegen` runs against
+ * the updated backend. `unwrapData` keeps the `ApiError` semantics the dialogs
+ * rely on (503 busy / 429 quota / field errors).
+ */
+import { apiClient, unwrapData } from '@/lib/api';
+
+import type {
+ ClarifyBountyDraftResult,
+ GenerateBountyDraftFromBriefBody,
+ GenerateBountyDraftFromBriefResponse,
+ RegenerateBountyDraftSectionBody,
+ RegenerateBountyDraftSectionResponse,
+} from '../types';
+
+/** Loose view of apiClient.POST for paths not yet in the generated schema. */
+type LooseApiClient = {
+ POST: (
+ path: string,
+ init: {
+ params: { path: Record };
+ body?: unknown;
+ }
+ ) => Promise<{ data?: unknown; error?: unknown; response: Response }>;
+};
+
+const loose = apiClient as unknown as LooseApiClient;
+
+/** Generate a full bounty draft from a brief; returns the created draft + meta. */
+export const generateBountyDraftFromBrief = async (
+ organizationId: string,
+ body: GenerateBountyDraftFromBriefBody
+): Promise =>
+ unwrapData(
+ await loose.POST(
+ '/api/organizations/{organizationId}/bounties/draft/from-brief',
+ { params: { path: { organizationId } }, body }
+ )
+ ) as GenerateBountyDraftFromBriefResponse;
+
+/** Adaptive clarify gate: triage a brief for follow-up questions before drafting. */
+export const clarifyBountyDraft = async (
+ organizationId: string,
+ brief: string
+): Promise =>
+ unwrapData(
+ await loose.POST(
+ '/api/organizations/{organizationId}/bounties/draft/clarify',
+ { params: { path: { organizationId } }, body: { brief } }
+ )
+ ) as ClarifyBountyDraftResult;
+
+/** Regenerate a single section (description | submission | reward) of a draft. */
+export const regenerateBountyDraftSection = async (
+ organizationId: string,
+ id: string,
+ body: RegenerateBountyDraftSectionBody
+): Promise =>
+ unwrapData(
+ await loose.POST(
+ '/api/organizations/{organizationId}/bounties/draft/{id}/regenerate-section',
+ { params: { path: { organizationId, id } }, body }
+ )
+ ) as RegenerateBountyDraftSectionResponse;
diff --git a/features/bounties/api/escrow-client.ts b/features/bounties/api/escrow-client.ts
index f5c1829a..ab52d832 100644
--- a/features/bounties/api/escrow-client.ts
+++ b/features/bounties/api/escrow-client.ts
@@ -20,12 +20,60 @@ import { apiClient, unwrapData } from '@/lib/api';
import type {
BountyEscrowOpResponse,
+ BountyFundingOtpRequestResult,
+ BountyFundingOtpVerifyResult,
CancelBountyEscrowRequest,
PublishBountyEscrowRequest,
SelectBountyWinnersRequest,
SubmitSignedXdrRequest,
} from '../types';
+/** Loose view of apiClient.POST for funding-otp paths not yet in the generated schema. */
+type LooseApiClient = {
+ POST: (
+ path: string,
+ init: { params: { path: Record }; body?: unknown }
+ ) => Promise<{ data?: unknown; error?: unknown; response: Response }>;
+};
+const looseApi = apiClient as unknown as LooseApiClient;
+
+/** Request a funding step-up code for a bounty (shared FundingOtpModule). */
+export const requestBountyFundingOtp = async (
+ organizationId: string,
+ bountyId: string
+): Promise =>
+ unwrapData(
+ await looseApi.POST(
+ '/api/organizations/{organizationId}/bounties/{id}/escrow/funding-otp/request',
+ { params: { path: { organizationId, id: bountyId } } }
+ )
+ ) as BountyFundingOtpRequestResult;
+
+/** Return a bounty stuck in draft_awaiting_funding back to draft (failed publish recovery). */
+export const resetBountyEscrowToDraft = async (
+ organizationId: string,
+ bountyId: string
+): Promise<{ id: string; status: string }> =>
+ unwrapData(
+ await looseApi.POST(
+ '/api/organizations/{organizationId}/bounties/{id}/escrow/reset-to-draft',
+ { params: { path: { organizationId, id: bountyId } } }
+ )
+ ) as { id: string; status: string };
+
+/** Verify a funding step-up code for a bounty. */
+export const verifyBountyFundingOtp = async (
+ organizationId: string,
+ bountyId: string,
+ code: string
+): Promise =>
+ unwrapData(
+ await looseApi.POST(
+ '/api/organizations/{organizationId}/bounties/{id}/escrow/funding-otp/verify',
+ { params: { path: { organizationId, id: bountyId } }, body: { code } }
+ )
+ ) as BountyFundingOtpVerifyResult;
+
/** Publish a bounty draft to the events contract (CREATE_EVENT). */
export const publishBountyEscrow = async (
organizationId: string,
diff --git a/features/bounties/api/use-draft-ai.ts b/features/bounties/api/use-draft-ai.ts
new file mode 100644
index 00000000..c032e664
--- /dev/null
+++ b/features/bounties/api/use-draft-ai.ts
@@ -0,0 +1,76 @@
+'use client';
+
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+
+import {
+ clarifyBountyDraft,
+ generateBountyDraftFromBrief,
+ regenerateBountyDraftSection,
+} from './draft-ai-client';
+import type {
+ ClarifyBountyDraftResult,
+ GenerateBountyDraftFromBriefBody,
+ GenerateBountyDraftFromBriefResponse,
+ RegenerateBountyDraftSectionBody,
+ RegenerateBountyDraftSectionResponse,
+} from '../types';
+import { bountyKeys } from './keys';
+
+/** Organizer Assist: adaptive clarify gate before drafting a bounty. */
+export function useClarifyBountyDraft(organizationId: string) {
+ return useMutation({
+ mutationFn: (brief: string): Promise =>
+ clarifyBountyDraft(organizationId, brief),
+ });
+}
+
+/**
+ * Organizer Assist: generate a bounty draft from a brief. The backend calls the
+ * AI service, creates + pre-fills a real draft (scope, mode, submission, reward),
+ * persists the server-owned suggestion, and returns the draft. We seed the draft
+ * cache so navigating into the wizard resumes it without a refetch flicker.
+ */
+export function useGenerateBountyDraftFromBrief(organizationId: string) {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: (
+ body: GenerateBountyDraftFromBriefBody
+ ): Promise =>
+ generateBountyDraftFromBrief(organizationId, body),
+ onSuccess: result => {
+ queryClient.setQueryData(
+ bountyKeys.draft(organizationId, result.draftId),
+ result.draft
+ );
+ queryClient.invalidateQueries({
+ queryKey: bountyKeys.drafts(organizationId),
+ });
+ },
+ });
+}
+
+/**
+ * Organizer Assist: regenerate one section of an AI-generated draft. The server
+ * owns the suggestion + generationId (persisted on the draft), so the client only
+ * names the section plus optional steering. The response carries the regenerated
+ * values already in the wizard section shape. The draft cache is invalidated so a
+ * resume reflects the persisted change.
+ */
+export function useRegenerateBountyDraftSection(organizationId: string) {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: ({
+ draftId,
+ body,
+ }: {
+ draftId: string;
+ body: RegenerateBountyDraftSectionBody;
+ }): Promise =>
+ regenerateBountyDraftSection(organizationId, draftId, body),
+ onSuccess: (_result, { draftId }) => {
+ queryClient.invalidateQueries({
+ queryKey: bountyKeys.draft(organizationId, draftId),
+ });
+ },
+ });
+}
diff --git a/features/bounties/index.ts b/features/bounties/index.ts
index 39cffa9e..99c3afb0 100644
--- a/features/bounties/index.ts
+++ b/features/bounties/index.ts
@@ -61,6 +61,34 @@ export {
useDeleteDraft,
} from './api/use-draft';
+// Organizer Assist (AI drafting) — types, client, hooks.
+export type {
+ BountyDraftAiGeneration,
+ BountyDraftAssumption,
+ BountyDraftGeneratedMode,
+ BountyDraftWithAi,
+ BountyAiGenerationMeta,
+ GenerateBountyDraftFromBriefBody,
+ GenerateBountyDraftFromBriefResponse,
+ BountyDraftRegenSection,
+ RegenerateBountyDraftSectionBody,
+ RegenerateBountyDraftSectionResponse,
+ ClarifyOption,
+ ClarifyQuestion,
+ ClarifyBountyDraftResult,
+} from './types';
+export { BOUNTY_DRAFT_REGEN_SECTIONS } from './types';
+export {
+ clarifyBountyDraft,
+ generateBountyDraftFromBrief,
+ regenerateBountyDraftSection,
+} from './api/draft-ai-client';
+export {
+ useClarifyBountyDraft,
+ useGenerateBountyDraftFromBrief,
+ useRegenerateBountyDraftSection,
+} from './api/use-draft-ai';
+
// Escrow client (typed openapi-fetch).
export {
publishBountyEscrow,
@@ -68,7 +96,14 @@ export {
selectBountyWinners,
submitSignedBountyEscrow,
getBountyEscrowOp,
+ requestBountyFundingOtp,
+ verifyBountyFundingOtp,
+ resetBountyEscrowToDraft,
} from './api/escrow-client';
+export type {
+ BountyFundingOtpRequestResult,
+ BountyFundingOtpVerifyResult,
+} from './types';
// Escrow hooks (React Query: polling primitive, mutation wrappers, op runner).
export {
diff --git a/features/bounties/types.ts b/features/bounties/types.ts
index d24e57c1..9d3a1369 100644
--- a/features/bounties/types.ts
+++ b/features/bounties/types.ts
@@ -43,6 +43,95 @@ export const DRAFT_SECTIONS = [
] as const;
export type DraftSection = (typeof DRAFT_SECTIONS)[number];
+// ── Organizer Assist (AI drafting) ────────────────────────────────────────────
+// Hand-typed until `npm run codegen` surfaces the new bounty AI DTOs +
+// the `aiGeneration` field on BountyDraftResponseDto. Mirrors the backend
+// bounty-ai-assist.dto.ts shapes exactly.
+
+/** A non-obvious choice the AI made, surfaced for organizer review. */
+export interface BountyDraftAssumption {
+ section: string;
+ field: string;
+ note: string;
+}
+
+/** The mode the AI generated for, to detect a later mode change. */
+export interface BountyDraftGeneratedMode {
+ entryType: BountyEntryType;
+ claimType: BountyClaimType;
+}
+
+/** AI provenance — present when the draft was generated with Organizer Assist. */
+export interface BountyDraftAiGeneration {
+ generationId: string;
+ assumptions?: BountyDraftAssumption[];
+ brief?: string | null;
+ generatedMode?: BountyDraftGeneratedMode | null;
+}
+
+/** Pre-draft clarify gate. */
+export interface ClarifyOption {
+ value: string;
+ label: string;
+}
+export interface ClarifyQuestion {
+ id: string;
+ question: string;
+ options: ClarifyOption[];
+}
+export interface ClarifyBountyDraftResult {
+ ready: boolean;
+ questions: ClarifyQuestion[];
+}
+
+/** A draft that may carry AI provenance (until codegen folds it into BountyDraft). */
+export type BountyDraftWithAi = BountyDraft & {
+ aiGeneration?: BountyDraftAiGeneration;
+};
+
+/** Cost/trace metadata returned with every Organizer Assist response. */
+export interface BountyAiGenerationMeta {
+ generationId: string;
+ model: string;
+ promptVersion: string;
+ costUsd: string;
+}
+
+/** Body for POST …/bounties/draft/from-brief. */
+export interface GenerateBountyDraftFromBriefBody {
+ brief: string;
+ budgetCapUsdc: string;
+ earliestStart: string;
+ examples?: string[];
+}
+
+export interface GenerateBountyDraftFromBriefResponse {
+ draftId: string;
+ draft: BountyDraft;
+ generation: BountyAiGenerationMeta;
+}
+
+/** Sections the AI can regenerate (mirrors backend BOUNTY_DRAFT_REGEN_SECTIONS). */
+export const BOUNTY_DRAFT_REGEN_SECTIONS = [
+ 'description',
+ 'submission',
+ 'reward',
+] as const;
+export type BountyDraftRegenSection =
+ (typeof BOUNTY_DRAFT_REGEN_SECTIONS)[number];
+
+export interface RegenerateBountyDraftSectionBody {
+ section: BountyDraftRegenSection;
+ instructions?: string;
+}
+
+export interface RegenerateBountyDraftSectionResponse {
+ section: BountyDraftRegenSection;
+ /** Regenerated values in the wizard section shape, ready to apply. */
+ data: Record;
+ generation: BountyAiGenerationMeta;
+}
+
// ── Escrow ───────────────────────────────────────────────────────────────────
/** The EscrowOp row returned by every bounty escrow endpoint. */
@@ -65,6 +154,19 @@ export type FundingMode = NonNullable<
PublishBountyEscrowRequest['fundingMode']
>;
+// Funding step-up OTP (shared FundingOtpModule). Hand-typed until codegen
+// surfaces the new bounty funding-otp endpoints.
+export interface BountyFundingOtpRequestResult {
+ required: boolean;
+ alreadyVerified: boolean;
+ sent: boolean;
+ expiresInSeconds: number;
+}
+export interface BountyFundingOtpVerifyResult {
+ verified: boolean;
+ expiresInSeconds: number;
+}
+
/** Terminal op states. Polling should stop once one is reached. */
export const TERMINAL_ESCROW_STATUSES: readonly EscrowOpStatus[] = [
'COMPLETED',
diff --git a/features/hackathons/api/use-clarify.ts b/features/hackathons/api/use-clarify.ts
new file mode 100644
index 00000000..c8bf143d
--- /dev/null
+++ b/features/hackathons/api/use-clarify.ts
@@ -0,0 +1,34 @@
+'use client';
+
+import { useMutation } from '@tanstack/react-query';
+
+import { apiClient, unwrapData } from '@/lib/api';
+
+import type { ClarifyHackathonDraftResult } from '../types';
+
+/**
+ * Adaptive clarify gate for the hackathon generate dialog. New endpoint
+ * (OrganizationHackathonsAiController `draft/clarify`) not yet in the generated
+ * `paths`, so it goes through a narrow loosely-typed view of `apiClient` until
+ * `npm run codegen` runs against the updated backend.
+ */
+type LooseApiClient = {
+ POST: (
+ path: string,
+ init: { params: { path: Record }; body?: unknown }
+ ) => Promise<{ data?: unknown; error?: unknown; response: Response }>;
+};
+
+const loose = apiClient as unknown as LooseApiClient;
+
+export function useClarifyDraft(organizationId: string) {
+ return useMutation({
+ mutationFn: async (brief: string): Promise =>
+ unwrapData(
+ await loose.POST(
+ '/api/organizations/{organizationId}/hackathons/draft/clarify',
+ { params: { path: { organizationId } }, body: { brief } }
+ )
+ ) as ClarifyHackathonDraftResult,
+ });
+}
diff --git a/features/hackathons/index.ts b/features/hackathons/index.ts
index 211da5c1..c6bdcae2 100644
--- a/features/hackathons/index.ts
+++ b/features/hackathons/index.ts
@@ -58,8 +58,15 @@ export {
// Organizer Assist (AI) hooks.
export { useGenerateDraftFromBrief } from './api/use-generate-from-brief';
export { useRegenerateDraftSection } from './api/use-regenerate-section';
+export { useClarifyDraft } from './api/use-clarify';
export { useBriefTemplates } from './api/use-brief-templates';
export type { BriefTemplate } from './api/use-brief-templates';
+export type {
+ HackathonDraftAssumption,
+ HackathonClarifyOption,
+ HackathonClarifyQuestion,
+ ClarifyHackathonDraftResult,
+} from './types';
// Draft client (imperative helpers, legacy-compatible signatures).
export { deleteDraft, previewAnnouncementAudience } from './api/draft-client';
diff --git a/features/hackathons/types.ts b/features/hackathons/types.ts
index f0005046..e5153bb5 100644
--- a/features/hackathons/types.ts
+++ b/features/hackathons/types.ts
@@ -62,6 +62,30 @@ export type AiGenerationMeta = Schemas['AiGenerationMetaDto'];
/** The sections the AI can regenerate (criteria | prizes | tracks | timeline | description). */
export type DraftRegenSection = RegenerateDraftSectionBody['section'];
+// Hand-typed until `npm run codegen` surfaces the clarify DTOs + the
+// `aiGeneration.assumptions` field on the draft response.
+
+/** A non-obvious choice the AI made, surfaced for organizer review. */
+export interface HackathonDraftAssumption {
+ section: string;
+ field: string;
+ note: string;
+}
+
+export interface HackathonClarifyOption {
+ value: string;
+ label: string;
+}
+export interface HackathonClarifyQuestion {
+ id: string;
+ question: string;
+ options: HackathonClarifyOption[];
+}
+export interface ClarifyHackathonDraftResult {
+ ready: boolean;
+ questions: HackathonClarifyQuestion[];
+}
+
// ── Escrow ─────────────────────────────────────────────────────────────────
/** The EscrowOp row returned by every hackathon escrow endpoint. */
diff --git a/hooks/use-ai-usage.ts b/hooks/use-ai-usage.ts
new file mode 100644
index 00000000..3556fdd2
--- /dev/null
+++ b/hooks/use-ai-usage.ts
@@ -0,0 +1,39 @@
+'use client';
+
+import { useQuery } from '@tanstack/react-query';
+
+import { apiClient, unwrapData } from '@/lib/api';
+
+export interface AiUsage {
+ tier: string;
+ limit: number | null;
+ used: number;
+ remaining: number | null;
+ resetAt: string;
+ costUsdThisMonth: string;
+}
+
+/** Loose view of apiClient.GET for the usage path (not yet in generated schema). */
+type LooseApiClient = {
+ GET: (
+ path: string,
+ init: { params: { path: Record } }
+ ) => Promise<{ data?: unknown; error?: unknown; response: Response }>;
+};
+
+const loose = apiClient as unknown as LooseApiClient;
+
+/** The org's monthly AI usage (calls used/remaining + spend). */
+export function useAiUsage(organizationId: string) {
+ return useQuery({
+ queryKey: ['ai-usage', organizationId],
+ enabled: Boolean(organizationId),
+ staleTime: 30_000,
+ queryFn: async (): Promise =>
+ unwrapData(
+ await loose.GET('/api/organizations/{organizationId}/ai/usage', {
+ params: { path: { organizationId } },
+ })
+ ) as AiUsage,
+ });
+}
diff --git a/hooks/use-bounty-publish.ts b/hooks/use-bounty-publish.ts
index 730d1c42..8674ed57 100644
--- a/hooks/use-bounty-publish.ts
+++ b/hooks/use-bounty-publish.ts
@@ -11,6 +11,7 @@ import {
} from '@/lib/utils/publish-op-storage';
import {
getBountyEscrowOp,
+ resetBountyEscrowToDraft,
useEscrowOpRunner,
usePublishBountyEscrow,
type FundingMode,
@@ -35,6 +36,13 @@ interface UseBountyPublishProps {
* Wallets Kit). Becomes the on-chain owner. Ignored for MANAGED.
*/
externalOwnerAddress?: string | null;
+ /**
+ * For MANAGED funding from an organization treasury wallet: the treasury
+ * wallet to fund + sign with (backend signs custodially). When set, its
+ * address becomes the on-chain owner and we forward its id as sourceWalletId.
+ * Omit to fund from the caller's personal managed wallet.
+ */
+ treasurySource?: { walletId: string; address: string } | null;
}
export interface BountyPublishResponse {
@@ -64,8 +72,8 @@ const PUBLISHED_STATUSES = new Set([
* 2. Poll the op to COMPLETED; the backend subscriber flips the bounty
* draft_awaiting_funding -> open on settle.
*
- * Mirrors useHackathonPublish. Treasury funding (sourceWalletId) is omitted
- * until the backend treasury-parity issue (boundless-nestjs #314) lands.
+ * Mirrors useHackathonPublish, including managed-treasury and external-wallet
+ * funding sources (the funding modal picks one).
*/
export const useBountyPublish = ({
organizationId,
@@ -73,16 +81,21 @@ export const useBountyPublish = ({
draftId,
fundingMode = 'MANAGED',
externalOwnerAddress,
+ treasurySource,
}: UseBountyPublishProps) => {
const { walletAddress, balances } = useWalletContext();
const bountyId = draftId || '';
const isExternal = fundingMode === 'EXTERNAL';
+ const usingTreasury = !isExternal && !!treasurySource;
- // The on-chain owner/source. EXTERNAL funds from the connected wallet;
- // MANAGED from the caller's platform-held custodial wallet.
+ // The on-chain owner/source. EXTERNAL funds from the connected wallet; a
+ // treasury source funds from the org treasury wallet; otherwise the caller's
+ // personal platform-held custodial wallet.
const ownerAddress = isExternal
? (externalOwnerAddress ?? null)
- : walletAddress;
+ : usingTreasury
+ ? treasurySource!.address
+ : walletAddress;
const publishMutation = usePublishBountyEscrow(organizationId, bountyId);
const runner = useEscrowOpRunner(
@@ -162,21 +175,33 @@ export const useBountyPublish = ({
if (currentStatus === 'draft_awaiting_funding') {
const storedOpRowId = readPublishOpId(bountyId);
- if (!storedOpRowId) {
+ if (storedOpRowId) {
+ // A publish op is in flight locally — resume polling it rather than
+ // starting a second (double-funding) publish.
+ finalizedRef.current = false;
+ setHasStarted(true);
+ toast.info('Resuming bounty funding…');
+ await runner.run(() =>
+ getBountyEscrowOp(organizationId, bountyId, storedOpRowId)
+ );
+ return null;
+ }
+ // No resumable op locally: the publish stranded the bounty (e.g. the
+ // managed sign/submit failed before settling). Try to return it to draft
+ // so we can republish. The backend refuses if the op might still settle.
+ try {
+ await resetBountyEscrowToDraft(organizationId, bountyId);
+ clearPublishOpId(bountyId);
+ toast.info('Returned to draft — retrying publish…');
+ // Status is now draft; fall through to the normal publish path below.
+ } catch {
toast.info(
- 'This bounty is already being funded on-chain. It will finish ' +
+ 'This bounty is still being funded on-chain. It will finish ' +
'publishing shortly, or return to draft if the transaction fails — ' +
'refresh in a moment to check.'
);
return null;
}
- finalizedRef.current = false;
- setHasStarted(true);
- toast.info('Resuming bounty funding…');
- await runner.run(() =>
- getBountyEscrowOp(organizationId, bountyId, storedOpRowId)
- );
- return null;
}
// ---- Normal draft publish path ----
@@ -205,7 +230,12 @@ export const useBountyPublish = ({
const usdcBalance = parseFloat(usdcEntry?.balance ?? '0');
const fmt = (n: number) =>
n.toLocaleString('en-US', { maximumFractionDigits: 2 });
- if (!isExternal && balances.length > 0 && usdcBalance < required) {
+ if (
+ !isExternal &&
+ !usingTreasury &&
+ balances.length > 0 &&
+ usdcBalance < required
+ ) {
toast.error(
usdcEntry
? `Insufficient USDC. Publishing locks ${fmt(required)} USDC ` +
@@ -239,9 +269,9 @@ export const useBountyPublish = ({
tokenAddress,
budget: String(budget),
submissionDeadline,
- applicationCreditCost: submission.applicationCreditCost ?? 1,
winnerDistribution: buildBountyWinnerDistribution(reward),
fundingMode,
+ ...(usingTreasury ? { sourceWalletId: treasurySource!.walletId } : {}),
};
finalizedRef.current = false;
diff --git a/lib/ai/stream-draft.ts b/lib/ai/stream-draft.ts
new file mode 100644
index 00000000..cb0cd8ad
--- /dev/null
+++ b/lib/ai/stream-draft.ts
@@ -0,0 +1,113 @@
+/**
+ * Streaming client for the Organizer Assist `draft/from-brief/stream` endpoints.
+ * The backend proxies the AI's SSE: `partial` frames carry the draft taking
+ * shape, a final `done` frame carries { draftId, draft }. EventSource can't POST,
+ * so this reads the fetch body stream directly. Mirrors the openapi client's
+ * absolute backend origin + cookie auth (`credentials: 'include'`).
+ */
+
+const backendOrigin = (
+ process.env.NEXT_PUBLIC_API_URL || 'https://staging-api.boundlessfi.xyz'
+)
+ .replace(/\/$/, '')
+ .replace(/\/api$/i, '');
+
+export interface StreamDraftHandlers {
+ /** A growing partial suggestion snapshot for the live reveal. */
+ onPartial: (suggestion: Record) => void;
+ /** The created draft. */
+ onDone: (data: { draftId: string; draft?: unknown }) => void;
+ /** Pre-stream failure (status set, e.g. 429 quota) or mid-stream error. */
+ onError: (err: { status?: number; message: string }) => void;
+}
+
+function parseFrame(raw: string): { event: string; data: unknown } | null {
+ let event = 'message';
+ const dataLines: string[] = [];
+ for (const line of raw.split('\n')) {
+ if (line.startsWith('event:')) event = line.slice(6).trim();
+ else if (line.startsWith('data:')) dataLines.push(line.slice(5).trim());
+ }
+ if (dataLines.length === 0) return null;
+ const payload = dataLines.join('\n');
+ try {
+ return { event, data: JSON.parse(payload) };
+ } catch {
+ return { event, data: payload };
+ }
+}
+
+export async function streamDraft(
+ path: string,
+ body: unknown,
+ signal: AbortSignal,
+ handlers: StreamDraftHandlers
+): Promise {
+ let res: Response;
+ try {
+ res = await fetch(`${backendOrigin}${path}`, {
+ method: 'POST',
+ credentials: 'include',
+ headers: { 'content-type': 'application/json' },
+ body: JSON.stringify(body),
+ signal,
+ });
+ } catch (err) {
+ if ((err as Error)?.name === 'AbortError') return;
+ handlers.onError({ message: 'Could not reach the AI service.' });
+ return;
+ }
+
+ // Pre-stream failure: the backend returns a normal JSON error (quota, etc.).
+ if (!res.ok) {
+ let message = 'Could not generate a draft. Please try again.';
+ try {
+ const j = (await res.json()) as { message?: string };
+ if (j?.message) message = j.message;
+ } catch {
+ /* keep default */
+ }
+ handlers.onError({ status: res.status, message });
+ return;
+ }
+
+ if (!res.body) {
+ handlers.onError({ message: 'The AI service returned no stream.' });
+ return;
+ }
+
+ const reader = res.body.getReader();
+ const decoder = new TextDecoder();
+ let buffer = '';
+ try {
+ for (;;) {
+ const { done, value } = await reader.read();
+ if (done) break;
+ buffer += decoder.decode(value, { stream: true });
+ let idx: number;
+ while ((idx = buffer.indexOf('\n\n')) !== -1) {
+ const frame = parseFrame(buffer.slice(0, idx));
+ buffer = buffer.slice(idx + 2);
+ if (!frame) continue;
+ if (frame.event === 'partial') {
+ const s = (frame.data as { suggestion?: Record })
+ ?.suggestion;
+ if (s) handlers.onPartial(s);
+ } else if (frame.event === 'done') {
+ handlers.onDone(frame.data as { draftId: string; draft?: unknown });
+ return;
+ } else if (frame.event === 'error') {
+ handlers.onError({
+ message:
+ (frame.data as { message?: string })?.message ??
+ 'Draft generation failed.',
+ });
+ return;
+ }
+ }
+ }
+ } catch (err) {
+ if ((err as Error)?.name === 'AbortError') return;
+ handlers.onError({ message: 'The draft stream was interrupted.' });
+ }
+}
diff --git a/lib/api/generated/schema.d.ts b/lib/api/generated/schema.d.ts
index a38c86fa..42fa8116 100644
--- a/lib/api/generated/schema.d.ts
+++ b/lib/api/generated/schema.d.ts
@@ -256,16 +256,17 @@ export interface paths {
patch?: never;
trace?: never;
};
- '/api/users/me': {
+ '/api/users/profile': {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- /** Get current user dashboard with overview, chart, and activities graph */
- get: operations['UserController_getProfile'];
- put?: never;
+ /** Get current user profile */
+ get: operations['ProfileController_getProfile'];
+ /** Update current user profile */
+ put: operations['ProfileController_updateProfile'];
post?: never;
delete?: never;
options?: never;
@@ -273,15 +274,15 @@ export interface paths {
patch?: never;
trace?: never;
};
- '/api/users/public': {
+ '/api/users/profile/stats': {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- /** Public test endpoint */
- get: operations['UserController_getPublic'];
+ /** Get user profile statistics */
+ get: operations['ProfileController_getProfileStats'];
put?: never;
post?: never;
delete?: never;
@@ -290,15 +291,15 @@ export interface paths {
patch?: never;
trace?: never;
};
- '/api/users/optional': {
+ '/api/users/profile/activity': {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- /** Optional authentication test endpoint */
- get: operations['UserController_getOptional'];
+ /** Get user activity */
+ get: operations['ProfileController_getActivity'];
put?: never;
post?: never;
delete?: never;
@@ -307,38 +308,32 @@ export interface paths {
patch?: never;
trace?: never;
};
- '/api/users/{username}': {
+ '/api/users/profile/avatar': {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- /**
- * Get user profile by username
- * @description Get a user profile by their username. Accessible to anyone without authentication.
- */
- get: operations['UserController_getUserByUsername'];
+ get?: never;
put?: never;
- post?: never;
+ /** Upload user avatar */
+ post: operations['ProfileController_uploadAvatar'];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- '/api/users/{username}/followers': {
+ '/api/users/preferences': {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- /**
- * Get user followers
- * @description Get a list of users who follow this profile
- */
- get: operations['UserController_getUserFollowers'];
+ /** Get user preferences */
+ get: operations['PreferencesController_getPreferences'];
put?: never;
post?: never;
delete?: never;
@@ -347,19 +342,16 @@ export interface paths {
patch?: never;
trace?: never;
};
- '/api/users/{username}/following': {
+ '/api/users/preferences/language': {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- /**
- * Get users followed by this profile
- * @description Get a list of entities (users, projects, organizations) followed by this user
- */
- get: operations['UserController_getUserFollowing'];
- put?: never;
+ get?: never;
+ /** Update language preference */
+ put: operations['PreferencesController_updateLanguage'];
post?: never;
delete?: never;
options?: never;
@@ -367,17 +359,16 @@ export interface paths {
patch?: never;
trace?: never;
};
- '/api/users/profile': {
+ '/api/users/preferences/timezone': {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- /** Get current user profile */
- get: operations['ProfileController_getProfile'];
- /** Update current user profile */
- put: operations['ProfileController_updateProfile'];
+ get?: never;
+ /** Update timezone preference */
+ put: operations['PreferencesController_updateTimezone'];
post?: never;
delete?: never;
options?: never;
@@ -385,15 +376,49 @@ export interface paths {
patch?: never;
trace?: never;
};
- '/api/users/profile/stats': {
+ '/api/users/preferences/categories': {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- /** Get user profile statistics */
- get: operations['ProfileController_getProfileStats'];
+ get?: never;
+ /** Update category preferences */
+ put: operations['PreferencesController_updateCategoryPreferences'];
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/users/preferences/skills': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ /** Update skill preferences */
+ put: operations['PreferencesController_updateSkillPreferences'];
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/users/me': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** Get the current user (lean identity payload) */
+ get: operations['UserController_getMe'];
put?: never;
post?: never;
delete?: never;
@@ -402,15 +427,15 @@ export interface paths {
patch?: never;
trace?: never;
};
- '/api/users/profile/activity': {
+ '/api/users/dashboard': {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- /** Get user activity */
- get: operations['ProfileController_getActivity'];
+ /** Get the current user dashboard: stats, chart, activities graph, and recent activities */
+ get: operations['UserController_getDashboard'];
put?: never;
post?: never;
delete?: never;
@@ -419,7 +444,7 @@ export interface paths {
patch?: never;
trace?: never;
};
- '/api/users/profile/avatar': {
+ '/api/users/onboarding': {
parameters: {
query?: never;
header?: never;
@@ -428,23 +453,23 @@ export interface paths {
};
get?: never;
put?: never;
- /** Upload user avatar */
- post: operations['ProfileController_uploadAvatar'];
+ /** Complete onboarding (persona, referral source, skills, goals) */
+ post: operations['UserController_completeOnboarding'];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- '/api/users/preferences': {
+ '/api/users/public': {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- /** Get user preferences */
- get: operations['PreferencesController_getPreferences'];
+ /** Public test endpoint */
+ get: operations['UserController_getPublic'];
put?: never;
post?: never;
delete?: never;
@@ -453,16 +478,16 @@ export interface paths {
patch?: never;
trace?: never;
};
- '/api/users/preferences/language': {
+ '/api/users/optional': {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- get?: never;
- /** Update language preference */
- put: operations['PreferencesController_updateLanguage'];
+ /** Optional authentication test endpoint */
+ get: operations['UserController_getOptional'];
+ put?: never;
post?: never;
delete?: never;
options?: never;
@@ -470,16 +495,19 @@ export interface paths {
patch?: never;
trace?: never;
};
- '/api/users/preferences/timezone': {
+ '/api/users/{username}': {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- get?: never;
- /** Update timezone preference */
- put: operations['PreferencesController_updateTimezone'];
+ /**
+ * Get user profile by username
+ * @description Get a user profile by their username. Accessible to anyone without authentication.
+ */
+ get: operations['UserController_getUserByUsername'];
+ put?: never;
post?: never;
delete?: never;
options?: never;
@@ -487,16 +515,19 @@ export interface paths {
patch?: never;
trace?: never;
};
- '/api/users/preferences/categories': {
+ '/api/users/{username}/followers': {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- get?: never;
- /** Update category preferences */
- put: operations['PreferencesController_updateCategoryPreferences'];
+ /**
+ * Get user followers
+ * @description Get a list of users who follow this profile
+ */
+ get: operations['UserController_getUserFollowers'];
+ put?: never;
post?: never;
delete?: never;
options?: never;
@@ -504,16 +535,19 @@ export interface paths {
patch?: never;
trace?: never;
};
- '/api/users/preferences/skills': {
+ '/api/users/{username}/following': {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- get?: never;
- /** Update skill preferences */
- put: operations['PreferencesController_updateSkillPreferences'];
+ /**
+ * Get users followed by this profile
+ * @description Get a list of entities (users, projects, organizations) followed by this user
+ */
+ get: operations['UserController_getUserFollowing'];
+ put?: never;
post?: never;
delete?: never;
options?: never;
@@ -1153,6 +1187,40 @@ export interface paths {
patch: operations['MessagesController_markConversationRead'];
trace?: never;
};
+ '/api/credits/me': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** Current user credit balance, tier and next refill */
+ get: operations['CreditsController_getMine'];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/credits/history': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** Paginated credit ledger for the current user */
+ get: operations['CreditsController_getHistory'];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
'/api/crowdfunding/validate': {
parameters: {
query?: never;
@@ -1842,6 +1910,23 @@ export interface paths {
patch?: never;
trace?: never;
};
+ '/api/wallet/summary': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** Total USD balance across the user wallet assets (nav chip) */
+ get: operations['WalletController_getWalletSummary'];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
'/api/wallet/balance/{address}': {
parameters: {
query?: never;
@@ -2010,26 +2095,6 @@ export interface paths {
patch?: never;
trace?: never;
};
- '/api/wallet/payout': {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- get?: never;
- put?: never;
- /**
- * Send payout from platform (Admin only)
- * @description Sends funds from the Boundless platform wallet to a destination. Validates: destination activated, trustline for asset, memo when required, platform balance. Idempotent when idempotencyKey is provided.
- */
- post: operations['WalletPayoutController_sendPayout'];
- delete?: never;
- options?: never;
- head?: never;
- patch?: never;
- trace?: never;
- };
'/api/api/comments': {
parameters: {
query?: never;
@@ -3072,6 +3137,26 @@ export interface paths {
patch?: never;
trace?: never;
};
+ '/api/organizations/{organizationId}/hackathons/draft/clarify': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Triage a hackathon brief for clarifying questions (Organizer Assist)
+ * @description A cheap pre-draft gate: returns { ready: true } when the brief is specific enough, or 1-3 clarifying questions (duration / structure / participation) the organizer answers before drafting.
+ */
+ post: operations['OrganizationHackathonsAiController_clarify'];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
'/api/organizations/{organizationId}/hackathons/draft/from-brief': {
parameters: {
query?: never;
@@ -3092,6 +3177,26 @@ export interface paths {
patch?: never;
trace?: never;
};
+ '/api/organizations/{organizationId}/hackathons/draft/from-brief/stream': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Generate a hackathon draft from a brief, streaming (Organizer Assist)
+ * @description Server-Sent Events: `partial` frames carry the draft taking shape for a live reveal, then a `done` frame carries { draftId, draft }. Errors arrive as an `error` frame (or a normal 4xx before the stream opens, e.g. quota).
+ */
+ post: operations['OrganizationHackathonsAiController_generateFromBriefStream'];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
'/api/organizations/{organizationId}/hackathons/draft/{id}/regenerate-section': {
parameters: {
query?: never;
@@ -7184,6 +7289,23 @@ export interface paths {
patch?: never;
trace?: never;
};
+ '/api/organizations/{organizationId}/ai/usage': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** Get this organization’s monthly AI usage + cost */
+ get: operations['AiUsageController_getUsage'];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
'/api/admin/ops/pause': {
parameters: {
query?: never;
@@ -8246,6 +8368,159 @@ export interface paths {
patch?: never;
trace?: never;
};
+ '/api/admin/v2/contract-governance/tokens': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** List whitelisted tokens with live on-chain status */
+ get: operations['GovernanceContractController_listTokens'];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/admin/v2/contract-governance/tokens/sync': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /** Sync the whitelist from contract state (authoritative enumeration, any age); falls back to an event rescan on pre-enumeration contracts. */
+ post: operations['GovernanceContractController_syncTokens'];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/admin/v2/contract-governance/tokens/import': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /** Import a token already whitelisted on-chain into the portal (verified via is_supported_token; no signing). For recovering older tokens. */
+ post: operations['GovernanceContractController_importToken'];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/admin/v2/contract-governance/pause-state': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** Live pause flag for the events contract */
+ get: operations['GovernanceContractController_pauseState'];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/admin/v2/contract-governance/tokens/build-xdr': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /** Build the unsigned register_supported_token transaction. Tier 3: step-up. */
+ post: operations['GovernanceContractController_buildRegisterToken'];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/admin/v2/contract-governance/tokens/deregister/build-xdr': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /** Build the unsigned deregister_supported_token transaction. Tier 3: step-up. */
+ post: operations['GovernanceContractController_buildDeregisterToken'];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/admin/v2/contract-governance/pause/build-xdr': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /** Build the unsigned pause transaction. Tier 3: step-up. */
+ post: operations['GovernanceContractController_buildPause'];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/admin/v2/contract-governance/unpause/build-xdr': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /** Build the unsigned unpause transaction. Tier 3: step-up. */
+ post: operations['GovernanceContractController_buildUnpause'];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/admin/v2/contract-governance/ops/{id}/submit-signed': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /** Submit the admin-signed transaction for a built op (verified against the built XDR). Tier 3: step-up. */
+ post: operations['GovernanceContractController_submitSigned'];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
'/api/admin/v2/kyc': {
parameters: {
query?: never;
@@ -8450,6 +8725,218 @@ export interface paths {
patch?: never;
trace?: never;
};
+ '/api/admin/v2/hackathon-brief-templates': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** List all hackathon brief templates (including inactive) */
+ get: operations['HackathonBriefTemplatesController_list'];
+ put?: never;
+ /** Create a hackathon brief template (Tier 1) */
+ post: operations['HackathonBriefTemplatesController_create'];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/admin/v2/hackathon-brief-templates/{id}': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ /** Update a hackathon brief template (Tier 1) */
+ put: operations['HackathonBriefTemplatesController_update'];
+ post?: never;
+ /** Archive a hackathon brief template (soft-delete) */
+ delete: operations['HackathonBriefTemplatesController_archive'];
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/hackathon-brief-templates': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** List active hackathon brief templates */
+ get: operations['HackathonBriefTemplatesPublicController_list'];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/admin/v2/marketing/templates': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** List all marketing templates */
+ get: operations['MarketingTemplatesController_list'];
+ put?: never;
+ /** Create a marketing template (Tier 1) */
+ post: operations['MarketingTemplatesController_create'];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/admin/v2/marketing/templates/{id}': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ /** Update a marketing template (Tier 1) */
+ put: operations['MarketingTemplatesController_update'];
+ post?: never;
+ /** Archive a marketing template (soft-delete) */
+ delete: operations['MarketingTemplatesController_archive'];
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/admin/v2/marketing/campaigns': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** List all marketing campaigns */
+ get: operations['MarketingCampaignsController_list'];
+ put?: never;
+ /** Create a campaign draft (Tier 1) */
+ post: operations['MarketingCampaignsController_create'];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/admin/v2/marketing/campaigns/{id}': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ /** Update a campaign draft (Tier 1) */
+ put: operations['MarketingCampaignsController_update'];
+ post?: never;
+ /** Cancel a campaign (soft-cancel) */
+ delete: operations['MarketingCampaignsController_cancel'];
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/admin/v2/marketing/campaigns/audience-size': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /** Preview audience count for a filter */
+ post: operations['MarketingCampaignsController_audienceSize'];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/admin/v2/marketing/campaigns/{id}/send': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /** Send a campaign to its audience (Tier 2 step-up) */
+ post: operations['MarketingCampaignsController_send'];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/admin/v2/marketing/automations': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** List all marketing automations */
+ get: operations['MarketingAutomationsController_list'];
+ put?: never;
+ /** Create a marketing automation (Tier 1) */
+ post: operations['MarketingAutomationsController_create'];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/admin/v2/marketing/automations/{id}': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ /** Update a marketing automation (Tier 1) */
+ put: operations['MarketingAutomationsController_update'];
+ post?: never;
+ /** Delete an automation (hard delete — no audit trail loss) */
+ delete: operations['MarketingAutomationsController_remove'];
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/admin/v2/marketing/automations/{id}/toggle': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ /** Enable or disable an automation */
+ patch: operations['MarketingAutomationsController_toggle'];
+ trace?: never;
+ };
'/api/prices': {
parameters: {
query?: never;
@@ -8769,6 +9256,140 @@ export interface paths {
patch?: never;
trace?: never;
};
+ '/api/organizations/{organizationId}/bounties/draft/clarify': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Triage a bounty brief for clarifying questions (Organizer Assist)
+ * @description A cheap pre-draft gate: returns { ready: true } when the brief is specific enough, or 1-3 clarifying questions (mode / winners / deadline) the organizer answers before drafting.
+ */
+ post: operations['OrganizationBountiesAiController_clarify'];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/organizations/{organizationId}/bounties/draft/from-brief': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Generate a bounty draft from a brief (Organizer Assist)
+ * @description Calls the AI service to turn a brief into a structured draft, persists a new bounty draft pre-filled with scope, mode, submission settings, and prize tiers, and returns it together with cost metadata. The organizer reviews, edits, and publishes.
+ */
+ post: operations['OrganizationBountiesAiController_generateFromBrief'];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/organizations/{organizationId}/bounties/draft/from-brief/stream': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Generate a bounty draft from a brief, streaming (Organizer Assist)
+ * @description Server-Sent Events: `partial` frames carry the draft taking shape for a live reveal, then a `done` frame carries { draftId, draft }. Errors arrive as an `error` frame (or a normal 4xx before the stream opens, e.g. quota).
+ */
+ post: operations['OrganizationBountiesAiController_generateFromBriefStream'];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/organizations/{organizationId}/bounties/draft/{id}/regenerate-section': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Regenerate one section of a bounty draft (Organizer Assist)
+ * @description Calls the AI service to regenerate a single section (description, submission, or reward) from the current draft and returns the new section for the organizer to accept or discard.
+ */
+ post: operations['OrganizationBountiesAiController_regenerateSection'];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/organizations/{organizationId}/bounties/{id}/escrow/funding-otp/request': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /** Request a funding step-up code for a bounty */
+ post: operations['OrganizationBountiesEscrowController_requestFundingOtp'];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/organizations/{organizationId}/bounties/{id}/escrow/funding-otp/verify': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /** Verify a funding step-up code for a bounty */
+ post: operations['OrganizationBountiesEscrowController_verifyFundingOtp'];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/api/organizations/{organizationId}/bounties/{id}/escrow/reset-to-draft': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Return a stuck bounty to draft after a failed publish
+ * @description Resets a bounty stranded in draft_awaiting_funding back to draft so the organizer can republish. Refuses while the publish op may still settle on-chain (PENDING_SUBMIT / PENDING_CONFIRM / COMPLETED).
+ */
+ post: operations['OrganizationBountiesEscrowController_resetToDraft'];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
'/api/organizations/{organizationId}/bounties/{id}/escrow/publish': {
parameters: {
query?: never;
@@ -15733,6 +16354,58 @@ export interface components {
/** @description Activity feed */
activities: components['schemas']['EarningActivityDto'][];
};
+ ProfileDetailsDto: {
+ bio: string | null;
+ website: string | null;
+ location: string | null;
+ company: string | null;
+ skills: string[];
+ /** @description Social links keyed by platform. */
+ socialLinks: {
+ [key: string]: unknown;
+ } | null;
+ };
+ ProfileResponseDto: {
+ id: string;
+ name: string | null;
+ email: string;
+ username: string | null;
+ displayUsername: string | null;
+ image: string | null;
+ emailVerified: boolean;
+ /** @description contributor | host (onboarding persona) */
+ persona: string | null;
+ /** @description When onboarding was completed; null if not done. */
+ onboardingCompletedAt: string | null;
+ /** Format: date-time */
+ createdAt: string;
+ /** Format: date-time */
+ updatedAt: string;
+ profile: components['schemas']['ProfileDetailsDto'];
+ };
+ ProfileSocialLinksDto: {
+ /** @description GitHub profile or repository URL */
+ github?: string;
+ /** @description Twitter/X profile URL */
+ twitter?: string;
+ /** @description LinkedIn profile URL */
+ linkedin?: string;
+ /** @description Discord profile or server URL */
+ discord?: string;
+ };
+ UpdateProfileDto: {
+ /** @description Short bio */
+ bio?: string;
+ /** @description Personal or project website URL */
+ website?: string;
+ /** @description Country or location */
+ location?: string;
+ /** @description Company or organization */
+ company?: string;
+ /** @description Skill tags */
+ skills?: string[];
+ socialLinks?: components['schemas']['ProfileSocialLinksDto'];
+ };
DashboardUserStatsDto: {
followers: number;
following: number;
@@ -15749,6 +16422,10 @@ export interface components {
createdAt: string;
stats?: components['schemas']['DashboardUserStatsDto'];
};
+ CurrentUserDto: {
+ /** @description Current user */
+ user: components['schemas']['DashboardUserDto'];
+ };
UserStatsDto: {
projectsCreated: number;
projectsFunded: number;
@@ -15804,7 +16481,19 @@ export interface components {
/** @description Recent activities */
recentActivities: components['schemas']['RecentActivityDto'][];
};
- UpdateProfileDto: Record;
+ CompleteOnboardingDto: {
+ /**
+ * @description How the user plans to use Boundless.
+ * @enum {string}
+ */
+ persona: 'contributor' | 'host';
+ /** @description How the user heard about Boundless. */
+ referralSource?: string;
+ /** @description Skill tags. */
+ skills?: string[];
+ /** @description What the user wants to use Boundless for. */
+ goals?: string[];
+ };
UpdateUserDto: Record;
UploadResponseDto: Record;
MultipleUploadResponseDto: Record;
@@ -15825,6 +16514,73 @@ export interface components {
/** @description Message text */
body: string;
};
+ /** @enum {string} */
+ CreditTier: 'BRONZE' | 'SILVER' | 'GOLD' | 'PLATINUM';
+ CreditSummaryDto: {
+ /**
+ * @description Current credit balance.
+ * @example 6
+ */
+ balance: number;
+ /**
+ * @description Hard ceiling on the balance.
+ * @example 12
+ */
+ cap: number;
+ /** @example BRONZE */
+ tier: components['schemas']['CreditTier'];
+ /**
+ * @description Credits this user resets to on the next refill.
+ * @example 3
+ */
+ refillAmount: number;
+ /**
+ * @description Credits spent so far this calendar month.
+ * @example 8
+ */
+ usedThisMonth: number;
+ /**
+ * Format: date-time
+ * @description When the next monthly refill applies.
+ */
+ nextRefillAt: string;
+ };
+ /** @enum {string} */
+ CreditLedgerReason:
+ | 'WELCOME'
+ | 'REFILL'
+ | 'SPEND'
+ | 'REFUND'
+ | 'SPAM_FORFEIT'
+ | 'ADMIN_GRANT'
+ | 'PURCHASE';
+ CreditLedgerEntryDto: {
+ id: string;
+ /**
+ * @description Signed change to the balance.
+ * @example -1
+ */
+ delta: number;
+ /**
+ * @description Balance after this entry.
+ * @example 5
+ */
+ balanceAfter: number;
+ reason: components['schemas']['CreditLedgerReason'];
+ /** @example BOUNTY_APPLICATION */
+ refType: Record | null;
+ refId: Record | null;
+ /** @example 2026-06 */
+ period: Record | null;
+ /** Format: date-time */
+ createdAt: string;
+ };
+ CreditHistoryDto: {
+ rows: components['schemas']['CreditLedgerEntryDto'][];
+ total: number;
+ page: number;
+ limit: number;
+ };
ContactDto: Record;
CreateCampaignDto: {
/**
@@ -16225,6 +16981,28 @@ export interface components {
/** @description ISO timestamp the dispute was filed */
createdAt: string;
};
+ WalletSummaryAssetDto: {
+ /** @example USDC */
+ assetCode: string;
+ /**
+ * @description Raw asset amount.
+ * @example 125.0000000
+ */
+ balance: string;
+ /** @example 125 */
+ usdValue: Record | null;
+ };
+ WalletSummaryDto: {
+ /** @description Wallet G-address. */
+ address: Record | null;
+ isActivated: boolean;
+ /**
+ * @description Total USD across all assets.
+ * @example 125
+ */
+ totalUsd: number;
+ assets: components['schemas']['WalletSummaryAssetDto'][];
+ };
ReclaimDormantDto: {
/**
* @description Minimum days a wallet must have been idle to be eligible. Default 90.
@@ -16272,36 +17050,6 @@ export interface components {
/** @description Idempotency key to prevent duplicate sends */
idempotencyKey?: string;
};
- SendPayoutDto: {
- /**
- * @description Stellar destination public key (G...)
- * @example GABCD...
- */
- destinationPublicKey: string;
- /**
- * @description Amount to send (positive number)
- * @example 100.5
- */
- amount: number;
- /**
- * @description Asset code (XLM, USDC, EURC, etc. – must be supported on network)
- * @example USDC
- */
- currency: string;
- /** @description Memo (required by some exchanges). Max 28 bytes UTF-8. */
- memo?: string;
- /**
- * @description Memo type
- * @enum {string}
- */
- memoType?: 'text' | 'id';
- /** @description If true, request fails when memo is missing (e.g. exchange requires it) */
- memoRequired?: boolean;
- /** @description Idempotency key to prevent duplicate payouts */
- idempotencyKey?: string;
- /** @description Reference for audit (e.g. earnings-payout-123) */
- reference?: string;
- };
CreateCommentDto: {
/**
* @description Comment content
@@ -17322,9 +18070,21 @@ export interface components {
judging?: components['schemas']['JudgingFormData'];
collaboration?: components['schemas']['CollaborationFormData'];
};
+ HackathonDraftAssumptionDto: {
+ /** @description Wizard section the assumption belongs to. */
+ section: string;
+ /** @description Field within the section. */
+ field: string;
+ /** @description One-line plain reason for the choice. */
+ note: string;
+ };
HackathonDraftAiGenerationDto: {
/** @description AI generationId that produced or last-updated this draft. */
generationId: string;
+ /** @description Non-obvious choices the AI made, for review. */
+ assumptions: components['schemas']['HackathonDraftAssumptionDto'][];
+ /** @description The brief that produced this draft (shown beside the draft). */
+ brief?: string | null;
};
HackathonDraftPrizePlacementDto: {
id: string;
@@ -17384,6 +18144,25 @@ export interface components {
/** @description Hint that this is an autosave (no completion side effects). */
autoSave?: boolean;
};
+ ClarifyHackathonBriefDto: {
+ /** @description Free-text brief to triage for clarifying questions. */
+ brief: string;
+ };
+ ClarifyHackathonQuestionOptionDto: {
+ value: string;
+ label: string;
+ };
+ ClarifyHackathonQuestionDto: {
+ /** @description Axis key, e.g. "duration" | "structure" | "participation". */
+ id: string;
+ question: string;
+ options: components['schemas']['ClarifyHackathonQuestionOptionDto'][];
+ };
+ ClarifyHackathonDraftResponseDto: {
+ /** @description True when the brief is specific enough to draft immediately. */
+ ready: boolean;
+ questions: components['schemas']['ClarifyHackathonQuestionDto'][];
+ };
GenerateDraftFromBriefDto: {
/**
* @description Free-text brief describing the hackathon to generate.
@@ -18457,11 +19236,6 @@ export interface components {
* @example 1
*/
position: number;
- /**
- * @description Override the platform default credit_earn for this position. Defaults to 20/10/5 for positions 1/2/3 and 3 for the rest. Capped at 100 by the contract.
- * @example 20
- */
- creditEarn?: number;
/**
* @description Override the platform default reputation_bump for this position. Defaults to 50/25/10 for positions 1/2/3 and 5 for the rest.
* @example 50
@@ -19064,9 +19838,20 @@ export interface components {
notes?: string;
};
RejectMilestoneDto: {
- /** @description Feedback shown to the builder; what needs to change. */
- rejectionFeedback: string;
- /** @description Optional ISO date by which the builder must resubmit. */
+ /**
+ * @description Rejection reason
+ * @example Incomplete deliverables
+ */
+ reason: string;
+ /**
+ * @description Detailed feedback for rejection
+ * @example The submitted work does not meet the project requirements. Please revise and resubmit.
+ */
+ feedback: string;
+ /**
+ * @description Deadline for resubmission
+ * @example 2025-02-01
+ */
resubmissionDeadline?: string;
};
RequestMilestoneResubmissionDto: {
@@ -19112,8 +19897,11 @@ export interface components {
recipientAddress?: string;
};
AssignDisputeDto: {
- /** @description staff_users id to assign the dispute to, or null to unassign. */
- assignedToStaffId: string | null;
+ /**
+ * @description Admin user ID to assign dispute to
+ * @example 550e8400-e29b-41d4-a716-446655440000
+ */
+ adminId: string;
};
AddDisputeNoteDto: {
/**
@@ -19129,7 +19917,8 @@ export interface components {
};
ResolveDisputeDto: {
/**
- * @description Resolution outcome recorded on the dispute.
+ * @description Resolution type
+ * @example APPROVED_WITH_CONDITIONS
* @enum {string}
*/
resolution:
@@ -19139,11 +19928,17 @@ export interface components {
| 'FULL_REFUND'
| 'DISMISSED'
| 'ARBITRATION';
- /** @description Resolution notes shown to the reporter and campaign creator. */
+ /**
+ * @description Detailed notes explaining the resolution
+ * @example Approved with condition that milestone is completed by next week
+ */
resolutionNotes: string;
};
EscalateDisputeDto: {
- /** @description Why this dispute is being escalated to arbitration. Recorded in the audit log. */
+ /**
+ * @description Reason for escalation to arbitration
+ * @example Parties cannot agree on resolution terms
+ */
reason: string;
};
RejectManualProjectDto: Record;
@@ -19246,6 +20041,23 @@ export interface components {
/** @description Audit label for the rate choice: default | foundation-tier | sales-override:* | waiver:*. */
reason: string;
};
+ AiUsageResponseDto: {
+ /** @description Subscription tier (FREE | PRO | BYOK | ENTERPRISE). */
+ tier: string;
+ /** @description Monthly billable-call limit, or null for unlimited tiers. */
+ limit: number | null;
+ /** @description Billable calls used this month. */
+ used: number;
+ /** @description Remaining billable calls, or null for unlimited. */
+ remaining: number | null;
+ /**
+ * Format: date-time
+ * @description Window reset (UTC).
+ */
+ resetAt: string;
+ /** @description Total spend this month as a decimal string (incl. clarify). */
+ costUsdThisMonth: string;
+ };
StaffPrincipalDto: {
id: string;
email: string;
@@ -19556,6 +20368,10 @@ export interface components {
/** @description ISO timestamp the dispute was opened */
created: string;
};
+ AdminV2AssignDisputeDto: {
+ /** @description staff_users id to assign the dispute to, or null to unassign. */
+ assignedToStaffId: string | null;
+ };
DisputeAssignmentResponseDto: {
id: string;
/** @description Assigned staff email, or null when unassigned */
@@ -19572,6 +20388,21 @@ export interface components {
/** @description ISO timestamp the note was recorded */
createdAt: string;
};
+ AdminV2ResolveDisputeDto: {
+ /**
+ * @description Resolution outcome recorded on the dispute.
+ * @enum {string}
+ */
+ resolution:
+ | 'APPROVED_WITH_CONDITIONS'
+ | 'REQUIRE_RESUBMISSION'
+ | 'PARTIAL_REFUND'
+ | 'FULL_REFUND'
+ | 'DISMISSED'
+ | 'ARBITRATION';
+ /** @description Resolution notes shown to the reporter and campaign creator. */
+ resolutionNotes: string;
+ };
DisputeActionResponseDto: {
id: string;
/** @description New dispute lifecycle status */
@@ -19581,6 +20412,10 @@ export interface components {
/** @description Whether the dispute is escalated to arbitration */
escalatedToArbitration: boolean;
};
+ AdminV2EscalateDisputeDto: {
+ /** @description Why this dispute is being escalated to arbitration. Recorded in the audit log. */
+ reason: string;
+ };
AdminMoneyEntryDto: {
id: string;
/** @enum {string} */
@@ -20217,6 +21052,73 @@ export interface components {
/** @description Hash of the offline-signed, executed transaction */
txHash: string;
};
+ SupportedTokenDto: {
+ id: string;
+ address: string;
+ symbol: string | null;
+ label: string | null;
+ logoUrl: string | null;
+ /** @description Last observed on-chain whitelist state. */
+ lastKnownOnChain: boolean;
+ createdAt: string;
+ };
+ SyncTokensResultDto: {
+ /** @description Number of token rows created or updated. */
+ applied: number;
+ /** @description Tokens seen on-chain (whitelist size for state sync, events scanned for the events fallback). */
+ total: number;
+ /**
+ * @description Which mechanism ran: authoritative state enumeration, or the events fallback when the contract predates the enumerable index.
+ * @enum {string}
+ */
+ source: 'state' | 'events';
+ };
+ RegisterTokenDto: {
+ /** @description Stellar Asset Contract (C...) address of the token to whitelist as an escrow denomination. */
+ token: string;
+ /** @description Display symbol, e.g. "USDC". */
+ symbol?: string;
+ /** @description Human label shown in the portal. */
+ label?: string;
+ /** @description Hosted logo image URL, shown next to the token on consumer surfaces. */
+ logoUrl?: string;
+ };
+ PauseStateDto: {
+ /** @description Live on-chain pause flag for the events contract. */
+ paused: boolean;
+ };
+ ContractOpXdrDto: {
+ /** @description AdminContractOp row id tracking this op. */
+ opId: string;
+ /** @enum {string} */
+ intent: 'REGISTER_TOKEN' | 'DEREGISTER_TOKEN' | 'PAUSE' | 'UNPAUSE';
+ /** @description On-chain contract admin G-address. This is the transaction source and the account that must sign at the Lab. */
+ source: string;
+ /** @description Unsigned transaction XDR for the admin to sign. */
+ unsignedXdr: string;
+ /** @description Stellar Lab "Sign Transaction" deep link. */
+ labUrl: string;
+ /** @enum {string} */
+ network: 'public' | 'testnet';
+ /** @description Network passphrase the signer must use. */
+ passphrase: string;
+ };
+ DeregisterTokenDto: {
+ /** @description Stellar Asset Contract (C...) address to deregister. */
+ token: string;
+ };
+ SubmitSignedContractOpDto: {
+ /** @description Base64 transaction XDR signed offline by the admin at the Stellar Lab. Must be the exact transaction returned by build-xdr (hash-checked). */
+ signedXdr: string;
+ };
+ ContractOpResultDto: {
+ opId: string;
+ /** @enum {string} */
+ intent: 'REGISTER_TOKEN' | 'DEREGISTER_TOKEN' | 'PAUSE' | 'UNPAUSE';
+ /** @description AdminContractOp status after submission. */
+ status: string;
+ txHash: string | null;
+ };
AdminKycListItemDto: {
/** @description User id */
id: string;
@@ -20345,6 +21247,12 @@ export interface components {
/** @description New milestone review status */
status: string;
};
+ AdminV2RejectMilestoneDto: {
+ /** @description Feedback shown to the builder; what needs to change. */
+ rejectionFeedback: string;
+ /** @description Optional ISO date by which the builder must resubmit. */
+ resubmissionDeadline?: string;
+ };
MilestoneReleaseXdrDto: {
/** @description EscrowOp row id tracking this release. */
opId: string;
@@ -20400,6 +21308,192 @@ export interface components {
total: number;
totalPages: number;
};
+ HackathonBriefTemplateDto: {
+ id: string;
+ name: string;
+ description: string;
+ category: string;
+ brief: string;
+ isActive: boolean;
+ sortOrder: number;
+ createdAt: string;
+ updatedAt: string;
+ };
+ HackathonBriefTemplatesResponseDto: {
+ items: components['schemas']['HackathonBriefTemplateDto'][];
+ };
+ CreateHackathonBriefTemplateDto: {
+ name: string;
+ description: string;
+ category: string;
+ brief: string;
+ /** @default true */
+ isActive: boolean;
+ /** @default 0 */
+ sortOrder: number;
+ };
+ UpdateHackathonBriefTemplateDto: {
+ name?: string;
+ description?: string;
+ category?: string;
+ brief?: string;
+ isActive?: boolean;
+ sortOrder?: number;
+ };
+ MarketingTemplateDto: {
+ id: string;
+ name: string;
+ subject: string;
+ body: string;
+ variables: string[];
+ isActive: boolean;
+ sortOrder: number;
+ createdAt: string;
+ updatedAt: string;
+ };
+ MarketingTemplatesResponseDto: {
+ items: components['schemas']['MarketingTemplateDto'][];
+ };
+ CreateMarketingTemplateDto: {
+ name: string;
+ subject: string;
+ body: string;
+ variables?: string[];
+ isActive?: boolean;
+ sortOrder?: number;
+ };
+ UpdateMarketingTemplateDto: {
+ name?: string;
+ subject?: string;
+ body?: string;
+ variables?: string[];
+ isActive?: boolean;
+ sortOrder?: number;
+ };
+ AudienceFilterDto: {
+ /** @enum {string} */
+ type:
+ | 'all'
+ | 'skills_match'
+ | 'participated_in'
+ | 'location'
+ | 'never_applied';
+ skills?: string[];
+ /** @enum {string} */
+ programType?: 'hackathon' | 'bounty' | 'grant';
+ location?: string;
+ };
+ MarketingCampaignDto: {
+ id: string;
+ name: string;
+ subject: string;
+ body: string;
+ audience: components['schemas']['AudienceFilterDto'];
+ templateId?: Record;
+ status: string;
+ scheduledAt?: Record;
+ sentAt?: Record;
+ sentCount: number;
+ /** @description Custom substitution vars for {{key}} placeholders */
+ variables: {
+ [key: string]: string;
+ };
+ createdById: string;
+ createdByEmail: string;
+ createdAt: string;
+ updatedAt: string;
+ };
+ MarketingCampaignsResponseDto: {
+ items: components['schemas']['MarketingCampaignDto'][];
+ };
+ CreateMarketingCampaignDto: {
+ name: string;
+ subject: string;
+ body: string;
+ audience: components['schemas']['AudienceFilterDto'];
+ templateId?: string;
+ scheduledAt?: string;
+ /** @description Key-value pairs substituted into {{key}} placeholders in subject/body */
+ variables?: {
+ [key: string]: string;
+ };
+ };
+ UpdateMarketingCampaignDto: {
+ name?: string;
+ subject?: string;
+ body?: string;
+ audience?: components['schemas']['AudienceFilterDto'];
+ templateId?: string;
+ scheduledAt?: string;
+ variables?: {
+ [key: string]: string;
+ };
+ };
+ PreviewCampaignAudienceDto: {
+ audience: components['schemas']['AudienceFilterDto'];
+ };
+ CampaignAudienceSizeDto: {
+ count: number;
+ };
+ AutomationConditionsDto: {
+ /** @description Min hours before re-sending to same user */
+ cooldownHours?: number;
+ };
+ MarketingAutomationDto: {
+ id: string;
+ name: string;
+ trigger: string;
+ audience: components['schemas']['AudienceFilterDto'];
+ templateId: string;
+ templateName: string;
+ conditions: components['schemas']['AutomationConditionsDto'];
+ isActive: boolean;
+ lastFiredAt?: Record;
+ fireCount: number;
+ createdById: string;
+ createdByEmail: string;
+ createdAt: string;
+ updatedAt: string;
+ };
+ MarketingAutomationsResponseDto: {
+ items: components['schemas']['MarketingAutomationDto'][];
+ };
+ CreateMarketingAutomationDto: {
+ name: string;
+ /** @enum {string} */
+ trigger:
+ | 'new_hackathon_published'
+ | 'new_bounty_published'
+ | 'new_grant_published'
+ | 'new_crowdfunding_launched'
+ | 'hackathon_deadline_3d'
+ | 'hackathon_deadline_7d'
+ | 'bounty_deadline_3d'
+ | 'grant_deadline_3d'
+ | 'user_signup'
+ | 'user_inactive';
+ audience: components['schemas']['AudienceFilterDto'];
+ templateId: string;
+ conditions?: components['schemas']['AutomationConditionsDto'];
+ };
+ UpdateMarketingAutomationDto: {
+ name?: string;
+ /** @enum {string} */
+ trigger?:
+ | 'new_hackathon_published'
+ | 'new_bounty_published'
+ | 'new_grant_published'
+ | 'new_crowdfunding_launched'
+ | 'hackathon_deadline_3d'
+ | 'hackathon_deadline_7d'
+ | 'bounty_deadline_3d'
+ | 'grant_deadline_3d'
+ | 'user_signup'
+ | 'user_inactive';
+ audience?: components['schemas']['AudienceFilterDto'];
+ templateId?: string;
+ conditions?: components['schemas']['AutomationConditionsDto'];
+ };
Function: Record;
BountyScopeSectionDto: {
/** @description Bounty title */
@@ -20488,6 +21582,30 @@ export interface components {
amount: string;
passMark?: number | null;
};
+ BountyDraftAssumptionDto: {
+ /** @description Wizard section the assumption belongs to. */
+ section: string;
+ /** @description Field within the section. */
+ field: string;
+ /** @description One-line plain reason for the choice. */
+ note: string;
+ };
+ BountyDraftGeneratedModeDto: {
+ entryType: string;
+ claimType: string;
+ };
+ BountyDraftAiGenerationDto: {
+ /** @description AI generationId that produced this draft. */
+ generationId: string;
+ /** @description Non-obvious choices the AI made, for review. */
+ assumptions: components['schemas']['BountyDraftAssumptionDto'][];
+ /** @description The brief that produced this draft (shown beside the draft). */
+ brief?: string | null;
+ /** @description The mode the AI generated for (to detect a later mode change). */
+ generatedMode?:
+ | components['schemas']['BountyDraftGeneratedModeDto']
+ | null;
+ };
BountyDraftResponseDto: {
id: string;
/**
@@ -20507,6 +21625,8 @@ export interface components {
validationErrors: {
[key: string]: Record[];
};
+ /** @description Present when the draft was generated with Organizer Assist; enables per-section AI regenerate in the wizard. */
+ aiGeneration?: components['schemas']['BountyDraftAiGenerationDto'];
/** Format: date-time */
createdAt: string;
/** Format: date-time */
@@ -20521,6 +21641,72 @@ export interface components {
/** @description Hint that this is an autosave (no completion side effects). */
autoSave?: boolean;
};
+ ClarifyBountyBriefDto: {
+ /** @description Free-text brief to triage for clarifying questions. */
+ brief: string;
+ };
+ ClarifyQuestionOptionDto: {
+ value: string;
+ label: string;
+ };
+ ClarifyQuestionDto: {
+ /** @description Axis key, e.g. "winners" | "entry" | "deadline". */
+ id: string;
+ question: string;
+ options: components['schemas']['ClarifyQuestionOptionDto'][];
+ };
+ ClarifyBountyDraftResponseDto: {
+ /** @description True when the brief is specific enough to draft immediately. */
+ ready: boolean;
+ questions: components['schemas']['ClarifyQuestionDto'][];
+ };
+ GenerateBountyDraftFromBriefDto: {
+ /**
+ * @description Free-text brief describing the bounty to generate.
+ * @example A one-week bounty to design three onboarding illustrations for a Stellar wallet, paid to the best entry.
+ */
+ brief: string;
+ /**
+ * @description Total reward budget in USDC, as a decimal string.
+ * @example 500
+ */
+ budgetCapUsdc: string;
+ /**
+ * @description Earliest the bounty may start (YYYY-MM-DD).
+ * @example 2026-07-01
+ */
+ earliestStart: string;
+ /** @description Up to 3 example briefs to steer style. */
+ examples?: string[];
+ };
+ BountyAiGenerationMetaDto: {
+ generationId: string;
+ model: string;
+ promptVersion: string;
+ /** @description Cost in USD as a decimal string (never a float). */
+ costUsd: string;
+ };
+ GenerateBountyDraftFromBriefResponseDto: {
+ /** @description Id of the created draft. */
+ draftId: string;
+ draft: components['schemas']['BountyDraftResponseDto'];
+ generation: components['schemas']['BountyAiGenerationMetaDto'];
+ };
+ RegenerateBountyDraftSectionDto: {
+ /** @enum {string} */
+ section: 'description' | 'submission' | 'reward';
+ /** @description Optional steering instructions for the regeneration. */
+ instructions?: string;
+ };
+ RegenerateBountyDraftSectionResponseDto: {
+ /** @enum {string} */
+ section: 'description' | 'submission' | 'reward';
+ /** @description Regenerated values in the wizard section shape. */
+ data: {
+ [key: string]: unknown;
+ };
+ generation: components['schemas']['BountyAiGenerationMetaDto'];
+ };
BountyWinnerDistributionEntryDto: {
/**
* @description 1-indexed winner position.
@@ -20554,12 +21740,6 @@ export interface components {
* @example 1804383600
*/
submissionDeadline: number | null;
- /**
- * @description Credits required to apply. Defaults to 1; raise on high-value bounties to deter spam. Max 100 (enforced by the contract).
- * @default 1
- * @example 1
- */
- applicationCreditCost: number;
/** @description Override for the winner distribution. Defaults to 100% to position 1. */
winnerDistribution?: components['schemas']['BountyWinnerDistributionEntryDto'][];
/** @description Override for the content URI stored on chain. Defaults to https://api.boundless.fi/bounties//content. */
@@ -20624,11 +21804,6 @@ export interface components {
* @example 1
*/
position: number;
- /**
- * @description Override the platform default credit_earn for this position. Defaults to 20/10/5 for positions 1/2/3 and 3 for the rest. Capped at 100 by the contract.
- * @example 20
- */
- creditEarn?: number;
/**
* @description Override the platform default reputation_bump for this position. Defaults to 50/25/10 for positions 1/2/3 and 5 for the rest.
* @example 50
@@ -21091,8 +22266,6 @@ export interface components {
* @example 1
*/
position: number;
- /** @example 20 */
- creditEarn?: number;
/** @example 50 */
reputationBump?: number;
};
@@ -21937,7 +23110,7 @@ export interface operations {
};
};
};
- UserController_getProfile: {
+ ProfileController_getProfile: {
parameters: {
query?: never;
header?: never;
@@ -21946,13 +23119,13 @@ export interface operations {
};
requestBody?: never;
responses: {
- /** @description User dashboard with profile, stats, chart data, activities graph, and recent activities retrieved successfully */
+ /** @description Profile retrieved successfully */
200: {
headers: {
[name: string]: unknown;
};
content: {
- 'application/json': components['schemas']['DashboardDto'];
+ 'application/json': components['schemas']['ProfileResponseDto'];
};
};
/** @description Unauthorized */
@@ -21964,25 +23137,43 @@ export interface operations {
};
};
};
- UserController_getPublic: {
+ ProfileController_updateProfile: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- requestBody?: never;
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['UpdateProfileDto'];
+ };
+ };
responses: {
- /** @description Public route accessed successfully */
+ /** @description Profile updated successfully */
200: {
headers: {
[name: string]: unknown;
};
content?: never;
};
+ /** @description Bad request */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Unauthorized */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
};
};
- UserController_getOptional: {
+ ProfileController_getProfileStats: {
parameters: {
query?: never;
header?: never;
@@ -21991,36 +23182,40 @@ export interface operations {
};
requestBody?: never;
responses: {
- /** @description Optional auth route accessed successfully */
+ /** @description Profile stats retrieved successfully */
200: {
headers: {
[name: string]: unknown;
};
content?: never;
};
+ /** @description Unauthorized */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
};
};
- UserController_getUserByUsername: {
+ ProfileController_getActivity: {
parameters: {
query?: never;
header?: never;
- path: {
- /** @description Username of the user to retrieve */
- username: string;
- };
+ path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description User profile retrieved successfully */
+ /** @description Activity retrieved successfully */
200: {
headers: {
[name: string]: unknown;
};
content?: never;
};
- /** @description User not found */
- 404: {
+ /** @description Unauthorized */
+ 401: {
headers: {
[name: string]: unknown;
};
@@ -22028,32 +23223,66 @@ export interface operations {
};
};
};
- UserController_getUserFollowers: {
+ ProfileController_uploadAvatar: {
parameters: {
- query?: {
- /** @description Pagination offset */
- offset?: number;
- /** @description Number of followers to return */
- limit?: number;
- };
+ query?: never;
header?: never;
- path: {
- /** @description Username of the user */
- username: string;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ 'multipart/form-data': {
+ /**
+ * Format: binary
+ * @description Avatar image file
+ */
+ avatar?: string;
+ };
+ };
+ };
+ responses: {
+ /** @description Avatar uploaded successfully */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Bad request */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
};
+ /** @description Unauthorized */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ PreferencesController_getPreferences: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description Followers retrieved successfully */
+ /** @description Preferences retrieved successfully */
200: {
headers: {
[name: string]: unknown;
};
content?: never;
};
- /** @description User not found */
- 404: {
+ /** @description Unauthorized */
+ 401: {
headers: {
[name: string]: unknown;
};
@@ -22061,41 +23290,38 @@ export interface operations {
};
};
};
- UserController_getUserFollowing: {
+ PreferencesController_updateLanguage: {
parameters: {
- query?: {
- /** @description Filter by entity type */
- entityType?:
- | 'USER'
- | 'PROJECT'
- | 'ORGANIZATION'
- | 'CROWDFUNDING_CAMPAIGN'
- | 'BOUNTY'
- | 'GRANT'
- | 'HACKATHON';
- /** @description Pagination offset */
- offset?: number;
- /** @description Number of following to return */
- limit?: number;
- };
+ query?: never;
header?: never;
- path: {
- /** @description Username of the user */
- username: string;
- };
+ path?: never;
cookie?: never;
};
- requestBody?: never;
+ requestBody: {
+ content: {
+ 'application/json': {
+ /** @example en */
+ language?: string;
+ };
+ };
+ };
responses: {
- /** @description Following list retrieved successfully */
+ /** @description Language preference updated successfully */
200: {
headers: {
[name: string]: unknown;
};
content?: never;
};
- /** @description User not found */
- 404: {
+ /** @description Bad request */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Unauthorized */
+ 401: {
headers: {
[name: string]: unknown;
};
@@ -22103,22 +23329,36 @@ export interface operations {
};
};
};
- ProfileController_getProfile: {
+ PreferencesController_updateTimezone: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- requestBody?: never;
+ requestBody: {
+ content: {
+ 'application/json': {
+ /** @example America/New_York */
+ timezone?: string;
+ };
+ };
+ };
responses: {
- /** @description Profile retrieved successfully */
+ /** @description Timezone preference updated successfully */
200: {
headers: {
[name: string]: unknown;
};
content?: never;
};
+ /** @description Bad request */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
/** @description Unauthorized */
401: {
headers: {
@@ -22128,7 +23368,7 @@ export interface operations {
};
};
};
- ProfileController_updateProfile: {
+ PreferencesController_updateCategoryPreferences: {
parameters: {
query?: never;
header?: never;
@@ -22137,11 +23377,19 @@ export interface operations {
};
requestBody: {
content: {
- 'application/json': components['schemas']['UpdateProfileDto'];
+ 'application/json': {
+ /**
+ * @example [
+ * "tech",
+ * "design"
+ * ]
+ */
+ categories?: string[];
+ };
};
};
responses: {
- /** @description Profile updated successfully */
+ /** @description Category preferences updated successfully */
200: {
headers: {
[name: string]: unknown;
@@ -22164,22 +23412,41 @@ export interface operations {
};
};
};
- ProfileController_getProfileStats: {
+ PreferencesController_updateSkillPreferences: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- requestBody?: never;
+ requestBody: {
+ content: {
+ 'application/json': {
+ /**
+ * @example [
+ * "javascript",
+ * "react"
+ * ]
+ */
+ skills?: string[];
+ };
+ };
+ };
responses: {
- /** @description Profile stats retrieved successfully */
+ /** @description Skill preferences updated successfully */
200: {
headers: {
[name: string]: unknown;
};
content?: never;
};
+ /** @description Bad request */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
/** @description Unauthorized */
401: {
headers: {
@@ -22189,7 +23456,7 @@ export interface operations {
};
};
};
- ProfileController_getActivity: {
+ UserController_getMe: {
parameters: {
query?: never;
header?: never;
@@ -22198,13 +23465,42 @@ export interface operations {
};
requestBody?: never;
responses: {
- /** @description Activity retrieved successfully */
+ /** @description Current user retrieved successfully */
200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['CurrentUserDto'];
+ };
+ };
+ /** @description Unauthorized */
+ 401: {
headers: {
[name: string]: unknown;
};
content?: never;
};
+ };
+ };
+ UserController_getDashboard: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Dashboard with profile, stats, chart data, activities graph, and recent activities retrieved successfully */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['DashboardDto'];
+ };
+ };
/** @description Unauthorized */
401: {
headers: {
@@ -22214,7 +23510,7 @@ export interface operations {
};
};
};
- ProfileController_uploadAvatar: {
+ UserController_completeOnboarding: {
parameters: {
query?: never;
header?: never;
@@ -22223,17 +23519,11 @@ export interface operations {
};
requestBody: {
content: {
- 'multipart/form-data': {
- /**
- * Format: binary
- * @description Avatar image file
- */
- avatar?: string;
- };
+ 'application/json': components['schemas']['CompleteOnboardingDto'];
};
};
responses: {
- /** @description Avatar uploaded successfully */
+ /** @description Onboarding completed */
200: {
headers: {
[name: string]: unknown;
@@ -22256,7 +23546,7 @@ export interface operations {
};
};
};
- PreferencesController_getPreferences: {
+ UserController_getPublic: {
parameters: {
query?: never;
header?: never;
@@ -22265,93 +23555,54 @@ export interface operations {
};
requestBody?: never;
responses: {
- /** @description Preferences retrieved successfully */
+ /** @description Public route accessed successfully */
200: {
headers: {
[name: string]: unknown;
};
content?: never;
};
- /** @description Unauthorized */
- 401: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
};
};
- PreferencesController_updateLanguage: {
+ UserController_getOptional: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- requestBody: {
- content: {
- 'application/json': {
- /** @example en */
- language?: string;
- };
- };
- };
+ requestBody?: never;
responses: {
- /** @description Language preference updated successfully */
+ /** @description Optional auth route accessed successfully */
200: {
headers: {
[name: string]: unknown;
};
content?: never;
};
- /** @description Bad request */
- 400: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
- /** @description Unauthorized */
- 401: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
};
};
- PreferencesController_updateTimezone: {
+ UserController_getUserByUsername: {
parameters: {
query?: never;
header?: never;
- path?: never;
- cookie?: never;
- };
- requestBody: {
- content: {
- 'application/json': {
- /** @example America/New_York */
- timezone?: string;
- };
+ path: {
+ /** @description Username of the user to retrieve */
+ username: string;
};
+ cookie?: never;
};
+ requestBody?: never;
responses: {
- /** @description Timezone preference updated successfully */
+ /** @description User profile retrieved successfully */
200: {
headers: {
[name: string]: unknown;
};
content?: never;
};
- /** @description Bad request */
- 400: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
- /** @description Unauthorized */
- 401: {
+ /** @description User not found */
+ 404: {
headers: {
[name: string]: unknown;
};
@@ -22359,43 +23610,32 @@ export interface operations {
};
};
};
- PreferencesController_updateCategoryPreferences: {
+ UserController_getUserFollowers: {
parameters: {
- query?: never;
+ query?: {
+ /** @description Pagination offset */
+ offset?: number;
+ /** @description Number of followers to return */
+ limit?: number;
+ };
header?: never;
- path?: never;
- cookie?: never;
- };
- requestBody: {
- content: {
- 'application/json': {
- /**
- * @example [
- * "tech",
- * "design"
- * ]
- */
- categories?: string[];
- };
+ path: {
+ /** @description Username of the user */
+ username: string;
};
+ cookie?: never;
};
+ requestBody?: never;
responses: {
- /** @description Category preferences updated successfully */
+ /** @description Followers retrieved successfully */
200: {
headers: {
[name: string]: unknown;
};
content?: never;
};
- /** @description Bad request */
- 400: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
- /** @description Unauthorized */
- 401: {
+ /** @description User not found */
+ 404: {
headers: {
[name: string]: unknown;
};
@@ -22403,43 +23643,41 @@ export interface operations {
};
};
};
- PreferencesController_updateSkillPreferences: {
+ UserController_getUserFollowing: {
parameters: {
- query?: never;
+ query?: {
+ /** @description Filter by entity type */
+ entityType?:
+ | 'USER'
+ | 'PROJECT'
+ | 'ORGANIZATION'
+ | 'CROWDFUNDING_CAMPAIGN'
+ | 'BOUNTY'
+ | 'GRANT'
+ | 'HACKATHON';
+ /** @description Pagination offset */
+ offset?: number;
+ /** @description Number of following to return */
+ limit?: number;
+ };
header?: never;
- path?: never;
- cookie?: never;
- };
- requestBody: {
- content: {
- 'application/json': {
- /**
- * @example [
- * "javascript",
- * "react"
- * ]
- */
- skills?: string[];
- };
+ path: {
+ /** @description Username of the user */
+ username: string;
};
+ cookie?: never;
};
+ requestBody?: never;
responses: {
- /** @description Skill preferences updated successfully */
+ /** @description Following list retrieved successfully */
200: {
headers: {
[name: string]: unknown;
};
content?: never;
};
- /** @description Bad request */
- 400: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
- /** @description Unauthorized */
- 401: {
+ /** @description User not found */
+ 404: {
headers: {
[name: string]: unknown;
};
@@ -23557,6 +24795,47 @@ export interface operations {
};
};
};
+ CreditsController_getMine: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['CreditSummaryDto'];
+ };
+ };
+ };
+ };
+ CreditsController_getHistory: {
+ parameters: {
+ query?: {
+ page?: number;
+ limit?: number;
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['CreditHistoryDto'];
+ };
+ };
+ };
+ };
CampaignsController_validateCampaign: {
parameters: {
query?: never;
@@ -24611,6 +25890,25 @@ export interface operations {
};
};
};
+ WalletController_getWalletSummary: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['WalletSummaryDto'];
+ };
+ };
+ };
+ };
WalletController_getAddressBalance: {
parameters: {
query?: never;
@@ -24841,42 +26139,6 @@ export interface operations {
};
};
};
- WalletPayoutController_sendPayout: {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- requestBody: {
- content: {
- 'application/json': components['schemas']['SendPayoutDto'];
- };
- };
- responses: {
- /** @description Payout submitted successfully */
- 201: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
- /** @description Validation or business rule error (e.g. destination not activated, no trustline, memo required) */
- 400: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
- /** @description Forbidden – admin only */
- 403: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
- };
- };
CommentsController_listComments: {
parameters: {
query?: {
@@ -27314,6 +28576,46 @@ export interface operations {
};
};
};
+ OrganizationHackathonsAiController_clarify: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description Organization ID */
+ organizationId: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['ClarifyHackathonBriefDto'];
+ };
+ };
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ClarifyHackathonDraftResponseDto'];
+ };
+ };
+ /** @description Invalid request */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Resource not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
OrganizationHackathonsAiController_generateFromBrief: {
parameters: {
query?: never;
@@ -27355,6 +28657,30 @@ export interface operations {
};
};
};
+ OrganizationHackathonsAiController_generateFromBriefStream: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description Organization ID */
+ organizationId: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['GenerateDraftFromBriefDto'];
+ };
+ };
+ responses: {
+ 201: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
OrganizationHackathonsAiController_regenerateSection: {
parameters: {
query?: never;
@@ -34745,6 +36071,28 @@ export interface operations {
};
};
};
+ AiUsageController_getUsage: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description Organization ID */
+ organizationId: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['AiUsageResponseDto'];
+ };
+ };
+ };
+ };
AdminOpsController_pause: {
parameters: {
query?: never;
@@ -35291,7 +36639,7 @@ export interface operations {
};
requestBody: {
content: {
- 'application/json': components['schemas']['AssignDisputeDto'];
+ 'application/json': components['schemas']['AdminV2AssignDisputeDto'];
};
};
responses: {
@@ -35341,7 +36689,7 @@ export interface operations {
};
requestBody: {
content: {
- 'application/json': components['schemas']['ResolveDisputeDto'];
+ 'application/json': components['schemas']['AdminV2ResolveDisputeDto'];
};
};
responses: {
@@ -35366,7 +36714,7 @@ export interface operations {
};
requestBody: {
content: {
- 'application/json': components['schemas']['EscalateDisputeDto'];
+ 'application/json': components['schemas']['AdminV2EscalateDisputeDto'];
};
};
responses: {
@@ -36383,6 +37731,195 @@ export interface operations {
};
};
};
+ GovernanceContractController_listTokens: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['SupportedTokenDto'][];
+ };
+ };
+ };
+ };
+ GovernanceContractController_syncTokens: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['SyncTokensResultDto'];
+ };
+ };
+ };
+ };
+ GovernanceContractController_importToken: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['RegisterTokenDto'];
+ };
+ };
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['SupportedTokenDto'];
+ };
+ };
+ };
+ };
+ GovernanceContractController_pauseState: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['PauseStateDto'];
+ };
+ };
+ };
+ };
+ GovernanceContractController_buildRegisterToken: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['RegisterTokenDto'];
+ };
+ };
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ContractOpXdrDto'];
+ };
+ };
+ };
+ };
+ GovernanceContractController_buildDeregisterToken: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['DeregisterTokenDto'];
+ };
+ };
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ContractOpXdrDto'];
+ };
+ };
+ };
+ };
+ GovernanceContractController_buildPause: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ContractOpXdrDto'];
+ };
+ };
+ };
+ };
+ GovernanceContractController_buildUnpause: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ContractOpXdrDto'];
+ };
+ };
+ };
+ };
+ GovernanceContractController_submitSigned: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['SubmitSignedContractOpDto'];
+ };
+ };
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ContractOpResultDto'];
+ };
+ };
+ };
+ };
KycController_list: {
parameters: {
query?: {
@@ -36580,7 +38117,7 @@ export interface operations {
};
requestBody: {
content: {
- 'application/json': components['schemas']['RejectMilestoneDto'];
+ 'application/json': components['schemas']['AdminV2RejectMilestoneDto'];
};
};
responses: {
@@ -36667,6 +38204,434 @@ export interface operations {
};
};
};
+ HackathonBriefTemplatesController_list: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['HackathonBriefTemplatesResponseDto'];
+ };
+ };
+ };
+ };
+ HackathonBriefTemplatesController_create: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['CreateHackathonBriefTemplateDto'];
+ };
+ };
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['HackathonBriefTemplateDto'];
+ };
+ };
+ };
+ };
+ HackathonBriefTemplatesController_update: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['UpdateHackathonBriefTemplateDto'];
+ };
+ };
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['HackathonBriefTemplateDto'];
+ };
+ };
+ };
+ };
+ HackathonBriefTemplatesController_archive: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 204: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ HackathonBriefTemplatesPublicController_list: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['HackathonBriefTemplatesResponseDto'];
+ };
+ };
+ };
+ };
+ MarketingTemplatesController_list: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['MarketingTemplatesResponseDto'];
+ };
+ };
+ };
+ };
+ MarketingTemplatesController_create: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['CreateMarketingTemplateDto'];
+ };
+ };
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['MarketingTemplateDto'];
+ };
+ };
+ };
+ };
+ MarketingTemplatesController_update: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['UpdateMarketingTemplateDto'];
+ };
+ };
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['MarketingTemplateDto'];
+ };
+ };
+ };
+ };
+ MarketingTemplatesController_archive: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 204: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ MarketingCampaignsController_list: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['MarketingCampaignsResponseDto'];
+ };
+ };
+ };
+ };
+ MarketingCampaignsController_create: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['CreateMarketingCampaignDto'];
+ };
+ };
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['MarketingCampaignDto'];
+ };
+ };
+ };
+ };
+ MarketingCampaignsController_update: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['UpdateMarketingCampaignDto'];
+ };
+ };
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['MarketingCampaignDto'];
+ };
+ };
+ };
+ };
+ MarketingCampaignsController_cancel: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 204: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ MarketingCampaignsController_audienceSize: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['PreviewCampaignAudienceDto'];
+ };
+ };
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['CampaignAudienceSizeDto'];
+ };
+ };
+ };
+ };
+ MarketingCampaignsController_send: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['MarketingCampaignDto'];
+ };
+ };
+ };
+ };
+ MarketingAutomationsController_list: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['MarketingAutomationsResponseDto'];
+ };
+ };
+ };
+ };
+ MarketingAutomationsController_create: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['CreateMarketingAutomationDto'];
+ };
+ };
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['MarketingAutomationDto'];
+ };
+ };
+ };
+ };
+ MarketingAutomationsController_update: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['UpdateMarketingAutomationDto'];
+ };
+ };
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['MarketingAutomationDto'];
+ };
+ };
+ };
+ };
+ MarketingAutomationsController_remove: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 204: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ MarketingAutomationsController_toggle: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['MarketingAutomationDto'];
+ };
+ };
+ };
+ };
PricesController_getAll: {
parameters: {
query?: never;
@@ -37124,6 +39089,184 @@ export interface operations {
};
};
};
+ OrganizationBountiesAiController_clarify: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description Organization ID */
+ organizationId: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['ClarifyBountyBriefDto'];
+ };
+ };
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ClarifyBountyDraftResponseDto'];
+ };
+ };
+ };
+ };
+ OrganizationBountiesAiController_generateFromBrief: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description Organization ID */
+ organizationId: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['GenerateBountyDraftFromBriefDto'];
+ };
+ };
+ responses: {
+ /** @description Draft generated and pre-filled. */
+ 201: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['GenerateBountyDraftFromBriefResponseDto'];
+ };
+ };
+ };
+ };
+ OrganizationBountiesAiController_generateFromBriefStream: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description Organization ID */
+ organizationId: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['GenerateBountyDraftFromBriefDto'];
+ };
+ };
+ responses: {
+ 201: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ OrganizationBountiesAiController_regenerateSection: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description Organization ID */
+ organizationId: string;
+ /** @description Bounty draft ID */
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['RegenerateBountyDraftSectionDto'];
+ };
+ };
+ responses: {
+ /** @description Regenerated section returned. */
+ 201: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['RegenerateBountyDraftSectionResponseDto'];
+ };
+ };
+ };
+ };
+ OrganizationBountiesEscrowController_requestFundingOtp: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ organizationId: string;
+ /** @description Bounty draft id */
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['RequestFundingOtpResponseDto'];
+ };
+ };
+ };
+ };
+ OrganizationBountiesEscrowController_verifyFundingOtp: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ organizationId: string;
+ /** @description Bounty draft id */
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['VerifyFundingOtpDto'];
+ };
+ };
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['VerifyFundingOtpResponseDto'];
+ };
+ };
+ };
+ };
+ OrganizationBountiesEscrowController_resetToDraft: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ organizationId: string;
+ /** @description Bounty draft id */
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Bounty reset to draft. */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
OrganizationBountiesEscrowController_publish: {
parameters: {
query?: never;