@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.
- package/README.md +13 -119
- package/dist/api-client.d.ts +33 -0
- package/dist/api-client.d.ts.map +1 -1
- package/dist/api-client.js +87 -4
- package/dist/api-client.js.map +1 -1
- package/dist/index.js +13 -6
- package/dist/index.js.map +1 -1
- package/dist/resources.d.ts +26 -0
- package/dist/resources.d.ts.map +1 -1
- package/dist/resources.js +96 -0
- package/dist/resources.js.map +1 -1
- package/dist/tools.d.ts +873 -0
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +909 -5
- package/dist/tools.js.map +1 -1
- package/package.json +10 -8
- package/skills/apex-communications/SKILL.md +127 -0
- package/skills/apex-experimentation/SKILL.md +9 -0
- package/skills/apex-growth-intelligence/SKILL.md +116 -0
- package/skills/apex-growth-tracking/SKILL.md +59 -3
- package/skills/apex-integration-cookbook/SKILL.md +240 -0
- package/skills/apex-journeys/SKILL.md +169 -0
- package/skills/apex-partner-network/SKILL.md +191 -0
|
@@ -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
|