Appearance
Admin Panel
The admin panel is accessible only to users with the admin role. It provides full control over the platform — creators, offers, approvals, analytics, payouts, and support. Admins access it via the "⚙ Admin" button on their dashboard or by navigating directly to /admin.
The admin panel has its own navigation sidebar with sections: Overview, Applications, Users, Offers, Approvals, Links, Payouts, Crons, Support.
Applications
The Applications section shows creator applications submitted through the moneymatchup.com apply form. Applications are stored in Airtable (base appmbrP38m4Ak1lb9, table Creator Submissions) and pulled into the admin panel in real time.
Application list Each row shows: creator name, email, submitted date, and monthly affiliate earnings. Spam submissions (bot-generated names with randomized casing and suspicious email patterns) are automatically filtered out before display.
Reviewing an application Clicking "Review" on any row expands a detail panel showing all information submitted in the application:
- Name and email
- Website link
- Social media channels and follower counts
- Bio / about section
- How they plan to market b2b and fintech products
- How they heard about Money Matchup
- Referral code (if any)
Spam auto-rejection When the Applications page loads, the server silently scans all Pending Review records in Airtable and auto-rejects any that match the spam detection heuristics (fire-and-forget, does not block the page load). Spam records are marked Status = "Rejected" with Rejection Notes = "Auto-rejected: spam submission". This keeps the pending queue clean without manual intervention. Note: "Spam" is not used as a status value because it is not a valid option in the Airtable Status field.
Duplicate detection If multiple pending applications share the same email address, each is flagged with an amber "duplicate" badge next to the email in the table row. The detail panel also shows a warning: "Another application exists with this email address."
Application age indicator Each row in the pending list shows a color-coded age badge next to the submission date based on how many days old the application is:
- 0–3 days: no badge (or subtle grey)
- 4–7 days: amber "Xd" badge (needs attention)
- 8+ days: red "Xd" badge (urgent)
Approving Click "Approve" — this:
- Updates the application's
Statusfield in Airtable to"Approved" - Auto-creates a platform account for the creator in the database (if one doesn't already exist) with role
creatorand tier 3 - Generates a one-time magic link for the creator's email (valid 24 hours, redirects to
/dashboard) - Sends the creator an approval email from
apple@creatorsagency.cowith the magic link embedded so they can sign in immediately — no separate account setup required
The approval email includes the magic link, explains how the platform works (browse offers, create links, track earnings), and invites the creator to reply with questions. The reply-to header is set to apple@creatorsagency.co.
If magic link generation fails, the email falls back to the auth page URL (https://partners.moneymatchup.com/auth) — the approval is never blocked.
Rejecting Click "Reject" — instead of immediately firing the API, a small inline form appears below the action buttons with a textarea for an internal rejection reason. The reason is optional and never sent to the creator.
- Click "Confirm Reject" to proceed (sends
{ action: 'reject', reason: '...' }to the API) - Click "Cancel" to go back without rejecting
When rejected:
- Updates the application's
Statusfield in Airtable to"Rejected" - If a reason was entered, stores it in the Airtable
"Rejection Notes"field (internal only) - Sends the creator a polite rejection email from
apple@creatorsagency.co. Thereply-toheader is set toapple@creatorsagency.co. The rejection reason is not included in the email.
Once approved or rejected, the application disappears from the Pending tab. Use the "All" tab to see the full history.
Users
The Users section lists every creator account registered on the platform. Each row in the table shows: profile photo (circular avatar), display name, email address, tier, and status. → DB: Reads from User table
Searching and filtering Admins can search creators by name or email. The table updates in real time.
Editing a creator Clicking a creator opens an edit modal with the following fields:
- Display name — The creator's name shown across the platform
- Legal name — Used for payment processing and payout records. Pulled from Airtable when available.
- Email — Read-only in the modal; changing email requires a database-level change due to auth constraints
- Tier — Dropdown with labeled options:
- Tier 1 — Full control (custom slugs, any length)
- Tier 2 — Partial control (slug customization with constraints)
- Tier 3 — Auto-generated slugs (random suffix, no control)
- Status — Custom styled dropdown: Active (green), Inactive (grey), Pending (yellow)
- Internal platforms — Multi-select checkboxes for "Affiliate Platform" and "Ps-Workflow". Controls what the creator sees on their dashboard and what appears in their magic link email.
- Profile photo — Admins can upload a photo directly from the modal. The file is uploaded to Vercel Blob storage and the URL is saved to the creator's profile. The circular avatar in the table updates immediately without a page reload. → DB: URL stored in
User.profilePhotoUrl(col:image) andCreatorProfile.profilePhotoUrl
Changes to tier and all other fields are saved together in a single "Save" action. The users table updates immediately after save without requiring a page reload. → DB: Updates User (name, legalName, tier, status, internalPlatforms, profilePhotoUrl)
"Open as" (Impersonation) Each creator row has an "↗ Open as" button (shown for all non-admin users). Clicking it:
- Sends a request to create a real BetterAuth session for that creator
- Returns a short-lived impersonation token
- Navigates to
/api/admin/impersonate/go?t=TOKEN - The platform signs the token, sets the creator's session cookies (both secure and non-secure variants)
- Stashes the admin's original session cookie as
impersonation-return - Redirects to
/dashboardas that creator
→ DB: Creates a new row in Session for the target creator
While impersonating, a purple banner appears at the top of every page reading "← Return to my account". Clicking it hits /api/admin/impersonate/return, which restores the stashed admin session and clears the impersonation cookies.
Offers
The Offers section is the most complex part of the admin panel. It manages all financial product offers available on the platform.
Offer list Each offer in the list shows: name, data source, approval type, number of providers, and active/inactive status. → DB: Reads from Offer (name, approvalType, status) joined to OfferProvider for dataSource
Creating an offer Clicking "New Offer" opens a form with:
- Name
- Category (investing, credit_cards, banking, insurance, loans, business, general)
- Approval type: None (auto), Standard (requires review), Manual (requires review + admin-provided link)
- Featured toggle (shows offer on creator dashboards in the featured grid)
→ DB: Inserts a row into Offer (name, category, approvalType, featured)
Editing an offer — tabs Opening an existing offer shows a tabbed editor with:
Details tab
- Name, category, approval type
- Featured toggle
- Allowlist: a multi-select of specific creators. When populated, only those creators can see and request this offer. If empty, the offer is available to all creators.
→ DB: Saves update to Offer (name, category, approvalType, featured, visibility); reads/writes OfferAllowlist (offerId, userId)
Providers tab
Shows all configured providers for this offer plus any draft providers being added.
Existing providers show:
- Data source type (badge)
- Current weight (percentage)
- Active/inactive toggle
- Edit button to expand settings
- Delete button (with confirmation dialog before removing)
Adding a new provider — a "+ Add Provider" button creates a draft provider card with:
- Data source dropdown (custom, manual, redeventures, money_com, bankrate, quinnstreet, impact, partnerstack)
- Fields that appear based on selected data source:
- custom: affiliate URL template field with
{slug}placeholder syntax - manual: no extra fields — links are set per-creator at approval time
- redeventures / money_com / bankrate: base URL field, loanPurpose field
- quinnstreet: base URL field, loanPurpose field
- impact: campaign dropdown (auto-loads from Impact API when selected), then ad unit dropdown (loads after campaign selection). Both use "Looking up..." loading state while fetching.
- partnerstack: company slug field (e.g. "quickbooks", "deel") + a lookup button. Clicking lookup hits PartnerStack's API to verify the partnership exists and fetch the default link settings. Shows a status indicator (valid / not found / error).
- custom: affiliate URL template field with
- Default weight input (number, 0–100)
- DRAFT badge shown on unsaved providers
→ DB: Adding a provider inserts into OfferProvider (offerId, displayName, dataSource, defaultWeight, active, affiliateUrlTemplate / baseUrl / loanPurpose / configJson)
Multiple draft providers can be open simultaneously. Each maintains its own independent state.
Weight validation The "Save Changes" button is disabled if the combined weight of all active providers does not equal exactly 100%. A warning message shows the current total. Inactive providers are excluded from this calculation. → DB: Validates sum of OfferProvider.defaultWeight where active = true equals 100
Saving "Save Changes" batch-PATCHes all existing providers in a single operation, then POSTs each new draft provider. After saving, the router backend is notified of the updated configuration. → DB: Updates OfferProvider for existing rows; inserts new rows for draft providers
Deleting a provider Clicking the delete button (with confirmation) removes the provider record. → DB: Deletes from OfferProvider; cascades to delete associated GeneratedLink rows
Split tab
The Split tab manages per-creator weight overrides for this offer. By default, all creators receive traffic according to the offer's default provider weights. Overrides allow specific creators to have different splits.
Each override row shows the creator and individual weight values per provider. Weights in each row must also total 100%. The "Save Changes" button validates before saving. → DB: Upserts CreatorWeights (userId, offerId, weightsJson)
Approvals
When a creator requests access to an offer that has approval type "Standard" or "Manual", the request appears in the Approvals queue.
Each approval request shows:
- Creator name and email
- Which offer they're requesting
- Request date
- Status: Pending, Approved, Denied
- Their connected channels (platform + handle)
→ DB: Reads from OfferApproval (status, userId, offerId, creatorPlatformIds)
Approving a Standard offer Click "Approve" — the creator's status updates to approved and they can now generate links immediately. → DB: Updates OfferApproval.status to "approved"
Approving a Manual offer Manual offers require the admin to paste in the creator-specific affiliate URL at the time of approval. A text input appears in the approval card for this. On approval, a GeneratedLink record is created with that URL, associated with the creator and the manual provider. The creator then sees this link appear on their Links page. → DB: Updates OfferApproval.status to "approved"; creates a row in GeneratedLink (affiliateLinkId, providerId, dataSource, link)
Denying Click "Deny" — the creator's request is rejected. Their offer card shows "Denied" with an option to re-apply. → DB: Updates OfferApproval.status to "denied", sets OfferApproval.denialReason
Links
The Links section provides an overview of all affiliate links across all creators.
Layout Creators are shown as an accordion list. Click a creator's row to expand and see all their affiliate links.
Filtering A filter bar at the top of the page lets admins narrow the list:
- Creator search — Type a name or email to filter the creator list in real time
- Offer filter — A searchable dropdown to show only links for a specific offer
- Min earnings — Enter a dollar amount to show only links that have earned at least that much
- Filters combine — all active at once. A "Clear" button resets everything.
Per-link information Each link shows:
- The slug (displayed as
secure.moneymatchup.com/{slug}) - Which offer it belongs to
- Which platform/channel it's for
- Status (active, pending)
- Earnings to date
- A Copy button — click to copy the full redirect URL to clipboard. The button turns green with a checkmark for 2 seconds to confirm.
Clicking a link row expands a detail panel showing clicks, conversions, and earnings broken out, plus a direct "↗ Open link" shortcut.
→ DB: Reads from AffiliateLink joined to affiliate_analytics
Stats shown here are the same data pulled by the analytics cron — they reflect the last completed sync.
Creating a link on behalf of a creator The "+ Create Link" button in the top-right opens a modal where admins can create a link on behalf of any creator. Fields: creator (searchable dropdown), channel (optional), offer/campaign, and optional custom slug (with real-time availability check). The link is created as active immediately and appears in the creator's list without a page reload.
Payouts
The Payouts page exists in the navigation but currently shows an empty state only. Real payout data is not yet wired. This section is planned for future development.
Crons
The Crons page shows and controls the platform's two automated background jobs.
Analytics Cron
- Schedule: Daily at 2:00am UTC
- What it does: Iterates over all affiliate links in the database, queries each provider's reporting API (Money.com, Impact, PartnerStack, RedVentures, Bankrate, QuinnStreet), matches results back to creator slugs, and writes click/conversion/earnings data to the
AffiliateAnalyticstable - Implementation detail: Accepts a single
{ date: string }per request. The admin UI loops client-side to call it once per date — this avoids Vercel's function timeout limits - Manual trigger: Admins select a date range and click "Run". A live log output appears in real time as each provider is queried for each date. Useful for backfilling missing data or re-running after a provider outage.
→ DB: Writes to affiliate_analytics (affiliateLinkId, providerId, date, clicks, conversions, earnings)
PartnerStack Auth Cron
- Schedule: Every Monday at 9:00am UTC
- What it does: Re-authenticates with PartnerStack using stored credentials (their session tokens expire weekly). The process involves solving a Turnstile CAPTCHA (automated), logging in via their API, and storing the new
_dwrfJWT string in theAppSessiondatabase table. - Why it matters: If this cron fails, PartnerStack link generation and analytics pulls will stop working. The current token's expiry date is tracked in the database.
- Manual trigger: Admins can force a re-auth from this page without waiting for Monday.
→ DB: Writes to AppSession (key = 'partnerstack_cookie', value = new _dwrf JWT)
Cron status display Each cron shows: last run time, next scheduled run, and recent run dates.
Support
The Support section shows all creator-submitted support tickets.
Ticket list Each ticket shows: creator name, subject/message preview, submitted date, and whether it has been replied to. → DB: Reads from SupportTicket; reply existence checked via SupportReply
Ticket detail Clicking a ticket shows the full message and any reply thread. Admins can type a reply directly in the panel and send it. Replies are stored in the SupportReply table and visible to the creator in their Support page. → DB: Inserts into SupportReply (ticketId, body)