Verity AI Internal Docs
Enter password to view implementation plan
Wrong password. Try again.
Tristan Hamm AI Sponsorship Engine
8-stage pipeline spec: architecture, data schema, week-by-week build plan, and stage-by-stage technical detail. Built to execute from.
Architecture Overview
The 8 stages map to 4 application systems sitting on top of a single data layer. Tristan sees Airtable. The systems run as Python processes on Annette's machine (or a VPS if uptime matters).
Stages 01 + 02. Finds brand prospects, scores them through a Claude rubric, writes qualified brands to Airtable.
Stages 03 + 04. Sends DMs and emails, logs every touch to Airtable, runs the nurture sequence via APScheduler.
Stage 05. Watches a folder for Zoom recordings, transcribes with Whisper, extracts deal intel with Claude, creates Deal records.
Stages 06 + 08. Routes sub-$15k deals to the boxer network, calculates splits, tracks fulfillment deliverables.
6-table Airtable base. Python writes to it via pyairtable. Nightly gspread export to Tristan's existing Google Sheet.
Data flow sketch
Humans in the loop
Stage 07 (Close) is fully human. Tristan takes the call, negotiates, and marks the deal Won in Airtable. Everything before and after that is automated. Stage 06 routing may be semi-automated depending on Tristan's preference (confirm at discovery call).
Discovery Call Agenda (90 minutes)
Run this as a structured intake. Take notes in a shared Google Doc or record the call (get permission). Every bolded item is something you need before you can build.
Block 1: Current Pipeline (20 min)
- "Walk me through how you land a sponsorship deal today, start to finish. Who does what?"
- "How many brands do you reach out to in a typical month?"
- "What's your current close rate? How many outreach attempts to one signed deal?"
- Get the Google Sheet URL. Ask them to share it with [email protected]. Look at it live: columns, statuses, how they've been tracking.
- "Which of these deals are you tracking actively vs. dead?"
- "What does a deal look like when it falls through? What's the most common reason?"
Block 2: Brand Categories (20 min)
- "Let's go category by category. Combat sports brands: who are your dream sponsors? Who have you already talked to?"
- "Equestrian: same question. Who's shown interest, who haven't you touched?"
- "Outdoor / lifestyle: who's on your radar?"
- "Any categories that are off the table? Anything you've tried that didn't fit your audience?"
- "Tell me about your best deal ever. What brand, what amount, how'd it happen?"
- "Tell me about a deal you thought was going to close and it didn't. What happened?"
- Collect: a list of 10-20 brands per category (even half-formed is fine). These seed the prospecting database.
- "What does a bad-fit brand look like? Anything you'd be embarrassed to promote?"
Block 3: Boxer Network (15 min)
- "Walk me through how you use the boxer network today. How many fighters are in it?"
- "How do you offer a deal to them currently? Email, DM, text?"
- Get: current split percentage (Tristan's cut vs. the boxer's cut).
- "Is the split the same for every deal, or does it vary?"
- "What's the threshold where you'd rather close it yourself vs. hand it off?"
- "Have any fighters turned down a deal? Why?"
- "Do boxers have content deliverables, or is it mostly appearance/endorsement?"
- Collect: boxer name, Instagram handle, email, and specialty category for each fighter in the network.
Block 4: Voice Samples (10 min)
- "Can you share 2-3 DMs you've sent to brands that you felt good about? Even screenshotted."
- "Do you have any outreach emails you've sent? Could be from Gmail sent folder."
- "How would you describe your tone when you're pitching? Casual, professional, hype-y?"
- "Any phrases or sign-offs you always use?"
- Collect: at least 3-5 real outreach samples. These train the voice model for DeepSeek.
Block 5: Zoom Setup (10 min)
- "Do you have a paid Zoom plan?" (Free plan doesn't have cloud recordings.)
- "Are you currently recording your sales calls? Cloud or local?"
- "If cloud: we can pull via Zoom API automatically. If local: we'll set up a folder you drop files into."
- Decision: cloud API vs. watchdog folder. Get Zoom API credentials if cloud.
- "How long are your typical first-call brand conversations? 20 min? 45?"
Block 6: Tech Access Checklist (10 min)
Go through these one by one. Note what's ready vs. needs setup.
| What | Why needed | Status to confirm |
|---|---|---|
| Instagram Business Account | Required for Messenger API | Business or Creator? Linked to a Facebook Page? |
| Facebook Page (linked to IG) | Required for Messenger API app approval | Does it exist? Is Tristan the admin? |
| Gmail / Google Workspace | Email outreach + Sheets sync | Get OAuth consent; confirm which Gmail address for outreach |
| Existing Google Sheet URL | Nightly sync target | Get during call |
| Zoom account | Recording access | Paid plan? Cloud recordings enabled? |
| Airtable account | CRM frontend | Does he have one? Or create new? |
Block 7: Deliverable Preferences (5 min)
- "What do you want to see in the CRM every morning? What's the daily dashboard view for you?"
- "Do you want email summaries, or will you check Airtable directly?"
- "When a deal gets routed to a boxer, do you want to approve it first, or should the system send automatically?"
- "Who else on your team needs access? Manager, assistant?"
- "Any deal categories where you'd want a human flag before outreach goes out?"
Tech Stack Decisions
| Component | Tool | Why | Gotcha |
|---|---|---|---|
| CRM / database | Airtable | Visual frontend Tristan can use without training. Python-writable via pyairtable. Free tier covers 1,000 records/base; pro tier at $20/mo if needed. | NOT Cloudflare D1 (no frontend). NOT a raw Postgres db. Tristan needs to see his pipeline without asking Annette. |
| Brand prospecting | Manual seed list + Claude expansion + Zendriver LinkedIn search | Instagram graph API won't give brand discovery. Industry sources (SponsorUnited, SportSponsor.com) have curated lists. Claude expands per category. Zendriver scrapes LinkedIn company pages for contact details. | Build from industry lists first. Use Instagram scraping only for contact lookup on known brands. |
| AI qualifier | Claude Sonnet 4.6 via OpenRouter | 4-dimension scoring rubric needs reasoning, not just classification. Sonnet is faster and cheaper than Opus for this volume (90-150 brands, runs once). | Include Tristan's audience demographics in every system prompt. Without them the scores are generic. |
| Instagram outreach | Instagram Messenger API (official). Fallback: Phantombuster. | Official API avoids account bans. Requires Business account + Facebook App + approval. Phantombuster is the bridge while approval processes. | Approval takes 1-5 business days. Apply Day 5 of Week 1. Confirm Tristan has a Business account (not personal or Creator) at discovery call. |
| Email outreach | Gmail API via OAuth (google-auth, googleapiclient) | Sends from Tristan's actual Gmail, so replies land in his inbox naturally. OAuth flow is one-time setup. | Gmail daily send limit: 500 emails/day for personal Gmail, 2,000/day for Workspace. Start at 20/day. |
| Outreach personalization | DeepSeek V3.1 via OpenRouter (deepseek/deepseek-chat) | Bulk generation task. DeepSeek is the cost-optimal choice here ($0.27/1M input). Voice guide in system prompt. | |
| Nurture sequencer | APScheduler (Python) | Runs inside the Outreach system process. Twice-daily poll for brands due a follow-up. Same send infra as Stage 03. | Check reply status before sending any nurture. A brand that replied (even "not interested") must be removed from the sequence immediately. |
| Zoom transcription | Whisper local (openai-whisper, model: medium). Fallback: OpenAI Whisper API. | Local Whisper costs nothing per call and keeps recordings private. "medium" model: good accuracy, ~5-8 min per 30-min call on CPU. If Tristan has Zoom cloud recordings, pull Zoom's own transcript via API first (faster, free). | Local Whisper "medium" needs ~2GB RAM. Use "base" for speed if CPU is slow. Zoom cloud transcript is preferred if available. |
| Zoom recording intake | watchdog library (folder monitor). Alternate: Zoom API polling. | Watchdog triggers immediately when Tristan drops a file in a designated folder. Zoom API polling works if cloud recordings are enabled (poll every 15 min post-call). | Decision made at discovery call based on Tristan's Zoom plan. |
| AI analysis of Zoom | Claude Sonnet 4.6 via Anthropic direct with JSON mode | Extraction needs structured output and long-context handling for full transcripts. JSON mode ensures clean field parsing into Airtable. | |
| Deal router | Python logic + Claude scoring + Gmail API for notification | Pure Python logic for the threshold check and split math. Claude scores boxer-deal fit. Gmail sends the offer to the boxer. | NEEDConfirm at discovery: fully automatic routing, or Tristan approves each assignment first? |
| Google Sheets sync | gspread library | One-way nightly export: Airtable data → Tristan's existing Sheet. Keeps his team in the tool they already use without migrating. | Sync is one-way (Airtable is the source of truth). Edits in Sheets will be overwritten. |
| Scheduler / cron | APScheduler (in-process) + Windows Task Scheduler (for process restarts) | APScheduler handles all in-app scheduling (nurture, sync). Task Scheduler ensures processes restart after machine reboots. | If this moves to a VPS, replace Task Scheduler with systemd services. |
Airtable Data Schema
6 tables. Create these in order (linked fields require the target table to exist first).
Table 1: Brands
| Field | Type | Notes |
|---|---|---|
brand_id | Auto number | Primary key |
name | Single line text | Brand name |
category | Single select | Options: combat_sports, equestrian, outdoor |
instagram_handle | Single line text | Without @ |
contact_email | Primary outreach email | |
qualifier_score | Number (decimal) | Composite score 1-10 from Stage 02 |
category_fit | Number | Sub-score 1-10 |
audience_alignment | Number | Sub-score 1-10 |
deal_size_potential | Number | Sub-score 1-10 |
brand_marketing_activity | Number | Sub-score 1-10 |
status | Single select | Options: uncontacted, outreach_ready, outreached, nurturing, responded, deal_opened, won, lost, cold |
last_contacted | Date | Date of most recent outreach touch |
website | URL | Optional |
notes | Long text | Any research notes, context from Tristan |
do_not_contact | Checkbox | Hard exclude from outreach |
Table 2: Outreach_Log
| Field | Type | Notes |
|---|---|---|
log_id | Auto number | Primary key |
brand_id | Link to Brands | Required |
channel | Single select | instagram_dm, email |
touch_number | Number | 1 = initial, 2 = day-3 follow-up, 3 = day-7, 4 = day-14 |
sent_at | Date/time | Timestamp of send |
message_preview | Long text | First 500 chars of what was sent |
response_status | Single select | sent, delivered, read, replied, bounced, opted_out |
follow_up_date | Date | Next scheduled touch (null if sequence complete) |
reply_preview | Long text | First 500 chars of their reply, if any |
Table 3: Deals
| Field | Type | Notes |
|---|---|---|
deal_id | Auto number | Primary key |
brand_id | Link to Brands | Required |
amount_discussed | Currency | Dollar amount from Zoom call |
status | Single select | prospect, negotiating, won, lost |
closer_notes | Long text | Tristan fills this manually post-call |
zoom_transcript | Long text | Full Whisper transcript (may truncate in Airtable UI; full version in file) |
zoom_recording_path | Single line text | Local file path to the recording |
objections_raised | Long text | Extracted from Zoom by Claude |
next_action | Single line text | Claude-extracted: e.g. "Send media kit" |
next_action_date | Date | Claude-extracted follow-up date |
signed_date | Date | Filled on Won |
deal_value_final | Currency | Actual contracted amount (may differ from discussed) |
routed_to_boxer | Checkbox | True if sent to boxer network |
Table 4: Boxer_Network
| Field | Type | Notes |
|---|---|---|
boxer_id | Auto number | Primary key |
name | Single line text | Full name |
categories | Multiple select | Same options as Brands.category |
instagram_handle | Single line text | Without @ |
email | Primary contact for deal offers | |
split_pct | Number (decimal) | Tristan's cut as a decimal (e.g., 0.20 = Tristan keeps 20%) |
active | Checkbox | Unchecked = not currently accepting deals |
follower_count | Number | Approx Instagram followers |
notes | Long text | Category strengths, audience notes |
Table 5: Boxer_Deals
| Field | Type | Notes |
|---|---|---|
assignment_id | Auto number | Primary key |
deal_id | Link to Deals | Required |
boxer_id | Link to Boxer_Network | Required |
split_amount | Currency | Tristan's cut in dollars (deal_value × boxer.split_pct) |
boxer_earnings | Currency | Boxer's cut in dollars |
status | Single select | offered, accepted, rejected, completed |
assigned_at | Date/time | When offer was sent |
offer_expires_at | Date/time | 48-hour expiry, then route to next boxer |
offer_message | Long text | Text of the offer email sent to the boxer |
Table 6: Fulfillment
| Field | Type | Notes |
|---|---|---|
fulfillment_id | Auto number | Primary key |
deal_id | Link to Deals | Required |
deliverable_name | Single line text | e.g. "1x Instagram Reel", "3x Stories", "1x Event Appearance" |
deliverable_type | Single select | reel, story, post, event, podcast, other |
due_date | Date | Contractual deadline |
status | Single select | pending, in_progress, delivered, approved, revision_requested |
brand_requirements | Long text | Brand's content guidelines, hashtags, mentions required |
delivered_url | URL | Link to submitted content |
brand_feedback | Long text | Brand approval notes or revision requests |
Recommended Airtable Views
- Brands: Pipeline Kanban- grouped by
status. Tristan's daily view. - Brands: Outreach Queue- filtered to
status = outreach_ready, sorted byqualifier_scoredesc. - Outreach_Log: Today's Activity- filtered to
sent_at = today. - Deals: Active Deals- filtered to
status IN (prospect, negotiating), sorted bynext_action_date. - Fulfillment: This Week's Deadlines- filtered to
due_date <= today + 7, sorted bydue_dateasc.
Week-by-Week Build Plan
Data Layer + Intelligence System (System A)
status = uncontacted.status = uncontacted, send each to Claude Sonnet via OpenRouter with the 4-dimension rubric, parse JSON response, write scores back to Airtable. Set status = outreach_ready for any brand scoring above 6.0 composite. Run it. Review first 20 results manually.gspread export script. OAuth with Tristan's Google account. Export: Brands table + Outreach_Log + Deals to three tabs in his existing Sheet. Schedule nightly at 11pm. Test it live.Outreach System (System B)
voice_guide.md.{{brand_name}}, {{specific_brand_detail}}, and {{tristan_relevant_tie_in}} as variable slots. Review every template manually before going live.response_status = delivered AND follow_up_date <= today AND touch_number < 4. For each: generate the appropriate follow-up (day-3 = check-in, day-7 = audience stat value-add, day-14 = "last touch"). Send, log, update follow_up_date to null after touch 4.Analysis + Routing + Go-Live (Systems C + D)
C:/TristanZoom/ (or equivalent). On new M4A/MP4 file: run Whisper medium, save transcript to .txt. Send transcript to Claude Sonnet with JSON extraction prompt. Parse JSON. Write to Deals table: brand_id (match by brand name), amount_discussed, objections_raised, next_action, next_action_date, zoom_transcript. If Zoom cloud API: add polling job (every 15 min) to check for new cloud recordings.amount_discussed < 15000 AND status = negotiating AND routed_to_boxer = false. For each: query active boxers whose categories match the deal's brand category. Run Claude scoring: "Rank these boxers for this deal. Consider category fit and follower count." Top match gets the offer email via Gmail API. Create Boxer_Deals record. Set 48-hour expiry. If no acceptance after 48h: offer to next ranked boxer.status = won: create a fulfillment checklist using the deal's deliverable type. APScheduler job (Monday 8am): query Fulfillment records due in next 7 days, compile summary, email to Tristan via Gmail API.Stage-by-Stage Technical Detail
Find Prospects
(2) Claude expansion prompt: "Given this creator's categories [combat sports / equestrian / outdoor] and existing partners [list], what other brand types belong on this list? Return 20 specific brand names with Instagram handles."
(3) Zendriver: for each brand without a contact email, search LinkedIn company page, extract email from About section or contact info.
status = uncontactedpyairtable, openrouter SDK (or requests), zendriverQualify
status = uncontactedSystem prompt: "You are scoring sponsorship prospects for Tristan Hamm, a combat sports / equestrian / outdoor creator with 2M followers. His audience is [demographics from discovery call]. Score each brand 1-10 on four dimensions."
User prompt: brand name, category, Instagram handle, any bio info.
Rubric (JSON output):
category_fit: How naturally does this brand fit combat sports, equestrian, or outdoor? (1 = zero fit, 10 = perfect match)audience_alignment: How well does this brand's target demo match Tristan's audience? (1 = mismatch, 10 = ideal overlap)deal_size_potential: Based on brand size and category, how large is a realistic deal? (1 = under $1k, 10 = over $50k)brand_marketing_activity: Does this brand actively run influencer programs? (1 = no evidence, 10 = clearly active)composite: Average of the four.rationale: One sentence.
status = outreach_ready where composite > 6.0.
pyairtable, openrouter SDKOutreach
status = outreach_ready and last_contacted is null or > 30 days ago(2) DeepSeek generates personalized message: system prompt = voice guide, user prompt = "Write an outreach [DM / email] to {{brand_name}} in the {{category}} space. One specific brand detail to mention: {{brand_detail}}. Tristan tie-in: {{relevant_content}}."
(3) If Instagram DM channel: POST to Messenger API endpoint
https://graph.facebook.com/v20.0/me/messages with recipient.id = brand's Instagram PSID.(4) If email channel: Gmail API
users.messages.send with HTML body.(5) Log to Outreach_Log: channel, sent_at, message_preview, response_status = "sent", follow_up_date = today + 3.
Brands.status updated to "outreached", Brands.last_contacted = todayrequests (Messenger API), google-auth, googleapiclient (Gmail), openrouter (DeepSeek)Nurture
response_status = delivered AND follow_up_date <= todayPer record: check
touch_number.Touch 2 (day 3): "Hey [brand], just checking in, curious if this caught your eye."
Touch 3 (day 7): Value-add message including one specific audience stat (e.g., "My audience is 68% male, 25-34, active in combat sports weekly, sharing this in case it's useful for your targeting."). DeepSeek generates.
Touch 4 (day 14): "Last touch from me, happy to chat if timing's better down the road." Then sequence ends.
Send via same channel as touch 1. Log to Outreach_Log. Set
follow_up_date = null after touch 4. Update brand status = cold if no reply after touch 4.
follow_up_date set to next touch date (or null if sequence complete)apscheduler, same send infra as Stage 03response_status before every send. If a brand replied (any status including "not interested"), they must NOT receive any nurture touch. Add this as a hard check before every send in the nurture loop. Also check do_not_contact flag.Zoom Intelligence
C:/TristanZoom/ or downloaded from Zoom cloud APIFileCreatedEvent fires on new file.(2) Whisper transcription:
brand_name to existing Brands record (fuzzy match with thefuzz library).(5) Write to Deals table: brand_id, amount_discussed, status = recommended_status, objections_raised, next_action, next_action_date, zoom_transcript (full text), zoom_recording_path.
watchdog, openai-whisper, openrouter (claude-sonnet-4-6 with JSON mode), pyairtable, thefuzzDeal Router
amount_discussed < 15000 AND status = negotiating AND routed_to_boxer = false(2) If multiple boxers match: rank by Claude scoring ("Rank these boxers for this deal. Consider audience fit for [brand category] and follower count.").
(3) Calculate split:
split_amount = deal_amount × boxer.split_pct, boxer_earnings = deal_amount - split_amount.(4) DeepSeek generates offer email to top-ranked boxer: deal summary, brand name, amount, deliverables expected, split breakdown, 48-hour response window.
(5) Send via Gmail API from Tristan's account (or a separate Verity sending address).
(6) Create Boxer_Deals record: deal_id, boxer_id, split_amount, boxer_earnings, status = "offered", offer_expires_at = now + 48h.
(7) Set Deals.routed_to_boxer = true.
(8) APScheduler checks every 4h for expired offers (offer_expires_at < now AND status = "offered"). If expired: mark "rejected", move to next ranked boxer.
NEEDConfirm with Tristan: auto-route or require approval before offer goes out?
pyairtable, openrouter (for ranking + email gen), googleapiclient (Gmail)split_pct field per boxer OR a deal_type_overrides config dict. Don't assume flat rate.Fulfill
status = won and no Fulfillment records yet created- Reel: due = signed_date + 14 days, requirements = brand guidelines from deal
- Story set (3x): due = signed_date + 7 days
- Post: due = signed_date + 10 days
- Event appearance: due = event date from deal terms
due_date <= today + 7 AND status NOT IN (delivered, approved). Compile weekly summary. Send via Gmail API to Tristan (and his manager if applicable).
apscheduler, pyairtable, googleapiclient (Gmail)Instagram Safety Protocol
The line: Messenger API (official) for outreach, not browser automation. Account bans are existential risk for a 2M-follower creator.
response_status = opted_out and do_not_contact = true in Brands table. Never contact again.
On the PSID problem
The Messenger API requires a recipient's Page-Scoped User ID (PSID), not their Instagram handle. Brands that haven't interacted with Tristan's page don't have a PSID yet. Short-term: Phantombuster handles cold outreach (browser session DMs). Long-term: as brands reply and establish a thread, they get a PSID and move to the official API. Build a field in Brands (ig_psid) that gets populated when a brand first replies. Once populated, all future messages go through the API.
Testing + Go-Live Checklist
Each item must be manually verified before go-live. "The script ran without errors" is not verification.
Per-Stage Verification
- Stage 01: 90+ brands in Airtable across all 3 categories. At least 30 per category. All have instagram_handle or contact_email (not both required, but one is required).
- Stage 02: All brands have qualifier_score populated. At least 30 brands at outreach_ready status. Spot-check 5 scores manually against what you'd expect, if they look off, revisit the system prompt.
- Stage 03: Send 10 test DMs + 10 test emails to real brands. Confirm all 20 show up in Outreach_Log with correct channel, sent_at, and follow_up_date. Confirm brand status updated to "outreached". Confirm at least one email lands in a real inbox (test with a brand you have a contact at).
- Stage 04: Set a test brand's follow_up_date to today, run the scheduler manually. Confirm a follow-up message generates and sends. Confirm a new Outreach_Log entry is created. Confirm follow_up_date advances to next scheduled touch.
- Stage 05: Drop a test audio file (even a 2-minute call recording from your phone) into the watch folder. Confirm: transcription runs, Claude extracts fields, Deal record appears in Airtable with brand_id correctly matched, all extracted fields populated.
- Stage 06: Create a test Deal with amount = $10,000, status = negotiating, routed_to_boxer = false. Run the router. Confirm: Boxer_Deals record created, split_amount calculated correctly, offer email sent to boxer's email, Deals.routed_to_boxer = true.
- Stage 08: Set a test Deal to status = won. Confirm: Fulfillment records created for each deliverable type. Run the Monday summary job manually. Confirm summary email arrives in Tristan's inbox.
- Google Sheets sync: Run the gspread export manually. Open Tristan's Sheet. Confirm all three tabs updated with current Airtable data. Confirm nightly scheduled run fires (check next morning).
- End-to-end run: One brand through the entire pipeline: uncontacted → outreach_ready → outreached → (simulate reply) responded → deal_opened → (fake Zoom file) Deal record created → amount < $15k → routed to boxer → (manually) won → Fulfillment checklist created → weekly reminder fires. All Airtable records correct at each step.
- Handoff doc: One-pager delivered to Tristan: daily system behavior, his responsibilities (mark Won, drop Zoom files, check Airtable morning view), emergency contact if system breaks.
- Process restart: Windows Task Scheduler configured for all Python processes. Simulate a reboot. Confirm all processes restart automatically.
NEED Flags: Confirm at Discovery Call
These block implementation decisions
- NEEDIs Tristan's Instagram account a Business account (not personal, not Creator)? If not, the Messenger API path doesn't exist. Migration to Business account may be needed.
- NEEDIs his Instagram Business account linked to a Facebook Page where he's the admin? Required for Meta app approval.
- NEEDWhat is the current boxer network split percentage? Does it vary by boxer or deal type?
- NEEDDeal routing: does Tristan want to approve each boxer assignment before the offer goes out, or is fully automatic routing acceptable?
- NEEDZoom setup: paid plan or free? Cloud recordings enabled? Decision determines whether we use Zoom API or watchdog folder.
- NEEDTristan's Instagram audience demographics (age, gender split, location top-5). Required for Stage 02 qualifier system prompt. Pull from his Instagram Insights at or before the discovery call.
- NEEDWhat Gmail address does outreach go from? His personal Gmail, a team inbox, or a new Verity-managed address?
- NEEDDoes Tristan want email summaries of daily activity, or will he check Airtable directly? This determines whether the daily digest feature gets built.
- NEEDThe $15,000 routing threshold: confirmed or still a rough number? This is a hard-coded logic gate in Stage 06.