@@ -740,6 +820,12 @@ function CreatorDetailPageContent() {
)}
+
+
{creator.publicLaunchDate && hasValidPublicLaunchDate && (
{isPublicLaunchPending
diff --git a/src/services/earlyIncentive.service.ts b/src/services/earlyIncentive.service.ts
new file mode 100644
index 00000000..844fc30a
--- /dev/null
+++ b/src/services/earlyIncentive.service.ts
@@ -0,0 +1,76 @@
+import { BaseApiService, type APIResponse } from './api.service';
+
+/** Early funding incentive returned by GET /keys/:keyId/early-incentive. */
+export interface EarlyIncentive {
+ /** Bonus yield in basis points granted to early investors. */
+ bonusBps: number;
+ /** Remaining early funding slots; 0 means every slot is taken. */
+ slotsRemaining: number;
+ /** Total early funding slots, when the API reports it. */
+ slotsTotal?: number;
+ /** False when the key has no early incentive configured. */
+ configured: boolean;
+}
+
+type UnknownRecord = Record;
+
+function asRecord(value: unknown): UnknownRecord {
+ return typeof value === 'object' && value !== null
+ ? (value as UnknownRecord)
+ : {};
+}
+
+function asNonNegativeInt(value: unknown): number {
+ const parsed = typeof value === 'number' ? value : Number(value);
+ return Number.isFinite(parsed) && parsed >= 0 ? Math.floor(parsed) : 0;
+}
+
+/**
+ * Normalize the early-incentive payload so the UI never depends on
+ * backend field aliases (slots_remaining vs slotsRemaining, etc.).
+ */
+export function normalizeEarlyIncentive(value: unknown): EarlyIncentive {
+ const raw = asRecord(value);
+ const bonusBps = asNonNegativeInt(raw.bonusBps ?? raw.bonus_bps ?? raw.bps);
+ const slotsRemaining = asNonNegativeInt(
+ raw.slotsRemaining ?? raw.slots_remaining ?? raw.remainingSlots
+ );
+ const slotsTotalRaw =
+ raw.slotsTotal ?? raw.slots_total ?? raw.totalSlots ?? undefined;
+ const slotsTotal =
+ slotsTotalRaw === undefined || slotsTotalRaw === null
+ ? undefined
+ : asNonNegativeInt(slotsTotalRaw);
+
+ const configuredFlag = raw.configured ?? raw.isConfigured ?? raw.active;
+ const configured =
+ typeof configuredFlag === 'boolean'
+ ? configuredFlag
+ : bonusBps > 0 || slotsRemaining > 0 || Boolean(slotsTotal);
+
+ return {
+ bonusBps,
+ slotsRemaining,
+ ...(slotsTotal !== undefined ? { slotsTotal } : {}),
+ configured,
+ };
+}
+
+class EarlyIncentiveService extends BaseApiService {
+ async getEarlyIncentive(keyId: string): Promise {
+ try {
+ const response = await this.api.get>(
+ `/keys/${encodeURIComponent(keyId)}/early-incentive`
+ );
+ return normalizeEarlyIncentive(response.data.data);
+ } catch (error) {
+ throw this.handleError(error);
+ }
+ }
+}
+
+export const earlyIncentiveService = new EarlyIncentiveService();
+
+export function fetchEarlyIncentive(keyId: string): Promise {
+ return earlyIncentiveService.getEarlyIncentive(keyId);
+}
diff --git a/src/services/issuerVerification.service.ts b/src/services/issuerVerification.service.ts
new file mode 100644
index 00000000..9a8129f6
--- /dev/null
+++ b/src/services/issuerVerification.service.ts
@@ -0,0 +1,80 @@
+import { BaseApiService, type APIResponse } from './api.service';
+
+export type IssuerVerificationTier = 'basic' | 'enhanced' | 'institutional';
+
+/** Issuer verification returned by GET /issuers/:wallet/verification. */
+export interface IssuerVerification {
+ tier: IssuerVerificationTier;
+ /** ISO expiry date for the verification tier, when the API reports it. */
+ expiresAt: string | null;
+ verified: boolean;
+}
+
+type UnknownRecord = Record;
+
+function asRecord(value: unknown): UnknownRecord {
+ return typeof value === 'object' && value !== null
+ ? (value as UnknownRecord)
+ : {};
+}
+
+const TIER_ALIASES: Record = {
+ basic: 'basic',
+ tier1: 'basic',
+ '1': 'basic',
+ enhanced: 'enhanced',
+ tier2: 'enhanced',
+ '2': 'enhanced',
+ institutional: 'institutional',
+ tier3: 'institutional',
+ '3': 'institutional',
+};
+
+function normalizeTier(value: unknown): IssuerVerificationTier | null {
+ const key = String(value ?? '').trim().toLowerCase();
+ return TIER_ALIASES[key] ?? null;
+}
+
+function normalizeExpiry(value: unknown): string | null {
+ if (typeof value === 'string' && value.trim() !== '') return value;
+ if (typeof value === 'number' && Number.isFinite(value)) {
+ return new Date(value < 1_000_000_000_000 ? value * 1_000 : value).toISOString();
+ }
+ return null;
+}
+
+export function normalizeIssuerVerification(value: unknown): IssuerVerification {
+ const raw = asRecord(value);
+ const tier =
+ normalizeTier(raw.tier ?? raw.verificationTier ?? raw.level) ?? 'basic';
+ const verifiedFlag = raw.verified ?? raw.isVerified;
+ const verified =
+ typeof verifiedFlag === 'boolean' ? verifiedFlag : tier !== null;
+
+ return {
+ tier,
+ expiresAt: normalizeExpiry(raw.expiresAt ?? raw.expires_at ?? raw.expiry),
+ verified,
+ };
+}
+
+class IssuerVerificationService extends BaseApiService {
+ async getIssuerVerification(wallet: string): Promise {
+ try {
+ const response = await this.api.get>(
+ `/issuers/${encodeURIComponent(wallet)}/verification`
+ );
+ return normalizeIssuerVerification(response.data.data);
+ } catch (error) {
+ throw this.handleError(error);
+ }
+ }
+}
+
+export const issuerVerificationService = new IssuerVerificationService();
+
+export function fetchIssuerVerification(
+ wallet: string
+): Promise {
+ return issuerVerificationService.getIssuerVerification(wallet);
+}
diff --git a/src/services/paymentSchedule.service.ts b/src/services/paymentSchedule.service.ts
new file mode 100644
index 00000000..4f9dae7c
--- /dev/null
+++ b/src/services/paymentSchedule.service.ts
@@ -0,0 +1,114 @@
+import { BaseApiService, type APIResponse } from './api.service';
+
+export type PaymentInstalmentStatus = 'paid' | 'pending' | 'overdue';
+
+/** One scheduled instalment on a payment schedule. */
+export interface PaymentInstalment {
+ /** 1-based instalment number. */
+ number: number;
+ /** ISO due date for the instalment. */
+ dueDate: string;
+ /** Instalment amount, in XLM. */
+ amount: number;
+ status: PaymentInstalmentStatus;
+}
+
+/** Instalment-based repayment schedule for a key/invoice. */
+export interface PaymentSchedule {
+ instalments: PaymentInstalment[];
+ /** Cumulative repaid amount, in XLM. */
+ paidAmount: number;
+ /** Total scheduled amount, in XLM. */
+ totalAmount: number;
+}
+
+type UnknownRecord = Record;
+
+function asRecord(value: unknown): UnknownRecord {
+ return typeof value === 'object' && value !== null
+ ? (value as UnknownRecord)
+ : {};
+}
+
+function asNumber(value: unknown, fallback = 0): number {
+ const parsed = typeof value === 'number' ? value : Number(value);
+ return Number.isFinite(parsed) ? parsed : fallback;
+}
+
+function normalizeDueDate(value: unknown): string {
+ if (typeof value === 'string' && value.trim() !== '') return value;
+ if (typeof value === 'number' && Number.isFinite(value)) {
+ return new Date(
+ value < 1_000_000_000_000 ? value * 1_000 : value
+ ).toISOString();
+ }
+ return new Date().toISOString();
+}
+
+function normalizeStatus(value: unknown, dueDate: string): PaymentInstalmentStatus {
+ const status = String(value ?? '').trim().toLowerCase();
+ if (status === 'paid' || status === 'complete' || status === 'completed') {
+ return 'paid';
+ }
+ if (status === 'overdue' || status === 'late') return 'overdue';
+ if (status === 'pending' || status === 'scheduled' || status === 'due') {
+ return 'pending';
+ }
+ // Derive from the due date when the API omits a status.
+ return new Date(dueDate).getTime() < Date.now() ? 'overdue' : 'pending';
+}
+
+export function normalizePaymentInstalment(
+ value: unknown,
+ index: number
+): PaymentInstalment {
+ const raw = asRecord(value);
+ const dueDate = normalizeDueDate(raw.dueDate ?? raw.due_date ?? raw.dueAt);
+ return {
+ number: Math.max(1, Math.floor(asNumber(raw.number ?? raw.instalmentNumber ?? index + 1))),
+ dueDate,
+ amount: asNumber(raw.amount ?? raw.value ?? raw.total),
+ status: normalizeStatus(raw.status ?? raw.state, dueDate),
+ };
+}
+
+export function normalizePaymentSchedule(value: unknown): PaymentSchedule {
+ const raw = asRecord(value);
+ const data =
+ raw.instalments ?? raw.schedule ?? raw.payments ?? raw.data ?? value;
+ const rows = Array.isArray(data) ? data : [];
+
+ const instalments = rows
+ .map((row, index) => normalizePaymentInstalment(row, index))
+ .sort((a, b) => a.number - b.number);
+
+ const totalAmount =
+ asNumber(raw.totalAmount ?? raw.total_amount) ||
+ instalments.reduce((sum, item) => sum + item.amount, 0);
+ const paidAmount =
+ asNumber(raw.paidAmount ?? raw.paid_amount) ||
+ instalments
+ .filter(item => item.status === 'paid')
+ .reduce((sum, item) => sum + item.amount, 0);
+
+ return { instalments, paidAmount, totalAmount };
+}
+
+class PaymentScheduleService extends BaseApiService {
+ async getPaymentSchedule(keyId: string): Promise {
+ try {
+ const response = await this.api.get>(
+ `/keys/${encodeURIComponent(keyId)}/payment-schedule`
+ );
+ return normalizePaymentSchedule(response.data.data);
+ } catch (error) {
+ throw this.handleError(error);
+ }
+ }
+}
+
+export const paymentScheduleService = new PaymentScheduleService();
+
+export function fetchPaymentSchedule(keyId: string): Promise {
+ return paymentScheduleService.getPaymentSchedule(keyId);
+}
diff --git a/src/services/secondaryPrices.service.ts b/src/services/secondaryPrices.service.ts
new file mode 100644
index 00000000..3b41949e
--- /dev/null
+++ b/src/services/secondaryPrices.service.ts
@@ -0,0 +1,90 @@
+import { BaseApiService, type APIResponse } from './api.service';
+
+export type SecondaryPriceRange = '7d' | '30d' | 'all';
+
+/** One secondary-market trade point from GET /keys/:keyId/secondary_prices. */
+export interface SecondaryPricePoint {
+ /** Trade time as a unix-ms timestamp. */
+ timestamp: number;
+ /** Trade price per fraction, in XLM. */
+ price: number;
+ /** Trade size (fractions) for the transaction. */
+ volume: number;
+}
+
+type UnknownRecord = Record;
+
+function asRecord(value: unknown): UnknownRecord {
+ return typeof value === 'object' && value !== null
+ ? (value as UnknownRecord)
+ : {};
+}
+
+function asNumber(value: unknown, fallback = 0): number {
+ const parsed = typeof value === 'number' ? value : Number(value);
+ return Number.isFinite(parsed) ? parsed : fallback;
+}
+
+function normalizeTimestamp(value: unknown): number {
+ if (typeof value === 'number' && Number.isFinite(value)) {
+ return value < 1_000_000_000_000 ? value * 1_000 : value;
+ }
+ if (typeof value === 'string') {
+ const numericValue = Number(value);
+ if (Number.isFinite(numericValue) && value.trim() !== '') {
+ return normalizeTimestamp(numericValue);
+ }
+ const parsed = new Date(value).getTime();
+ if (Number.isFinite(parsed)) return parsed;
+ }
+ return Date.now();
+}
+
+export function normalizeSecondaryPricePoint(
+ value: unknown
+): SecondaryPricePoint {
+ const raw = asRecord(value);
+ return {
+ timestamp: normalizeTimestamp(
+ raw.timestamp ?? raw.createdAt ?? raw.tradedAt ?? raw.time
+ ),
+ price: asNumber(raw.price ?? raw.pricePerFraction ?? raw.tradePrice),
+ volume: asNumber(raw.volume ?? raw.quantity ?? raw.amount ?? raw.size),
+ };
+}
+
+export function normalizeSecondaryPrices(value: unknown): SecondaryPricePoint[] {
+ const data =
+ asRecord(value).prices ?? asRecord(value).trades ?? asRecord(value).data ?? value;
+ const rows = Array.isArray(data) ? data : [];
+
+ return rows
+ .map(normalizeSecondaryPricePoint)
+ .sort((a, b) => a.timestamp - b.timestamp);
+}
+
+class SecondaryPricesService extends BaseApiService {
+ async getSecondaryPrices(
+ keyId: string,
+ range: SecondaryPriceRange = '7d'
+ ): Promise {
+ try {
+ const response = await this.api.get>(
+ `/keys/${encodeURIComponent(keyId)}/secondary_prices`,
+ { params: { range } }
+ );
+ return normalizeSecondaryPrices(response.data.data);
+ } catch (error) {
+ throw this.handleError(error);
+ }
+ }
+}
+
+export const secondaryPricesService = new SecondaryPricesService();
+
+export function fetchSecondaryPrices(
+ keyId: string,
+ range: SecondaryPriceRange = '7d'
+): Promise {
+ return secondaryPricesService.getSecondaryPrices(keyId, range);
+}