tellatribe

Enter the access code to read the project documents.

tellatribe Docs Status: All modules built and live · Oct 19: Creator Campaigns Part B shipped (briefs, applications, deals, verification → bounties, brand balance + Stripe funding, disputes, reports) on top of the tax-identity gate and annual statements · awaiting owner inputs: tellatribe Stripe account (payments + Connect + funding webhook), HealthyApp webhook secret, Ayrshare key

Reference · packages/campaigns/README.md

@affiliate-coach/campaigns

The Creator Campaigns engine: everything between a brand's brief and the bounty row the payout run pays. Pure TypeScript over the ledger's Sql; no HTTP, no UI. The admin, portal and API call it; nothing else writes the campaign tables.

The contract it was built against is build-plan/04-creator-campaigns.md; the requirements are PRD-Creator-Campaigns.md Part B; the decisions are 55–60 and 67–106 in DECISIONS-FOR-REVIEW.md. Schema: 0017_campaigns, 0018_funding_endpoint, 0019_campaigns_vocabulary.

The shape of a campaign

brief (campaign + deliverables)
  └─ application (snapshot frozen at apply)
       └─ deal (frozen SOW; escrow held from the brand balance)
            └─ task per deliverable: draft → review → post → verify
                 └─ bounty commission row  →  payout run (packages/payouts)
  • Brand balance is an append-only ledger per brand (brand_balance_entries). Funding in; escrow_hold at acceptance; at verification an escrow_release for the consumed hold beside the bounty and the platform_fee. brandBalance() returns freeCents (Σ entries), reservedCents (open holds − releases) and balanceCents (free + reserved). "Can this brand pay" reads freeCents.
  • Gift value lives on the deal and is reported on the 1099 total; it never becomes a commission row.
  • Eligibility is evaluated here, in the app, so a screen can show an ineligible brief dimmed with its one plain-words reason.

Modules

File What it holds
rules.ts Pure: tierFor, evaluateEligibility (one reason), qualityScore (0–100 with reasons; good / check / weak), rateGuidance, paysWords ("$150 + 25%"), asksWords ("1 Reel · by Nov 2"), sowMarkdown, businessDaysAfter
read.ts loadCampaign, creatorFacts, brandBalance, row loaders
brand.ts Brief lifecycle (createCampaign → publishCampaign → closeCampaign), invites, applicants (shortlist, counterOffer, decline, accept → deal + tasks + enrollment + escrow), fundWaitingDeals, reviewDraft, autoApproveStaleDrafts (10 business days), approveUnverifiable, cancelDeal, decideDispute, recordFunding (idempotent on the Stripe ref), refundFunding
creator.ts upsertCreatorProfile, apply, withdraw, respondToCounter, acceptSow, submitDraft, acknowledgeDisclosure, markPosted (post window enforced), openDispute, appealDispute
verify.ts verifyTask (tags, handles, disclosure, label → bounty + brand spend + rights grant), verificationSweep, captureMetrics, markDealsPaid, flagDeletedPosts, rightsReminders; the PostLookup port the API binds to the aggregator
reputation.ts creatorReputation ("4 deals · 100% on time"), brandReputation
errors.ts CampaignError with its code union; messages are written to be shown

Every state-changing call writes an audit_log row and sets the ledger trigger context (affiliate_coach.source = 'campaigns') before touching commissions.

Using it

import { apply, accept, verifyTask, brandBalance } from '@affiliate-coach/campaigns';

const { applicationId } = await apply(sql, { workspaceId, affiliateId, campaignId, pitch: 'Three reels a week, mostly mornings.' });
const { dealId, status } = await accept(sql, { workspaceId, actorUserId, applicationId, flatFeeCents: 15000n });
// status is 'funded' when the brand's free balance covered the fee, else 'unfunded' (the Queue says so; fundWaitingDeals() retries)

Errors are CampaignError instances; check err.code (for example ineligible, not_open, bid_over_cap, outside_window, balance_short) and show err.message.

Tests

npx vitest run in this package: 37 tests on PGlite through @affiliate-coach/db/test-harness, with test/support.ts building a brief, a funded brand and a creator profile. npx tsc --noEmit is clean.

Rendered from packages/campaigns/README.md. Edit the source, not this page.