@apex-inc/mcp-server 0.3.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,240 @@
1
+ ---
2
+ name: apex-integration-cookbook
3
+ description: Recipes for integrating Apex into products — SDK wiring, form tracking, auth/signup events, Stripe lifecycle, communication triggers, and event naming conventions. Use when the user is adding Apex tracking to their product or wiring events to communications and experiments.
4
+ ---
5
+
6
+ # Apex integration cookbook (SDK)
7
+
8
+ Step-by-step recipes for integrating Apex via the `@apex-inc/sdk` package. For snippet-only sites or MCP-driven setup, see the full cookbook in `@apex-inc/mcp-server`.
9
+
10
+ ## SDK setup
11
+
12
+ ```typescript
13
+ import { init, track, identify, trackForm, shutdown } from "@apex-inc/sdk";
14
+
15
+ init({
16
+ projectKey: "YOUR_PROJECT_KEY",
17
+ apiUrl: "https://your-apex-url", // optional, defaults to relative /api
18
+ flushInterval: 10000, // ms between batch sends (default: 10s)
19
+ flushAt: 20, // batch size trigger (default: 20)
20
+ debug: false // log events to console
21
+ });
22
+ ```
23
+
24
+ Events are batched and sent to `POST /api/events` automatically. Call `flush()` before page unload if needed, or `shutdown()` to stop tracking.
25
+
26
+ ## Recipe 1: Signup and authentication
27
+
28
+ ```typescript
29
+ // On signup
30
+ track("signup_completed", { method: "email", plan: "free" });
31
+ identify(user.id, {
32
+ email: user.email,
33
+ name: user.name,
34
+ visitorId: getCookie("apex_vid") // stitch to pre-signup browsing
35
+ });
36
+
37
+ // On login
38
+ track("login", { method: "google" });
39
+ identify(user.id, { email: user.email });
40
+ ```
41
+
42
+ The `visitorId` trait links the anonymous marketing session to the authenticated user. Without it, pre-signup attribution is lost.
43
+
44
+ ## Recipe 2: Onboarding funnel
45
+
46
+ ```typescript
47
+ track("onboarding_step_completed", { step: "connect_data_source", stepNumber: 2, totalSteps: 5 });
48
+ track("onboarding_completed", { timeToComplete: elapsedMs });
49
+ ```
50
+
51
+ Events like `onboarding_completed` automatically match to active tenant communications when the trigger system is wired.
52
+
53
+ ## Recipe 3: Stripe subscription lifecycle
54
+
55
+ ```typescript
56
+ // In your Stripe webhook handler
57
+ track("subscription_created", { plan: "pro", amount: 9900, currency: "usd" });
58
+ identify(userId, { plan: "pro", lifecycleStage: "paying" });
59
+
60
+ // On cancellation
61
+ track("subscription_cancelled", { reason: "too_expensive" });
62
+ identify(userId, { lifecycleStage: "churned" });
63
+ ```
64
+
65
+ ## Recipe 4: Feature usage
66
+
67
+ ```typescript
68
+ track("feature_used", { feature: "export_report", format: "csv" });
69
+ track("milestone_reached", { milestone: "first_experiment_completed" });
70
+ ```
71
+
72
+ Use one event name with a `feature` property — not dozens of unique event names.
73
+
74
+ ## Recipe 5: Form tracking (shortcut)
75
+
76
+ ```typescript
77
+ trackForm({
78
+ email: formData.email,
79
+ formId: "contact-form",
80
+ action: "demo_request",
81
+ fields: { company: formData.company, role: formData.role }
82
+ });
83
+ ```
84
+
85
+ `trackForm` fires `form_submit` + `identify` in one call.
86
+
87
+ ## Recipe 6: Server-side events (Server Events API)
88
+
89
+ For backend events (webhooks, cron jobs, CRM milestones), use the **Server Events API**: `sendServerEvent` / `sendServerEvents` from `@apex-inc/sdk`. This is the canonical replacement for raw `fetch()` calls to `/api/events`.
90
+
91
+ ```typescript
92
+ import { sendServerEvent, newIdempotencyKey } from "@apex-inc/sdk";
93
+
94
+ // Stripe webhook example
95
+ await sendServerEvent(
96
+ { apiKey: process.env.APEX_API_KEY! }, // apex_sk_…
97
+ {
98
+ type: "purchase_completed",
99
+ email: customer.email,
100
+ visitorId: cookieJar.get("apex_vid"), // optional but improves stitching
101
+ data: { value: invoice.amount_paid / 100, currency: "USD", order_id: invoice.id },
102
+ },
103
+ { idempotencyKey: stripeEvent.id }, // dedupe on retry
104
+ );
105
+ ```
106
+
107
+ Why prefer the Server Events API over `fetch("/api/events")`:
108
+
109
+ - **Idempotency-key** — retries don't double-count.
110
+ - **clientType: "server"** — the Conversion Loop dashboard classifies events correctly.
111
+ - **Single endpoint** — `/api/v1/events` is the documented, OpenAPI'd integration surface.
112
+ - **Typed errors** — the wrapper throws on non-2xx with a clear `error.code` from the API.
113
+
114
+ For batches > 1 event, use `sendServerEvents(config, events, { idempotencyKey })` (max 100 per call).
115
+
116
+ For agent-driven testing through MCP, use the **`send_server_event`** tool — same endpoint, exposed as a single-call MCP tool with the idempotency key generated for you.
117
+
118
+ ## Event naming conventions
119
+
120
+ - **snake_case**: `signup_completed`, `feature_used`, `invoice_paid`
121
+ - Context in **properties**: `track("button_clicked", { surface: "pricing", label: "start_trial" })`
122
+ - For experiments, include `experimentId` and `variant` in properties
123
+ - One primary metric per experiment; secondary metrics as properties
124
+
125
+ ## Identity stitching checklist
126
+
127
+ 1. Call `identify(userId, { email, visitorId })` when the user becomes known
128
+ 2. `visitorId` = value of the browser `apex_vid` cookie
129
+ 3. Server-side: pass `traits.visitorId` to link to the browser session
130
+ 4. Without stitching, pre-signup attribution is lost
131
+
132
+ ## Partner Network patterns
133
+
134
+ ### Credit a partner for a self-serve signup
135
+
136
+ When a visitor arrives from an affiliate link (`?utm_source=affiliate&utm_campaign=<handle>`) and signs up on your site, the Apex snippet automatically stitches the referral to the resulting Contact. The conversion auto-credits on the next purchase event (MMP-096 wiring).
137
+
138
+ No SDK calls needed for basic attribution. Use `list_partner_program_members(programId)` to see who's actually earning.
139
+
140
+ ### Run a manual-approval program + vet applicants in a script
141
+
142
+ ```typescript
143
+ import { createManagementClient } from "@apex-inc/sdk/management";
144
+ const apex = createManagementClient({ projectKey, apiKey });
145
+
146
+ // Create the program
147
+ const program = await apex.createProgram({
148
+ name: "Vetted Partners",
149
+ vertical: "saas",
150
+ commissionStructure: { type: "revshare", percentage: 25, appliesTo: "subscription" },
151
+ visibility: "public",
152
+ approvalMode: "manual", // every applicant needs review
153
+ });
154
+
155
+ // ...applicants land in /admin/partners/applications for human review.
156
+ // The SDK doesn't wrap the approval flow — that's an admin-auth path.
157
+ // But merchants can monitor who's joined:
158
+ const members = await apex.listProgramMembers(program.id);
159
+ ```
160
+
161
+ ### Batch-approve the weekly payout queue
162
+
163
+ ```typescript
164
+ const queue = await apex.listPendingPayouts();
165
+ const olderThanThirtyDays = queue.buckets["30_60"].concat(queue.buckets["60_plus"]);
166
+ const result = await apex.approvePayouts(olderThanThirtyDays.map(c => c.id));
167
+ console.log(`Approved ${result.approved.length}, skipped ${result.skipped.length}`);
168
+ ```
169
+
170
+ **Approving does not fire Stripe transfers.** The next `run-payouts` cron cycle (per your `autoPayoutPolicy`) batches + fires. Design reason: reserve holdback window (90 days for NEW partners) has to elapse before transfer.
171
+
172
+ ### Give an enterprise partner a custom rate
173
+
174
+ ```typescript
175
+ await apex.updateMembershipRate("prof_acme", {
176
+ commissionStructure: { type: "revshare", percentage: 35, appliesTo: "subscription" },
177
+ reason: "Enterprise negotiation - Q4 contract",
178
+ });
179
+ ```
180
+
181
+ Overrides are sticky. Updating the program default later won't overwrite this partner's rate (only `applyProgramDefaultToMembers` can mass-update, and it respects sticky overrides).
182
+
183
+ ### Disclose fees accurately in your UI
184
+
185
+ ```typescript
186
+ const { fees } = await apex.getEffectiveFees();
187
+ // Show merchants what they're charged per fee kind.
188
+ // source: "merchant_override" means they have a custom (enterprise) rate.
189
+ ```
190
+
191
+ ## Recipe — Mobile build sources (TestFlight vs App Store)
192
+
193
+ Apex auto-detects which build a mobile event came from and routes accordingly. Use the same `projectKey` everywhere — there's no separate sandbox key to provision.
194
+
195
+ ### Detection (no SDK code required)
196
+
197
+ iOS uses StoreKit 2's `appStoreReceiptURL` to distinguish `app-store`, `testflight`, and `xcode-debug`. Android uses `PackageManager.getInstallSourceInfo()` (API 30+) to distinguish `play-production`, `play-internal`, and `sideloaded`, with `BuildConfig.DEBUG` overriding to `xcode-debug` for local builds.
198
+
199
+ ```typescript
200
+ import { Apex } from "@apex-inc/capacitor-plugin";
201
+
202
+ await Apex.initialize({ projectKey: "wsk_..." });
203
+ // That's it. The next event includes `releaseChannel` + `releaseChannelSource`.
204
+ ```
205
+
206
+ ### Override for Android closed-test tracks
207
+
208
+ iOS overrides are ignored (the receipt always wins). On Android, `getInstallerPackageName()` can't distinguish Play production from closed-test tracks, so you can supply a hint at init time:
209
+
210
+ ```typescript
211
+ import { Apex } from "@apex-inc/capacitor-plugin";
212
+ import { ApexChannel } from "./build-config"; // BuildConfig.APEX_CHANNEL
213
+
214
+ await Apex.initialize({
215
+ projectKey: "wsk_...",
216
+ releaseChannel: ApexChannel, // "play-internal" for closed-test variant
217
+ });
218
+ ```
219
+
220
+ Use Gradle `buildConfigField` to inject the channel per build variant so the value is wired at compile time, not runtime.
221
+
222
+ ### Billing implications
223
+
224
+ - **Dev** (Xcode-debug, Gradle-debug, sideloaded): always **free**.
225
+ - **Beta** (TestFlight, Play internal/closed): always **free**.
226
+ - **Production** (App Store, Play production): **billable**.
227
+ - **Unknown** (older SDK without detection): billable, but the dashboard surfaces a fix-it nudge with an SDK upgrade prompt.
228
+
229
+ No retroactive credits — if you misclassify a build, the fix takes effect for future events.
230
+
231
+ ### When NOT to recommend a separate project
232
+
233
+ Some MMP vendors require two project keys (sandbox + live). Apex does not. If a merchant asks "should I use a different project for beta?" or "do I need a sandbox key?", the answer is **no**: point to `/docs/mobile/test-vs-production` and walk them through the **Build environments** card in Settings → Mobile apps.
234
+
235
+ ## Related
236
+
237
+ - Event tracking fundamentals: see **apex-growth-tracking** skill
238
+ - Experiment design: see **apex-experimentation** skill
239
+ - Communication setup: see **apex-communications** skill
240
+ - Affiliate / partner programs: see **apex-partner-network** skill
@@ -0,0 +1,169 @@
1
+ ---
2
+ name: apex-journeys
3
+ description: Help users design, audit, and ship Adaptive Journeys — multi-step lifecycle sequences with waits, branches, and exit rules. Use whenever the user mentions journeys, cart abandonment, onboarding sequences, drip campaigns, or asks to add/remove an exit rule. Always run alongside apex-communications for any cart-recovery, lifecycle, or sequence work.
4
+ ---
5
+
6
+ # Apex Journeys (MCP)
7
+
8
+ Journeys are multi-step communication sequences with waits, branches, and exit rules. A single `send_email` is a Communication; a `send_push → wait 1h → send_email → wait 24h → send_email` is a Journey.
9
+
10
+ The single hardest thing about journeys is **knowing when to stop**. The vast majority of lifecycle programs that look broken to end users are journeys that fired a "you forgot something" message after the user already came back. This skill makes exit rules a first-class concern.
11
+
12
+ ## When to Activate
13
+
14
+ - User says "journey", "sequence", "drip", "lifecycle program", "cart abandonment", "abandoned cart recovery"
15
+ - User wants to chain multiple messages with delays between them
16
+ - User is editing or auditing exit rules
17
+ - User mentions branching on subject state (cart contents, plan tier, lifecycle stage)
18
+ - User just created a marketing communication and you sense it should be part of a sequence
19
+
20
+ ## Tool Map
21
+
22
+ | Tool | When to use |
23
+ |---|---|
24
+ | `list_journeys` | Browse all journeys with their exit-rule counts. Start here for any audit or "what do I have?" question. |
25
+ | `get_journey` | Read full configuration for a specific journey (steps, exit rules, goal). |
26
+ | `list_journey_exits` | Read only the exit rules for one journey. Use when the user asks "what triggers an exit for this?" |
27
+ | `add_journey_exit` | Add an exit rule to a journey draft. Use when the user (or you proactively) decides a journey needs a new exit trigger. |
28
+ | `remove_journey_exit` | Remove an exit rule from a journey draft. |
29
+ | `audit_journey_exits` | Sweep across all published marketing journeys and find ones missing purchase exits. Run this proactively when a user mentions journeys, cart recovery, or marketing programs. |
30
+ | `get_contact_cart` | Inspect a contact's current cart state to debug cart-recovery journeys or branch.condition evaluations. |
31
+
32
+ ## Core Concepts to Convey
33
+
34
+ ### A journey is just steps with edges
35
+
36
+ Each step is one of: `send_email`, `send_push`, `send_sms`, `wait` (`fixed` or `until_event`), `branch.condition`, `goal`, or `exit`. Edges describe which step runs next. The runtime (a Step Functions state machine per execution) advances pointers and emits events.
37
+
38
+ ### Exit Events are journey-level, not step-level
39
+
40
+ Exit rules are declared once at the journey level and apply to every in-flight execution. When a matching event fires for the executing subject, the matcher halts the execution **regardless of which step it was on**.
41
+
42
+ ```ts
43
+ journey.exitEvents = [
44
+ {
45
+ id: "exit-cart-purchased",
46
+ eventName: "in_app_purchase",
47
+ description: "Stop messaging if the user completes a purchase",
48
+ enabled: true,
49
+ },
50
+ {
51
+ id: "exit-cart-cleared",
52
+ eventName: "cart_snapshot",
53
+ guard: { kind: "compare", path: "itemCount", op: "==", value: 0 },
54
+ description: "Stop messaging if the cart was emptied (not via purchase)",
55
+ enabled: true,
56
+ },
57
+ ];
58
+ ```
59
+
60
+ When teaching users, this is the mental model:
61
+
62
+ > "A journey starts because something happened. An exit fires when the reason for the journey no longer exists."
63
+
64
+ ### Send-time gate is authoritative
65
+
66
+ Even if the stream-based exit matcher misses an event (race conditions are rare but possible), every `send_*` step re-checks exit rules immediately before firing. This is the guarantee Apex makes to its users: **if an exit event has fired by the moment we send, we will not send**.
67
+
68
+ You can mention this to users; it removes a class of "what if the event arrived 2ms before send?" worries.
69
+
70
+ ### Counters that prove it works
71
+
72
+ `/api/journeys/[id]/calibrated-impact` exposes:
73
+
74
+ - `triggered_exposed_exited_before_send` — how many people exited before any message went out. **A healthy cart-recovery journey will have this > 0**: it's the number of customers Apex protected from a stale message.
75
+ - `converted_exposed_messaged` — converted users who received at least one message. This is the honest "did the message contribute?" denominator.
76
+ - `messagedLift` — calibrated lift restricted to messaged subjects.
77
+
78
+ When discussing journey performance, lead with `messagedLift`, not raw conversion.
79
+
80
+ ## Tool Invocation Order
81
+
82
+ ### "Set up cart abandonment recovery"
83
+
84
+ This is the canonical journey-skill workflow. **Always involve apex-communications** for the payloads.
85
+
86
+ ```
87
+ 1. list_journeys → check if a cart-recovery journey already exists
88
+ 2. If not, recommend the mobile-cart-recovery template (it's seeded in Apex)
89
+ 3. Ensure communications exist: hand off to apex-communications.recommend_communications + generate_communications for the cart-recovery push and email
90
+ 4. Confirm exit rules: add_journey_exit for in_app_purchase, cart_snapshot itemCount==0, and checkout_completed
91
+ 5. list_journey_exits to confirm
92
+ 6. Walk the user through the apex.trackCart helpers their app needs to call (see apex-growth-tracking for the SDK shape)
93
+ ```
94
+
95
+ ### "Audit my journeys for missing exits"
96
+
97
+ ```
98
+ 1. audit_journey_exits(active_only=true)
99
+ 2. For each missing_purchase_exit row: walk the user through add_journey_exit
100
+ 3. Re-run audit_journey_exits to confirm all journeys are green
101
+ ```
102
+
103
+ ### "Why didn't this user get my cart-recovery push?"
104
+
105
+ ```
106
+ 1. get_contact_cart(contact_id=...) → check current cart state (itemCount, lastEventId)
107
+ 2. If cart.itemCount === 0, the exit rule fired — that's correct behavior
108
+ 3. If cart still has items, get_journey(journey_id) and verify the wait duration + exit rules
109
+ 4. Check calibrated-impact for triggered_exposed_exited_before_send count
110
+ ```
111
+
112
+ ### "Add a new exit condition to onboarding"
113
+
114
+ ```
115
+ 1. list_journeys → find the onboarding journey id
116
+ 2. list_journey_exits(journey_id) → see what already exits
117
+ 3. add_journey_exit({journey_id, event_name: "onboarding_completed", description: "User finished setup before wait expired"})
118
+ ```
119
+
120
+ ## Proactive Behavior
121
+
122
+ ### When the user creates a marketing or lifecycle communication
123
+
124
+ After helping a user generate a single communication, ask whether it's part of a sequence:
125
+
126
+ > "This 'cart abandoned' push is a single send. Most cart-recovery programs are a sequence: push at 30 min → email at 2 hr → final reminder at 24 hr. Want me to wire this as a journey so it doesn't fire for users who already bought?"
127
+
128
+ ### When the user mentions a wait, delay, or "after X hours"
129
+
130
+ Anything with a wait needs exit rules. Don't ship a journey without them.
131
+
132
+ > "I set up the journey with a 1-hour wait before the next message. Before we publish, let me add exit rules so we stop messaging anyone whose cart is purchased or cleared during that hour."
133
+
134
+ ### When auditing existing programs
135
+
136
+ Run `audit_journey_exits` proactively at the start of any conversation about journeys, marketing programs, or cart abandonment. If anything returns missing, surface it before doing anything else.
137
+
138
+ ### When the user asks "should I send this?"
139
+
140
+ If the answer involves a wait, the real question is "what should stop this from sending?" Reframe the conversation toward exit rules.
141
+
142
+ ## Common Mistakes to Catch
143
+
144
+ | Mistake | What to say |
145
+ |---|---|
146
+ | Cart-recovery journey with no exit on purchase | "Add an exit on `in_app_purchase`. Without it you'll message buyers after they bought." |
147
+ | `wait.fixed` for cart abandonment | "Use `wait.until_event` with a deadline. That way the wait ends as soon as the user comes back, not on a fixed timer." |
148
+ | Exit rule with no `guard` on `cart_snapshot` | "`cart_snapshot` fires whenever the cart changes, including adds. Add a guard like `itemCount == 0` so you only exit on a cleared cart." |
149
+ | Same exit rule on multiple journeys, hand-managed | "Apex doesn't share exit rules across journeys, but the underlying event matches the same. Each journey gets its own copy — that's fine." |
150
+ | Disabling all exit rules to "always send" | "If you want to always send, remove the wait. Don't disable exits and keep the wait — you'll re-create the original problem." |
151
+
152
+ ## Glossary
153
+
154
+ - **AdaptiveJourney**: A draft/published journey with steps and exit rules.
155
+ - **ExitEventRule**: One rule on `journey.exitEvents`. Has `eventName`, optional `guard` predicate, `description`, `enabled`.
156
+ - **JourneyExecutionPointer**: Server-side record of an in-flight execution for one contact.
157
+ - **wait.until_event**: A wait step that ends when a named event fires (or a deadline expires).
158
+ - **branch.condition**: A step that routes execution based on a predicate over subject attributes (e.g., `cart.itemCount >= 2`).
159
+ - **Send-time gate**: The authoritative re-check that runs immediately before any send step.
160
+
161
+ ## Resources
162
+
163
+ - `apex://journeys` — current journeys list with exit-rule counts (if available as an MCP resource)
164
+
165
+ ## See Also
166
+
167
+ - **apex-communications** — for the per-message payloads (subject, body, channel) the journey fires
168
+ - **apex-growth-tracking** — for the `apex.trackCart` SDK helpers that emit the events journeys consume and exit on
169
+ - **apex-experimentation** — to A/B test variants within a journey step
@@ -0,0 +1,191 @@
1
+ ---
2
+ name: apex-partner-network
3
+ description: Build, run, and grow an affiliate / referral program on the Apex Partner Network. Use when a merchant wants to pay partners a commission for driving installs / purchases / subscriptions, manage a roster of affiliates, or invite specific people to promote their product. The SDK wraps programs, memberships, invitations, payouts, fee disclosure, and merchant vouching.
4
+ ---
5
+
6
+ # Apex Partner Network (SDK)
7
+
8
+ The Apex Partner Network is a two-sided affiliate network: merchants own programs, partners join one or many programs across every merchant they work with, Apex facilitates the money rail between them via Stripe Connect.
9
+
10
+ This skill covers the **merchant-facing management SDK** — programmatic access from a Node.js backend using an `apex_sk_*` service key. Partner-facing flows (self-signup, dashboard, Stripe Connect OAuth consent) happen in the hosted portal at `partners.apex.inc`.
11
+
12
+ ## Core mental model
13
+
14
+ | Concept | What it is |
15
+ |---|---|
16
+ | **Program** | A merchant-owned commission offer (CPA or RevShare). Merchants can run multiple. |
17
+ | **Membership** | One partner × one program. Carries the partner's current commission (and its audit history). |
18
+ | **Conversion** | A tracked install/purchase/subscription credited to a partner via their Apex link. |
19
+ | **HoldbackEntry** | One per approved conversion. Splits commission into available + held portions per the partner's trust tier. |
20
+ | **MerchantPayoutBatch** | A settled cycle of all released-available commissions for one partner, fired per the merchant's `autoPayoutPolicy`. |
21
+
22
+ ## SDK bootstrap
23
+
24
+ ```typescript
25
+ import { createManagementClient } from "@apex-inc/sdk/management";
26
+
27
+ const apex = createManagementClient({
28
+ projectKey: "your-project-key",
29
+ apiKey: process.env.APEX_API_KEY!,
30
+ apiUrl: "https://app.apex.inc", // omit for local dev
31
+ });
32
+ ```
33
+
34
+ All calls below go through this client. Under the hood, requests attach `x-api-key` + `x-apex-project` headers to every `/api/mobile/affiliates/*` route.
35
+
36
+ ## Creating your first program
37
+
38
+ ```typescript
39
+ const program = await apex.createProgram({
40
+ name: "Partner Program",
41
+ description: "Refer a customer, earn 20% for 12 months.",
42
+ vertical: "saas",
43
+ commissionStructure: {
44
+ type: "revshare",
45
+ percentage: 20,
46
+ appliesTo: "subscription",
47
+ },
48
+ visibility: "public", // shows in /marketplace for partner discovery
49
+ approvalMode: "manual", // review each applicant
50
+ });
51
+ ```
52
+
53
+ Programs can be `paused`/`archived` later via `updateProgram(id, { status: "paused" })`. Pausing freezes new applications + new conversions but keeps existing memberships alive.
54
+
55
+ ## Inviting a partner
56
+
57
+ ```typescript
58
+ await apex.invitePartner({
59
+ email: "sarah@example.com",
60
+ programId: program.id,
61
+ inviterName: "Alice from Acme",
62
+ inviterMessage: "I'd love to partner with you — here's a special rate.",
63
+ customRate: {
64
+ commissionStructure: {
65
+ type: "revshare",
66
+ percentage: 30, // negotiated higher rate
67
+ appliesTo: "subscription",
68
+ },
69
+ },
70
+ });
71
+ ```
72
+
73
+ Sarah gets a magic-link email. If she's already a partner on Apex, the invite creates a new `AffiliateMembership` bound to this program. If she's new, she claims a handle + completes onboarding at the portal.
74
+
75
+ **Handle rule**: one `@sarah` works across every merchant program she joins. She doesn't need a separate account per merchant.
76
+
77
+ ## Adjusting commission rates
78
+
79
+ Three commission sources are possible:
80
+ - `program_default` — set by the program; member has never been individually adjusted
81
+ - `membership_override` — explicit override set on this membership (sticky; survives program default updates)
82
+ - `invite_override` — set at invite time (sticky same as membership_override)
83
+
84
+ ```typescript
85
+ // Bump this specific partner to 35%
86
+ await apex.updateMembershipRate("prof_sarah", {
87
+ commissionStructure: {
88
+ type: "revshare",
89
+ percentage: 35,
90
+ appliesTo: "subscription",
91
+ },
92
+ reason: "Volume performer — Q3 bump",
93
+ });
94
+
95
+ // ...later, reset them back to program default
96
+ await apex.resetMembershipRate("prof_sarah", "Q3 promo ended");
97
+ ```
98
+
99
+ Updating a program's default does NOT retroactively change existing memberships — call `applyProgramDefaultToMembers(programId)` to fan it out (skips overrides).
100
+
101
+ ## Approving payouts
102
+
103
+ Commissions sit as `pending` until approved. The full batch flow:
104
+
105
+ ```typescript
106
+ // 1. What's owed
107
+ const queue = await apex.listPendingPayouts();
108
+ // {
109
+ // buckets: { "0_30": [...], "30_60": [...], "60_plus": [...] },
110
+ // totals: { "0_30": 1450.00, ... },
111
+ // effectivePolicy: { policy: "manual", source: "project", capUsd: undefined },
112
+ // caps: { daily: undefined }
113
+ // }
114
+
115
+ // 2. Approve (creates HoldbackEntry + transitions to "approved")
116
+ const result = await apex.approvePayouts(
117
+ queue.buckets["0_30"].map(c => c.id),
118
+ );
119
+ // { approved: [{ id, heldbackAmountUsd }], skipped: [...], failed: [...] }
120
+ ```
121
+
122
+ **Approving does NOT fire the Stripe transfer**. The transfer happens at the merchant's `autoPayoutPolicy` cycle (daily / weekly / manual). This decoupling:
123
+
124
+ - lets the merchant review multiple conversions before committing
125
+ - lets the platform batch per (merchant, partner) to minimize Stripe fees
126
+ - gives the holdback release window (90d for NEW partners, 30d for VERIFIED) time to run
127
+
128
+ ## Fee disclosure
129
+
130
+ Merchants should always know what Apex charges them:
131
+
132
+ ```typescript
133
+ const { fees } = await apex.getEffectiveFees();
134
+ // fees: {
135
+ // facilitation_payout: { bps: 25, flatUsd: 0.5, source: "platform_default", isCustom: false },
136
+ // identity_verification: { flatUsd: 2.5, source: "platform_default", isCustom: false },
137
+ // instant_payout: { bps: 100, flatUsd: 0.5, source: "platform_default", isCustom: false },
138
+ // }
139
+ ```
140
+
141
+ Enterprise deals show `isCustom: true` with `source: "merchant_override"`. Admin edits propagate within 60 seconds (cache TTL).
142
+
143
+ ## Vouching for a partner
144
+
145
+ A merchant who knows a partner outside Apex can vouch for them — bumping their trust tier one level for 60 days. This unlocks higher payout caps for the partner immediately. It's NOT free: the merchant takes on indemnity.
146
+
147
+ ```typescript
148
+ await apex.vouchForPartner("prof_sarah", {
149
+ indemnityAcknowledged: true, // MUST be literally true
150
+ reason: "10-year professional relationship, verified off-platform",
151
+ });
152
+ ```
153
+
154
+ **Important**: if the partner commits fraud during the 60-day vouched window, the merchant's account is liable for clawbacks the reserve can't cover. Use sparingly.
155
+
156
+ ## What happens when things fail
157
+
158
+ **Partner reaches a cap**: the batch surfaces a tier-aware message. NEW-tier partners see "processing" (masked); TRUSTED/VERIFIED partners see the reason.
159
+
160
+ **Merchant's Stripe balance is short**: retry fires in 24h. After 48h (2nd fail), the merchant gets an email. After 72h (3rd fail), a dashboard card appears + partner messaging flips. After 7 days (7th fail), Apex opens a support case — **not labeled as fraud** — for human triage.
161
+
162
+ Automation never freezes a merchant's `autoPayoutPolicy`. That's always a human decision.
163
+
164
+ ## Reading auto-payout policy
165
+
166
+ Set per-project or per-program:
167
+
168
+ ```typescript
169
+ await apex.updateProgram(programId, {
170
+ autoPayoutPolicy: "auto_under_cap", // auto-pay up to cap, manual above
171
+ autoPayoutCapUsd: 1000,
172
+ });
173
+ ```
174
+
175
+ Policies:
176
+ - `"manual"` — every payout needs merchant click-to-approve (default)
177
+ - `"auto"` — approved conversions auto-queue for the next payout cycle
178
+ - `"auto_under_cap"` — auto under `autoPayoutCapUsd`, queue above
179
+
180
+ ## What this SDK does NOT cover
181
+
182
+ - **Partner-facing flows** (self-signup at `/apply`, portal dashboard, Stripe Connect Express onboarding) — these happen in the hosted portal + require Cognito auth, not API key auth.
183
+ - **Admin / Fraud Ops** (case queue, appeal decisions, ban list, fee policy edits) — admin Cognito flow, different tooling.
184
+ - **Webhooks** (Stripe Connect events, identity verification status) — those are inbound from Stripe, not outbound from the SDK.
185
+ - **Real-time Stripe transfers** — transfers fire on the merchant's schedule via the platform's payout orchestrator, not synchronously from the SDK.
186
+
187
+ ## See also
188
+
189
+ - `apex-integration-cookbook` — end-to-end patterns combining partner network + communications + experiments
190
+ - MCP tools: `list_partner_programs`, `create_partner_program`, `invite_partner`, `approve_payouts`, `vouch_for_partner`, `get_effective_fees`
191
+ - Docs: `/docs/partner-network/` for concept walkthroughs + integration guides