@seldonframe/mcp 1.0.4 → 1.0.5
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/package.json +2 -2
- package/src/client.js +12 -0
- package/src/tools.js +260 -16
- package/src/welcome.js +3 -3
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@seldonframe/mcp",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "MCP server for SeldonFrame — AI-native Business OS platform. One command creates a real hosted workspace with CRM, booking, intake, and
|
|
3
|
+
"version": "1.0.5",
|
|
4
|
+
"description": "MCP server for SeldonFrame — AI-native Business OS platform. One command creates a real hosted workspace with CRM, booking page, intake form, and AI agents. v1.0.5: collect_operator_email tool closes the onboarding loop (welcome email + lead capture); auto-injects x-org-id from workspace bearer for ALL tool calls (fixes list_contacts / list_deals / list_bookings / send_welcome_email); structured Soul-seed fields on create_workspace (phone, services, testimonials) so the landing page renders real data on the first GET; operator-facing labels replace internal slugs in responses.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"seldonframe-mcp": "src/index.js"
|
package/src/client.js
CHANGED
|
@@ -139,6 +139,18 @@ export async function api(method, path, opts = {}) {
|
|
|
139
139
|
};
|
|
140
140
|
if (auth) headers.Authorization = auth;
|
|
141
141
|
|
|
142
|
+
// May 1, 2026 — inject x-org-id from the resolved workspace for
|
|
143
|
+
// EVERY tool call. Several CRM endpoints (contacts, deals,
|
|
144
|
+
// bookings, activities) use guardApiRequest which strictly
|
|
145
|
+
// requires x-org-id. Without this header, the bearer token alone
|
|
146
|
+
// returns 400 "Missing x-org-id" even though the bearer is
|
|
147
|
+
// workspace-scoped. Tools can opt out by passing
|
|
148
|
+
// `extra_headers: { "x-org-id": null }` if a route needs the
|
|
149
|
+
// header omitted, but the default is to send it.
|
|
150
|
+
if (workspace_id && !("x-org-id" in headers) && !("X-Org-ID" in headers)) {
|
|
151
|
+
headers["x-org-id"] = workspace_id;
|
|
152
|
+
}
|
|
153
|
+
|
|
142
154
|
const res = await fetch(`${API_BASE}${path}`, {
|
|
143
155
|
method,
|
|
144
156
|
headers,
|
package/src/tools.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
api,
|
|
3
|
+
API_INFO,
|
|
3
4
|
fetchText,
|
|
4
5
|
forgetWorkspace,
|
|
5
6
|
htmlToText,
|
|
@@ -41,18 +42,61 @@ export const TOOLS = [
|
|
|
41
42
|
{
|
|
42
43
|
name: "create_workspace",
|
|
43
44
|
description:
|
|
44
|
-
"Create a real, hosted workspace on <slug>.app.seldonframe.com with CRM,
|
|
45
|
+
"Create a real, hosted workspace on <slug>.app.seldonframe.com with a CRM, booking page, intake form, and AI agents pre-installed. The first workspace requires no API key. When the user mentions a phone, email, address, list of services, or testimonials, pass them as structured fields so the landing page renders with their real content immediately. Example: create_workspace({ name: 'Desert Cool HVAC', phone: '(602) 555-0188', services: [{ name: 'AC Repair' }, { name: 'Heating' }] })",
|
|
45
46
|
inputSchema: obj(
|
|
46
47
|
{
|
|
47
48
|
name: str("Human-readable workspace name."),
|
|
48
|
-
source: str("Optional URL or description to seed the
|
|
49
|
+
source: str("Optional URL or description to seed the business profile from."),
|
|
50
|
+
phone: str("Operator's business phone (any format — we render as-is). Renders in nav, hero, and footer when set."),
|
|
51
|
+
email: str("Optional contact email surfaced in the landing footer."),
|
|
52
|
+
address: str("Optional business address. Comma-separated street, city, region, postal, country renders as-is."),
|
|
53
|
+
tagline: str("Short hero tagline (one line)."),
|
|
54
|
+
business_description: str("One paragraph about the business (used for hero subhead + about section)."),
|
|
55
|
+
services: {
|
|
56
|
+
type: "array",
|
|
57
|
+
description:
|
|
58
|
+
"List of services / products / offerings the business provides. Renders as the services grid on the landing page. Each item: { name, description? }.",
|
|
59
|
+
items: {
|
|
60
|
+
type: "object",
|
|
61
|
+
properties: {
|
|
62
|
+
name: { type: "string" },
|
|
63
|
+
description: { type: "string" },
|
|
64
|
+
},
|
|
65
|
+
required: ["name"],
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
testimonials: {
|
|
69
|
+
type: "array",
|
|
70
|
+
description:
|
|
71
|
+
"Optional customer testimonials to seed the landing page's testimonials section. Each item: { quote, name?, role?, company? }.",
|
|
72
|
+
items: {
|
|
73
|
+
type: "object",
|
|
74
|
+
properties: {
|
|
75
|
+
quote: { type: "string" },
|
|
76
|
+
name: { type: "string" },
|
|
77
|
+
role: { type: "string" },
|
|
78
|
+
company: { type: "string" },
|
|
79
|
+
},
|
|
80
|
+
required: ["quote"],
|
|
81
|
+
},
|
|
82
|
+
},
|
|
49
83
|
},
|
|
50
84
|
["name"],
|
|
51
85
|
),
|
|
52
86
|
handler: async (args) => {
|
|
53
87
|
const firstEver = isFirstEverCall();
|
|
54
88
|
const result = await api("POST", "/workspace/create", {
|
|
55
|
-
body: {
|
|
89
|
+
body: {
|
|
90
|
+
name: args.name,
|
|
91
|
+
source: args.source ?? null,
|
|
92
|
+
phone: args.phone ?? null,
|
|
93
|
+
email: args.email ?? null,
|
|
94
|
+
address: args.address ?? null,
|
|
95
|
+
tagline: args.tagline ?? null,
|
|
96
|
+
business_description: args.business_description ?? null,
|
|
97
|
+
services: args.services ?? null,
|
|
98
|
+
testimonials: args.testimonials ?? null,
|
|
99
|
+
},
|
|
56
100
|
allow_anonymous: true,
|
|
57
101
|
});
|
|
58
102
|
const ws = result.workspace ?? result;
|
|
@@ -94,13 +138,41 @@ export const TOOLS = [
|
|
|
94
138
|
: null,
|
|
95
139
|
urls: publicUrls,
|
|
96
140
|
public_urls: result.public_urls ?? publicUrls,
|
|
97
|
-
|
|
141
|
+
// May 1, 2026 — installed-block labels translated to operator
|
|
142
|
+
// copy. The MCP-internal slugs ("caldiy-booking",
|
|
143
|
+
// "formbricks-intake", "brain-v2") are deployment details no
|
|
144
|
+
// operator should need to learn — surfacing them on every
|
|
145
|
+
// create_workspace turn was friction without payoff.
|
|
146
|
+
installed: (result.installed ?? [
|
|
147
|
+
"crm",
|
|
148
|
+
"caldiy-booking",
|
|
149
|
+
"formbricks-intake",
|
|
150
|
+
"brain-v2",
|
|
151
|
+
]).map((slug) => {
|
|
152
|
+
if (slug === "caldiy-booking") return "Booking page";
|
|
153
|
+
if (slug === "formbricks-intake") return "Intake form";
|
|
154
|
+
if (slug === "brain-v2") return "AI learning";
|
|
155
|
+
if (slug === "crm") return "CRM";
|
|
156
|
+
return slug;
|
|
157
|
+
}),
|
|
158
|
+
// May 1, 2026 — email collection promoted to the first
|
|
159
|
+
// suggested next step. Per the onboarding-loop spec, the
|
|
160
|
+
// operator gets a permanent inbox record (admin URLs,
|
|
161
|
+
// landing/booking/intake links) AND we earn a CRM lead in
|
|
162
|
+
// SeldonFrame's own workspace, so every workspace creation
|
|
163
|
+
// becomes a tracked relationship.
|
|
164
|
+
prompt_for_email: {
|
|
165
|
+
message:
|
|
166
|
+
"What's your email? I'll send you a summary with all the links above (so you don't lose them) and use it for your admin login when you upgrade. — calls collect_operator_email({ email, name? })",
|
|
167
|
+
tool: "collect_operator_email",
|
|
168
|
+
},
|
|
98
169
|
next: [
|
|
99
170
|
adminUrl
|
|
100
171
|
? `⚡ Admin Dashboard (bookmark this!): ${adminUrl} (paste into your browser; no signup needed; token expires in 7 days)`
|
|
101
172
|
: null,
|
|
102
|
-
"
|
|
103
|
-
"
|
|
173
|
+
"Ask the user for their email, then call collect_operator_email({ email, name? }) — sends the welcome email + creates a permanent record. Do this BEFORE any further customization.",
|
|
174
|
+
"install_vertical_pack({ pack: '<industry>' }) — set up an industry template (real-estate, dental, legal, …)",
|
|
175
|
+
"fetch_source_for_soul({ url: 'https://yoursite.com' }) → submit_soul({ soul }) — pull the business profile from a website",
|
|
104
176
|
"get_workspace_snapshot({}) — read workspace state to reason about next steps",
|
|
105
177
|
].filter(Boolean),
|
|
106
178
|
};
|
|
@@ -207,6 +279,178 @@ export const TOOLS = [
|
|
|
207
279
|
};
|
|
208
280
|
},
|
|
209
281
|
},
|
|
282
|
+
{
|
|
283
|
+
name: "send_welcome_email",
|
|
284
|
+
description:
|
|
285
|
+
"Email the active workspace's four key URLs (landing, booking, intake, admin dashboard) to a user. Use this AFTER create_workspace, only when the user has explicitly given their email — never auto-send. The admin URL is bearer-token-scoped and expires in 7 days. Example: send_welcome_email({ email: 'alice@example.com', name: 'Alice' }).",
|
|
286
|
+
inputSchema: obj(
|
|
287
|
+
{
|
|
288
|
+
email: str("Recipient email address."),
|
|
289
|
+
name: str("Optional recipient name (used in the greeting)."),
|
|
290
|
+
workspace_id: str("Optional workspace override. Defaults to active workspace."),
|
|
291
|
+
},
|
|
292
|
+
["email"],
|
|
293
|
+
),
|
|
294
|
+
handler: async (a) => {
|
|
295
|
+
const workspaceId = a.workspace_id ?? getDefaultWorkspace();
|
|
296
|
+
if (!workspaceId) {
|
|
297
|
+
throw new Error(
|
|
298
|
+
"No workspace selected. Run create_workspace({ name: '…' }) first, or pass workspace_id.",
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
const bearer = getWorkspaceBearer(workspaceId);
|
|
302
|
+
if (!bearer) {
|
|
303
|
+
throw new Error(
|
|
304
|
+
`No local bearer token for workspace ${workspaceId}. This device did not create it. Re-run create_workspace or switch to the device that did.`,
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const snapshot = await api(
|
|
309
|
+
"GET",
|
|
310
|
+
`/workspace/${encodeURIComponent(workspaceId)}/snapshot`,
|
|
311
|
+
{ workspace_id: workspaceId },
|
|
312
|
+
);
|
|
313
|
+
const publicUrls = snapshot?.public_urls ?? {};
|
|
314
|
+
if (!publicUrls.home || !publicUrls.book || !publicUrls.intake) {
|
|
315
|
+
throw new Error(
|
|
316
|
+
"Snapshot did not return public_urls (home/book/intake). Re-check the workspace.",
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Construct the bearer-scoped admin URL from the API base.
|
|
321
|
+
// API_INFO.base is `<host>/api/v1` — strip that suffix to get the app host.
|
|
322
|
+
const appHost = API_INFO.base.replace(/\/api\/v1\/?$/, "");
|
|
323
|
+
const adminUrl = `${appHost}/admin/${encodeURIComponent(workspaceId)}?token=${encodeURIComponent(bearer)}`;
|
|
324
|
+
|
|
325
|
+
await api("POST", "/email/send-welcome", {
|
|
326
|
+
body: {
|
|
327
|
+
email: a.email,
|
|
328
|
+
name: a.name ?? null,
|
|
329
|
+
workspace: {
|
|
330
|
+
landing_url: publicUrls.home,
|
|
331
|
+
booking_url: publicUrls.book,
|
|
332
|
+
intake_url: publicUrls.intake,
|
|
333
|
+
admin_url: adminUrl,
|
|
334
|
+
},
|
|
335
|
+
},
|
|
336
|
+
workspace_id: workspaceId,
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
return {
|
|
340
|
+
ok: true,
|
|
341
|
+
message: `Welcome email sent to ${a.email}`,
|
|
342
|
+
};
|
|
343
|
+
},
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
name: "collect_operator_email",
|
|
347
|
+
description:
|
|
348
|
+
"Onboarding-loop helper: ask the operator for their email post-create_workspace, then send the welcome email + record them as a lead in SeldonFrame's own CRM. Combines send_welcome_email + a lead-capture call so a single tool call closes the loop. Call this BEFORE further customization (configure_booking / customize_intake_form / install_vertical_pack) so the operator gets a permanent inbox record of their links. Example: collect_operator_email({ email: 'max@desertcool.com', name: 'Max' })",
|
|
349
|
+
inputSchema: obj(
|
|
350
|
+
{
|
|
351
|
+
email: str("Operator email — used as the welcome email recipient AND as the unique key for the SeldonFrame CRM lead."),
|
|
352
|
+
name: str("Optional operator name (used in the email greeting and on the CRM lead)."),
|
|
353
|
+
workspace_id: str("Optional workspace override. Defaults to the workspace just created."),
|
|
354
|
+
},
|
|
355
|
+
["email"],
|
|
356
|
+
),
|
|
357
|
+
handler: async (a) => {
|
|
358
|
+
const workspaceId = a.workspace_id ?? getDefaultWorkspace();
|
|
359
|
+
if (!workspaceId) {
|
|
360
|
+
throw new Error(
|
|
361
|
+
"No workspace selected. Run create_workspace({ name: '…' }) first, or pass workspace_id.",
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
const bearer = getWorkspaceBearer(workspaceId);
|
|
365
|
+
if (!bearer) {
|
|
366
|
+
throw new Error(
|
|
367
|
+
`No local bearer token for workspace ${workspaceId}. This device did not create it. Re-run create_workspace or switch to the device that did.`,
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Step 1: pull the workspace snapshot so we have the public URLs
|
|
372
|
+
// for the welcome email.
|
|
373
|
+
const snapshot = await api(
|
|
374
|
+
"GET",
|
|
375
|
+
`/workspace/${encodeURIComponent(workspaceId)}/snapshot`,
|
|
376
|
+
{ workspace_id: workspaceId },
|
|
377
|
+
);
|
|
378
|
+
const publicUrls = snapshot?.public_urls ?? {};
|
|
379
|
+
const slug = snapshot?.workspace?.slug ?? null;
|
|
380
|
+
if (!publicUrls.home || !publicUrls.book || !publicUrls.intake) {
|
|
381
|
+
throw new Error(
|
|
382
|
+
"Snapshot did not return public_urls (home/book/intake). Re-check the workspace.",
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
const appHost = API_INFO.base.replace(/\/api\/v1\/?$/, "");
|
|
386
|
+
const adminUrl = `${appHost}/admin/${encodeURIComponent(workspaceId)}?token=${encodeURIComponent(bearer)}`;
|
|
387
|
+
|
|
388
|
+
// Step 2: send the welcome email. Failures here are surfaced
|
|
389
|
+
// (operator told us their email, we owe them the email) but
|
|
390
|
+
// don't block the lead-capture step below.
|
|
391
|
+
let emailSent = false;
|
|
392
|
+
let emailError = null;
|
|
393
|
+
try {
|
|
394
|
+
await api("POST", "/email/send-welcome", {
|
|
395
|
+
body: {
|
|
396
|
+
email: a.email,
|
|
397
|
+
name: a.name ?? null,
|
|
398
|
+
workspace: {
|
|
399
|
+
landing_url: publicUrls.home,
|
|
400
|
+
booking_url: publicUrls.book,
|
|
401
|
+
intake_url: publicUrls.intake,
|
|
402
|
+
admin_url: adminUrl,
|
|
403
|
+
},
|
|
404
|
+
},
|
|
405
|
+
workspace_id: workspaceId,
|
|
406
|
+
});
|
|
407
|
+
emailSent = true;
|
|
408
|
+
} catch (err) {
|
|
409
|
+
emailError = err?.message ?? String(err);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Step 3: record the operator as a lead in SeldonFrame's own
|
|
413
|
+
// CRM workspace. Anonymous endpoint — no bearer required, ops
|
|
414
|
+
// workspace ID is server-side env only. Soft-failure: if the
|
|
415
|
+
// ops workspace isn't configured we still return ok.
|
|
416
|
+
let leadRecorded = false;
|
|
417
|
+
let leadId = null;
|
|
418
|
+
let leadError = null;
|
|
419
|
+
try {
|
|
420
|
+
const leadResp = await api("POST", "/leads/operator-signup", {
|
|
421
|
+
body: {
|
|
422
|
+
email: a.email,
|
|
423
|
+
name: a.name ?? null,
|
|
424
|
+
source_workspace_id: workspaceId,
|
|
425
|
+
source_workspace_slug: slug,
|
|
426
|
+
source: "mcp-onboarding",
|
|
427
|
+
},
|
|
428
|
+
allow_anonymous: true,
|
|
429
|
+
});
|
|
430
|
+
leadRecorded = Boolean(leadResp?.recorded);
|
|
431
|
+
leadId = leadResp?.lead_id ?? null;
|
|
432
|
+
} catch (err) {
|
|
433
|
+
leadError = err?.message ?? String(err);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return {
|
|
437
|
+
ok: emailSent || leadRecorded,
|
|
438
|
+
email_sent: emailSent,
|
|
439
|
+
email_error: emailError,
|
|
440
|
+
lead_recorded: leadRecorded,
|
|
441
|
+
lead_id: leadId,
|
|
442
|
+
lead_error: leadError,
|
|
443
|
+
message: emailSent
|
|
444
|
+
? `Welcome email sent to ${a.email}. Check your inbox — the admin URL is in there too.`
|
|
445
|
+
: `Could not send welcome email${emailError ? `: ${emailError}` : ""}. Lead ${leadRecorded ? "recorded" : "not recorded"}.`,
|
|
446
|
+
next: [
|
|
447
|
+
"configure_booking({ title, duration_minutes, description }) — tune the booking page if you collected business hours",
|
|
448
|
+
"customize_intake_form({ ... }) — match your intake to the workspace's lead-qualification questions",
|
|
449
|
+
"install_vertical_pack({ pack: '<industry>' }) — add domain-specific objects, fields, and views",
|
|
450
|
+
],
|
|
451
|
+
};
|
|
452
|
+
},
|
|
453
|
+
},
|
|
210
454
|
{
|
|
211
455
|
name: "revoke_bearer",
|
|
212
456
|
description:
|
|
@@ -394,10 +638,10 @@ export const TOOLS = [
|
|
|
394
638
|
{
|
|
395
639
|
name: "install_vertical_pack",
|
|
396
640
|
description:
|
|
397
|
-
"
|
|
641
|
+
"Set up an industry template (real-estate, dental, legal, …). Adds industry-specific objects, fields, and views to the CRM and pre-fills the booking page + intake form for that line of work.",
|
|
398
642
|
inputSchema: obj(
|
|
399
643
|
{
|
|
400
|
-
pack: str("
|
|
644
|
+
pack: str("Industry template name, e.g. 'real-estate', 'dental', 'legal'."),
|
|
401
645
|
workspace_id: str("Optional workspace override."),
|
|
402
646
|
},
|
|
403
647
|
["pack"],
|
|
@@ -413,10 +657,10 @@ export const TOOLS = [
|
|
|
413
657
|
{
|
|
414
658
|
name: "install_caldiy_booking",
|
|
415
659
|
description:
|
|
416
|
-
"Install the
|
|
660
|
+
"Install the booking page (event types, availability, scheduled bookings). Example: install_caldiy_booking({})",
|
|
417
661
|
inputSchema: obj({
|
|
418
662
|
workspace_id: str("Optional workspace override."),
|
|
419
|
-
config: { type: "object", description: "Optional
|
|
663
|
+
config: { type: "object", description: "Optional booking-page configuration overrides." },
|
|
420
664
|
}),
|
|
421
665
|
handler: async (a) => {
|
|
422
666
|
const ws = wsOrDefault(a.workspace_id);
|
|
@@ -429,10 +673,10 @@ export const TOOLS = [
|
|
|
429
673
|
{
|
|
430
674
|
name: "install_formbricks_intake",
|
|
431
675
|
description:
|
|
432
|
-
"Install
|
|
676
|
+
"Install an intake form (questions, conditional logic, automatic CRM sync). Example: install_formbricks_intake({})",
|
|
433
677
|
inputSchema: obj({
|
|
434
678
|
workspace_id: str("Optional workspace override."),
|
|
435
|
-
form_id: str("Optional existing
|
|
679
|
+
form_id: str("Optional existing intake-form id to bind."),
|
|
436
680
|
}),
|
|
437
681
|
handler: async (a) => {
|
|
438
682
|
const ws = wsOrDefault(a.workspace_id);
|
|
@@ -459,7 +703,7 @@ export const TOOLS = [
|
|
|
459
703
|
{
|
|
460
704
|
name: "fetch_source_for_soul",
|
|
461
705
|
description:
|
|
462
|
-
"Fetch a URL and return normalized text (headings + body, up to 256KB). Use this to gather raw content
|
|
706
|
+
"Fetch a URL and return normalized text (headings + body, up to 256KB). Use this to gather raw content from the operator's existing website; then extract a structured business profile and save it with submit_soul. Zero LLM cost to SeldonFrame — extraction runs in this session.",
|
|
463
707
|
inputSchema: obj(
|
|
464
708
|
{
|
|
465
709
|
url: str("Absolute URL to fetch."),
|
|
@@ -478,7 +722,7 @@ export const TOOLS = [
|
|
|
478
722
|
truncated,
|
|
479
723
|
text,
|
|
480
724
|
next: [
|
|
481
|
-
"Extract a
|
|
725
|
+
"Extract a business profile: { mission, audience, tone, offerings[], differentiators[], faqs[] }",
|
|
482
726
|
"submit_soul({ soul: <extracted> })",
|
|
483
727
|
],
|
|
484
728
|
};
|
|
@@ -487,13 +731,13 @@ export const TOOLS = [
|
|
|
487
731
|
{
|
|
488
732
|
name: "submit_soul",
|
|
489
733
|
description:
|
|
490
|
-
"
|
|
734
|
+
"Save a business profile to the active workspace. The profile drives the landing page, intake form copy, and AI-agent context. Call this after fetch_source_for_soul or after gathering details from the user. Triggers a re-render of the public landing page so changes are visible immediately.",
|
|
491
735
|
inputSchema: obj(
|
|
492
736
|
{
|
|
493
737
|
soul: {
|
|
494
738
|
type: "object",
|
|
495
739
|
description:
|
|
496
|
-
"
|
|
740
|
+
"Business profile. Expected keys: business_name, tagline, soul_description, phone, email, address, offerings, faqs, testimonials. Additional keys allowed — they're preserved for future use.",
|
|
497
741
|
},
|
|
498
742
|
workspace_id: str("Optional workspace override."),
|
|
499
743
|
},
|
package/src/welcome.js
CHANGED
|
@@ -104,9 +104,9 @@ Soul compilation runs HERE, not on the backend:
|
|
|
104
104
|
|
|
105
105
|
## When you'll need \`SELDONFRAME_API_KEY\`
|
|
106
106
|
|
|
107
|
-
The first workspace is free forever. Paid tiers (
|
|
108
|
-
$99/mo,
|
|
109
|
-
white-label, and advanced Brain capabilities. A key is required for:
|
|
107
|
+
The first workspace is free forever. Paid tiers (Growth $29/mo, Scale
|
|
108
|
+
$99/mo, both with metered usage) unlock additional workspaces, custom
|
|
109
|
+
domains, white-label, and advanced Brain capabilities. A key is required for:
|
|
110
110
|
|
|
111
111
|
- Adding a **second workspace**
|
|
112
112
|
- Connecting a **custom domain**
|