@sprid/cli 0.1.5 → 0.1.6
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/CHANGELOG.md +10 -0
- package/package.json +1 -1
- package/src/clipboard.mjs +75 -0
- package/src/commands/connect.mjs +54 -15
- package/src/docs/commands.mjs +1 -0
- package/src/docs/guides.generated.mjs +47 -47
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,16 @@
|
|
|
3
3
|
sprid follows semver: a breaking change to a command's arguments, its output
|
|
4
4
|
shape or its exit codes is a major release.
|
|
5
5
|
|
|
6
|
+
## 0.1.6 - 2026-09-24
|
|
7
|
+
|
|
8
|
+
- `sprid connect <service> --key-from-clipboard` reads a copied key from the
|
|
9
|
+
clipboard, saves it to Sprid and then empties the clipboard, so the key never
|
|
10
|
+
reaches the screen, shell history or a file. It covers every service whose
|
|
11
|
+
key is a copied string (RevenueCat, Stripe, PostHog, Polar, Paddle, Lemon
|
|
12
|
+
Squeezy, Plausible, Umami, Cloudflare); the ones whose credential is a
|
|
13
|
+
downloaded file keep `--key <file>`. macOS, Windows, and Linux through
|
|
14
|
+
`wl-paste`, `xclip` or `xsel`.
|
|
15
|
+
|
|
6
16
|
## 0.1.5 - 2026-09-24
|
|
7
17
|
|
|
8
18
|
- `sprid connect ga4|plausible|umami` saves a website-analytics key beside the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sprid/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "The Sprid command line: connect your app, review results, prepare posts and store screenshots, and release mobile apps.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"type": "module",
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// The clipboard as a key source. A key copied out of a provider's dashboard
|
|
2
|
+
// goes straight from the clipboard into the request: it never reaches the
|
|
3
|
+
// screen, a chat, shell history or a file on disk. Nothing here prints what it
|
|
4
|
+
// read.
|
|
5
|
+
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
7
|
+
import { UsageError } from "./args.mjs";
|
|
8
|
+
|
|
9
|
+
// First reader that exists wins. Linux has no single clipboard tool, so try
|
|
10
|
+
// Wayland, then the two X11 ones.
|
|
11
|
+
const READERS = {
|
|
12
|
+
darwin: [["pbpaste", []]],
|
|
13
|
+
win32: [["powershell", ["-NoProfile", "-Command", "Get-Clipboard -Raw"]]],
|
|
14
|
+
linux: [
|
|
15
|
+
["wl-paste", ["--no-newline"]],
|
|
16
|
+
["xclip", ["-selection", "clipboard", "-o"]],
|
|
17
|
+
["xsel", ["--clipboard", "--output"]],
|
|
18
|
+
],
|
|
19
|
+
};
|
|
20
|
+
const CLEARERS = {
|
|
21
|
+
darwin: [["pbcopy", []]],
|
|
22
|
+
win32: [["powershell", ["-NoProfile", "-Command", "Set-Clipboard -Value $null"]]],
|
|
23
|
+
linux: [
|
|
24
|
+
["wl-copy", ["--clear"]],
|
|
25
|
+
["xclip", ["-selection", "clipboard", "-i"]],
|
|
26
|
+
["xsel", ["--clipboard", "--clear"]],
|
|
27
|
+
],
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// A clearer's stdout is ignored, not piped: xclip forks a child that keeps
|
|
31
|
+
// owning the selection, and a pipe it inherits would hold spawnSync open.
|
|
32
|
+
function run(candidates, input) {
|
|
33
|
+
const clearing = input !== undefined;
|
|
34
|
+
for (const [cmd, args] of candidates ?? []) {
|
|
35
|
+
const r = spawnSync(cmd, args, { input: input ?? "", encoding: "utf8", stdio: ["pipe", clearing ? "ignore" : "pipe", "ignore"] });
|
|
36
|
+
if (r.error?.code === "ENOENT") continue;
|
|
37
|
+
if (r.status === 0) return { ok: true, out: r.stdout ?? "" };
|
|
38
|
+
}
|
|
39
|
+
return { ok: false, out: "" };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Check what the clipboard holds looks like a key, not what is usually there
|
|
44
|
+
* by mistake: nothing, or the command itself (copied out of a guide or a chat
|
|
45
|
+
* a moment before). Keys and tokens from every provider we read are one
|
|
46
|
+
* unbroken string, so any whitespace means it is not one.
|
|
47
|
+
*/
|
|
48
|
+
export function checkClipboardKey(raw, what = "the key") {
|
|
49
|
+
const text = String(raw ?? "").trim();
|
|
50
|
+
if (!text) throw new UsageError(`The clipboard is empty. Copy ${what}, then run this again.`);
|
|
51
|
+
if (/\s/.test(text)) {
|
|
52
|
+
throw new UsageError(
|
|
53
|
+
`The clipboard holds text with spaces, not a key (often this command itself). Copy ${what}, then run this again.`,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
if (text.length < 8) throw new UsageError(`The clipboard holds something too short to be a key. Copy ${what}, then run this again.`);
|
|
57
|
+
return text;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function readClipboardKey(what, platform = process.platform, exec = run) {
|
|
61
|
+
const r = exec(READERS[platform]);
|
|
62
|
+
if (!r.ok) {
|
|
63
|
+
throw new UsageError(
|
|
64
|
+
platform === "linux"
|
|
65
|
+
? "Could not read the clipboard. Install wl-clipboard, xclip or xsel, or save the key to a file and pass --key <file>."
|
|
66
|
+
: "Could not read the clipboard. Save the key to a file and pass --key <file> instead.",
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
return checkClipboardKey(r.out, what);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Best effort: a key left on the clipboard is the next paste's accident. */
|
|
73
|
+
export function clearClipboard(platform = process.platform, exec = run) {
|
|
74
|
+
return exec(CLEARERS[platform], "").ok;
|
|
75
|
+
}
|
package/src/commands/connect.mjs
CHANGED
|
@@ -15,6 +15,7 @@ import { UsageError } from "../args.mjs";
|
|
|
15
15
|
import { secretsPath } from "../creds.mjs";
|
|
16
16
|
import { ApiError } from "../http.mjs";
|
|
17
17
|
import { pendingGrant } from "../pending.mjs";
|
|
18
|
+
import { clearClipboard, readClipboardKey } from "../clipboard.mjs";
|
|
18
19
|
import { C, BAD, EXPIRING, NONE, OK, table, wrap } from "../format.mjs";
|
|
19
20
|
import { connectionRows } from "./status.mjs";
|
|
20
21
|
import { confirm } from "./reviews.mjs";
|
|
@@ -283,13 +284,25 @@ export function fileOrValue(arg, cwd) {
|
|
|
283
284
|
return String(arg);
|
|
284
285
|
}
|
|
285
286
|
|
|
286
|
-
|
|
287
|
-
|
|
287
|
+
// Services whose credential is a downloaded file (a .p8, a service-account
|
|
288
|
+
// JSON). There is nothing to copy, so the clipboard is not offered for them.
|
|
289
|
+
const FILE_KEY_SERVICES = new Set(["asc", "play", "gsc", "ga4"]);
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Flags → the PATCH body for one service. Reads files; never echoes them.
|
|
293
|
+
* `pasted` is a key already read from the clipboard (`--key-from-clipboard`);
|
|
294
|
+
* it takes the place of the --key / --token file.
|
|
295
|
+
*/
|
|
296
|
+
export function sensorPatch(service, flags, cwd, pasted) {
|
|
288
297
|
const need = (name) => {
|
|
289
298
|
const v = flags[name];
|
|
290
|
-
if (!v || v === true)
|
|
299
|
+
if (!v || v === true) {
|
|
300
|
+
const hint = !FILE_KEY_SERVICES.has(service) && (name === "key" || name === "token") ? ` (or --key-from-clipboard)` : "";
|
|
301
|
+
throw new UsageError(`sprid connect ${service} needs --${name}${hint}`);
|
|
302
|
+
}
|
|
291
303
|
return String(v);
|
|
292
304
|
};
|
|
305
|
+
const secret = (name) => pasted ?? fileOrValue(need(name), cwd);
|
|
293
306
|
switch (service) {
|
|
294
307
|
case "asc":
|
|
295
308
|
return {
|
|
@@ -307,7 +320,7 @@ export function sensorPatch(service, flags, cwd) {
|
|
|
307
320
|
secrets: { gscServiceAccountJson: readKeyFile(need("key"), cwd) },
|
|
308
321
|
};
|
|
309
322
|
case "posthog": {
|
|
310
|
-
const body = { posthogProjectId: need("project"), secrets: { posthogApiKey:
|
|
323
|
+
const body = { posthogProjectId: need("project"), secrets: { posthogApiKey: secret("key") } };
|
|
311
324
|
if (flags.host && flags.host !== true) {
|
|
312
325
|
const host = String(flags.host);
|
|
313
326
|
body.posthogHost = host === "eu" || host === "us" ? `https://${host}.posthog.com` : host;
|
|
@@ -331,7 +344,7 @@ export function sensorPatch(service, flags, cwd) {
|
|
|
331
344
|
case "plausible": {
|
|
332
345
|
const body = {
|
|
333
346
|
plausibleSiteId: String(need("site")).replace(/^https?:\/\//, "").replace(/\/+$/, ""),
|
|
334
|
-
secrets: { plausibleApiKey:
|
|
347
|
+
secrets: { plausibleApiKey: secret("key") },
|
|
335
348
|
};
|
|
336
349
|
if (flags.host && flags.host !== true) body.plausibleHost = String(flags.host).replace(/\/+$/, "");
|
|
337
350
|
if (flags.use) body.trafficProvider = "plausible";
|
|
@@ -340,7 +353,7 @@ export function sensorPatch(service, flags, cwd) {
|
|
|
340
353
|
case "umami": {
|
|
341
354
|
const body = {
|
|
342
355
|
umamiWebsiteId: String(need("site")),
|
|
343
|
-
secrets: { umamiApiKey:
|
|
356
|
+
secrets: { umamiApiKey: secret("key") },
|
|
344
357
|
};
|
|
345
358
|
// A self-hosted Umami serves its API under /api. Leaving that off is the
|
|
346
359
|
// commonest setup mistake, and it fails as a 404 on every read, so add
|
|
@@ -353,41 +366,65 @@ export function sensorPatch(service, flags, cwd) {
|
|
|
353
366
|
return body;
|
|
354
367
|
}
|
|
355
368
|
case "revenuecat":
|
|
356
|
-
return { revenuecatProjectId: need("project"), secrets: { revenuecatApiKey:
|
|
369
|
+
return { revenuecatProjectId: need("project"), secrets: { revenuecatApiKey: secret("key") } };
|
|
357
370
|
// Stripe and Paddle need no id: the key names the account. Polar takes an
|
|
358
371
|
// optional organization id (an org token already implies one), Lemon
|
|
359
372
|
// Squeezy needs its store id.
|
|
360
373
|
case "stripe":
|
|
361
|
-
return { secrets: { stripeApiKey:
|
|
374
|
+
return { secrets: { stripeApiKey: secret("key") } };
|
|
362
375
|
case "polar": {
|
|
363
|
-
const body = { secrets: { polarApiKey:
|
|
376
|
+
const body = { secrets: { polarApiKey: secret("key") } };
|
|
364
377
|
if (flags.org && flags.org !== true) body.polarOrganizationId = String(flags.org);
|
|
365
378
|
return body;
|
|
366
379
|
}
|
|
367
380
|
case "lemonsqueezy":
|
|
368
381
|
return {
|
|
369
382
|
lemonSqueezyStoreId: String(need("store")),
|
|
370
|
-
secrets: { lemonSqueezyApiKey:
|
|
383
|
+
secrets: { lemonSqueezyApiKey: secret("key") },
|
|
371
384
|
};
|
|
372
385
|
case "paddle": {
|
|
373
|
-
const body = { secrets: { paddleApiKey:
|
|
386
|
+
const body = { secrets: { paddleApiKey: secret("key") } };
|
|
374
387
|
if (flags.env && flags.env !== true) body.paddleEnvironment = String(flags.env) === "sandbox" ? "sandbox" : "live";
|
|
375
388
|
return body;
|
|
376
389
|
}
|
|
377
390
|
case "cloudflare":
|
|
378
|
-
return { cloudflareZoneId: need("zone"), secrets: { cloudflareApiToken:
|
|
391
|
+
return { cloudflareZoneId: need("zone"), secrets: { cloudflareApiToken: secret("token") } };
|
|
379
392
|
default:
|
|
380
393
|
throw new UsageError(`Unknown key type "${service}"`);
|
|
381
394
|
}
|
|
382
395
|
}
|
|
383
396
|
|
|
397
|
+
/**
|
|
398
|
+
* `--key-from-clipboard`: the key a provider just showed, read at run time.
|
|
399
|
+
* Refused with a file flag beside it (which one would win is a guess) and for
|
|
400
|
+
* services whose credential is a download.
|
|
401
|
+
*/
|
|
402
|
+
function pastedKey(ctx, service, label) {
|
|
403
|
+
if (!ctx.flags["key-from-clipboard"]) return undefined;
|
|
404
|
+
if (FILE_KEY_SERVICES.has(service)) {
|
|
405
|
+
throw new UsageError(`${label} uses a downloaded key file. Pass --key <path to the file> instead of --key-from-clipboard.`);
|
|
406
|
+
}
|
|
407
|
+
if ((ctx.flags.key && ctx.flags.key !== true) || (ctx.flags.token && ctx.flags.token !== true)) {
|
|
408
|
+
throw new UsageError("Pass either --key-from-clipboard or a key file, not both.");
|
|
409
|
+
}
|
|
410
|
+
return readClipboardKey(`the ${label} key`);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function clearPasted(ctx, pasted) {
|
|
414
|
+
if (pasted === undefined) return;
|
|
415
|
+
const cleared = clearClipboard();
|
|
416
|
+
if (!ctx.json) ctx.print(C.dim(cleared ? " Clipboard cleared." : " Could not clear the clipboard. Copy something else over the key."));
|
|
417
|
+
}
|
|
418
|
+
|
|
384
419
|
async function connectSensor(ctx, service) {
|
|
385
420
|
if (ctx.flags.clear) return clearSensor(ctx, service);
|
|
386
|
-
const
|
|
421
|
+
const pasted = pastedKey(ctx, service, SENSOR_LABEL[service]);
|
|
422
|
+
const body = sensorPatch(service, ctx.flags, ctx.cwd, pasted);
|
|
387
423
|
const wid = await ctx.workspaceId();
|
|
388
424
|
const profile = await resolveProfile(ctx, wid);
|
|
389
425
|
const updated = await patchProfile(ctx, wid, profile.id, body);
|
|
390
426
|
report(ctx, updated, Object.keys(body.secrets), `${SENSOR_LABEL[service]} key`, wid, service);
|
|
427
|
+
clearPasted(ctx, pasted);
|
|
391
428
|
return 0;
|
|
392
429
|
}
|
|
393
430
|
|
|
@@ -432,9 +469,10 @@ function report(ctx, profile, keys, what, workspaceId, service) {
|
|
|
432
469
|
*/
|
|
433
470
|
async function connectProvider(ctx, provider) {
|
|
434
471
|
if (ctx.flags.clear) return clearProvider(ctx, provider);
|
|
472
|
+
const pasted = pastedKey(ctx, provider, provider);
|
|
435
473
|
const raw = ctx.flags.key;
|
|
436
|
-
if (!raw || raw === true) throw new UsageError(`sprid connect ${provider} --key <file or key
|
|
437
|
-
const apiKey = fileOrValue(raw, ctx.cwd);
|
|
474
|
+
if (pasted === undefined && (!raw || raw === true)) throw new UsageError(`sprid connect ${provider} --key <file or key> (or --key-from-clipboard)`);
|
|
475
|
+
const apiKey = pasted ?? fileOrValue(raw, ctx.cwd);
|
|
438
476
|
if (apiKey.length < 8) throw new UsageError("That key is too short to be one.");
|
|
439
477
|
const wid = await ctx.workspaceId();
|
|
440
478
|
let created;
|
|
@@ -452,6 +490,7 @@ async function connectProvider(ctx, provider) {
|
|
|
452
490
|
ctx.print(` ${C.ok(OK)} ${provider} key set on the workspace${hint ? ` ${C.dim(hint)}` : ""}`);
|
|
453
491
|
if (provider === "anthropic" || provider === "openai") ctx.print(C.dim(" Review replies and other drafting now run on your own key."));
|
|
454
492
|
}
|
|
493
|
+
clearPasted(ctx, pasted);
|
|
455
494
|
return 0;
|
|
456
495
|
}
|
|
457
496
|
|
package/src/docs/commands.mjs
CHANGED
|
@@ -66,6 +66,7 @@ export const COMMAND_GROUPS = [
|
|
|
66
66
|
['init', 'sprid init', 'Create an App Profile from .sprid/app.json in your repo.'],
|
|
67
67
|
['connect', 'sprid connect', 'Show missing connections and the commands to add them.'],
|
|
68
68
|
['connect', 'sprid connect <service> --app <slug> --key <file> …', 'Save a store or analytics key. Read sprid docs <service> for the required fields.'],
|
|
69
|
+
['connect', 'sprid connect <service> --app <slug> --key-from-clipboard …', 'Save a key you just copied from the provider. It is read from the clipboard, never printed or written to disk, and the clipboard is cleared after saving. Not for services whose key is a downloaded file.'],
|
|
69
70
|
['connect', 'sprid connect <platform> --account <slug>', 'Connect a publishing account through the platform’s sign-in page.'],
|
|
70
71
|
['connect', 'sprid connect ga4|plausible|umami --app <slug> --key <file> … [--use]', 'PostHog, Google Analytics, Plausible and Umami all count the same website visits, so Sprid reads one of them and never adds them together. --use makes this one the source the traffic card reports from.'],
|
|
71
72
|
['pinterest', 'sprid pinterest boards --account <slug> [--channel <id>] [--bookmark <token>]', 'List boards and sections for the exact connected Pinterest channel; a returned bookmark reads the next page.'],
|
|
@@ -299,7 +299,7 @@ export const GUIDES = [
|
|
|
299
299
|
"id": "posthog",
|
|
300
300
|
"title": "PostHog",
|
|
301
301
|
"summary": "See how people use your app and where they stop.",
|
|
302
|
-
"command": "sprid connect posthog --app myapp --key
|
|
302
|
+
"command": "sprid connect posthog --app myapp --key-from-clipboard --project 12345 --host eu",
|
|
303
303
|
"url": "https://sprid.studio/docs/connect/posthog",
|
|
304
304
|
"sections": [
|
|
305
305
|
{
|
|
@@ -312,7 +312,7 @@ export const GUIDES = [
|
|
|
312
312
|
"id": "click-path-usposthogcom-or-euposthogcom",
|
|
313
313
|
"title": "Click path (us.posthog.com or eu.posthog.com)",
|
|
314
314
|
"kind": "steps",
|
|
315
|
-
"markdown": "1. **Settings → Account → Personal API keys → Create personal API key**, named `Sprid <app name>`.\n2. Under access, select **Projects** and choose your app’s project. Leave **All access** off.\n3. Select **Query → Read** and **Project → Read**. Leave write access off.\n4.
|
|
315
|
+
"markdown": "1. **Settings → Account → Personal API keys → Create personal API key**, named `Sprid <app name>`.\n2. Under access, select **Projects** and choose your app’s project. Leave **All access** off.\n3. Select **Query → Read** and **Project → Read**. Leave write access off.\n4. Create the key and keep the dialog open: PostHog shows it once. Use a separate key per app.\n5. Copy the key and leave it on your clipboard."
|
|
316
316
|
},
|
|
317
317
|
{
|
|
318
318
|
"id": "project-id-and-host",
|
|
@@ -324,7 +324,7 @@ export const GUIDES = [
|
|
|
324
324
|
"id": "then-run",
|
|
325
325
|
"title": "Then run",
|
|
326
326
|
"kind": "command",
|
|
327
|
-
"markdown": "```sh\nsprid connect posthog --app myapp --key
|
|
327
|
+
"markdown": "```sh\nsprid connect posthog --app myapp --key-from-clipboard --project 12345 --host eu\n```\n\nUse your own app slug, project id and host. Add `--workspace <slug>` if needed. Keep the key out of chat.\n\nSprid reads the key from your clipboard, saves it and clears the clipboard, so it never shows on screen or lands in a file. Working with an agent? Tell it the key is copied and it runs this for you. Typing it yourself, copy the key last: paste the command into your terminal first, then copy the key, then press Enter. If the clipboard still holds the command, Sprid refuses it; copy the key and run it again. Without clipboard access (a remote shell), save the key to a file and pass `--key <file>` instead."
|
|
328
328
|
},
|
|
329
329
|
{
|
|
330
330
|
"id": "how-to-check-it-worked",
|
|
@@ -369,8 +369,8 @@ export const GUIDES = [
|
|
|
369
369
|
"markdown": "Setup and live reads checked 2026-09-09.\n\n- [PostHog personal API keys](https://posthog.com/docs/api/personal-api-keys)\n- [Project identity endpoint](https://posthog.com/docs/api/projects)\n- [HogQL query endpoint](https://posthog.com/docs/api/query)\n- [SDK and framework guides](https://posthog.com/docs/libraries)\n- [React Native screen tracking](https://posthog.com/docs/libraries/react-native)\n- [Identifying users](https://posthog.com/docs/product-analytics/identify)\n- [Funnels](https://posthog.com/docs/product-analytics/funnels)"
|
|
370
370
|
}
|
|
371
371
|
],
|
|
372
|
-
"markdown": "# PostHog\n\n**What Sprid does with this:** See how people use your app and where they stop.\n\n## You need\n\nAccess to your app’s PostHog project and permission to create a **personal API key**. Your app must already send events to PostHog; connecting Sprid adds no tracking. The public key inside your app does not work here.\n\n## Click path (us.posthog.com or eu.posthog.com)\n\n1. **Settings → Account → Personal API keys → Create personal API key**, named `Sprid <app name>`.\n2. Under access, select **Projects** and choose your app’s project. Leave **All access** off.\n3. Select **Query → Read** and **Project → Read**. Leave write access off.\n4. Copy the key before closing the dialog and save it to a private file such as `~/keys/posthog-myapp.txt`. Use a separate key per app.\n\n## Project id and host\n\n- **Project id:** the number after `/project/` in your PostHog address, such as `12345`.\n- **Host:** `eu` for `eu.posthog.com`, `us` for `us.posthog.com`, or your full self-hosted address, whichever you open the dashboard on.\n\n## Then run\n\n```sh\nsprid connect posthog --app myapp --key ~/keys/posthog-myapp.txt --project 12345 --host eu\n```\n\nUse your own app slug, file path, project id and host. Add `--workspace <slug>` if needed. Keep the key out of chat.\n\n## How to check it worked\n\nAsk your agent: “Check that Sprid can read recent PostHog events for this app, and confirm the project is correct.” The check reports recent activity or explains why there is none; a saved connection alone does not prove events arrive.\n\n## Metric definitions\n\nIn **Settings → Apps → your app → PostHog**, set the account-created event and the first UTC date from which tracking is complete.\n\n| Metric | What counts |\n|---|---|\n| Registrations | First `sprid_signup` event per PostHog person, across all surfaces |\n| New product users | First product activity, including anonymous people |\n| Active product users | Distinct people with product activity in the period |\n| Web visitors | Distinct pageview visitors on the saved website hostname, minus your product |\n\n**Registrations.** Send `sprid_signup` from your server after the account is created, with the account id as `distinct_id`, and identify that id in your clients. Never fire it on login, page load or install. An existing `posthogEvents.signup` mapping also works; `posthogConfig.registration.event` wins over it. No matching event history reads as unavailable; history with no new registrations reads as zero. Periods before the tracking start are unavailable, and comparisons crossing it, or with no confirmed start, are withheld. Backfill with the original creation timestamps.\n\n### What counts as your product\n\nSet this under **What counts as your product** on the same screen. It decides who is an active person, and it is the single setting most likely to make the people card wrong.\n\n| Your product is | Choose | Also give |\n|---|---|---|\n| An iOS or Android app | **A phone app** | Nothing. This is the default |\n| A web app on its own subdomain | **A website or web app** | The hostnames, such as `app.example.com` |\n| A web app under a path on your marketing domain | **A website or web app** | The paths, such as `/app`, `/dashboard` |\n| A phone app with a web client | **Both** | The web hostnames or paths |\n\n**A web product must say where it lives.** Your marketing pages and your product are the same PostHog events; without a hostname or a path there is nothing to tell them apart, and every visitor would be counted as someone using the product. Sprid refuses that configuration rather than reporting it.\n\nWhatever you name here is **subtracted from your website visitor numbers**, so a page inside the product is never also counted as a visit.\n\n**A phone app** needs `posthog-react-native` on iOS, iPadOS or Android and rejects explicit web surfaces: set `app_surface: 'web'` on Expo web and `'native'` on devices. Web counts need a browser and exclude reported bots and crawler user agents. Every metric excludes events or people with `is_internal`, `is_test` or `sprid_test = true`. What remains is observed identities, not guaranteed humans.\n\n### Custom properties\n\nAdvanced rules go under **Custom properties**:\n\n```json\n{\"registration\":{\"identity\":{\"scope\":\"event\",\"property\":\"account_id\"}},\"app\":{\"kind\":\"web\",\"hosts\":[\"app.example.com\"]},\"exclude\":[{\"scope\":\"person\",\"property\":\"staff\",\"operator\":\"in\",\"values\":[true]}]}\n```\n\n- `app.kind` is `native`, `web` or `both`, with `app.hosts` and `app.pathPrefixes` saying where a web product lives. This is what the screen above writes.\n- `app.filters` replaces the whole definition with your own rule; all filters must match. Choosing it shows as **Custom rules** on the screen, and the rule is subtracted from web visitors the same way.\n- `exclude` removes matching traffic from every metric.\n- `registration.filters` narrows the registration event, for example `result = success`.\n- `registration.identity` defaults to `person_id`. A configured property must be present and should never change, because it counts accounts.\n- Each rule takes `scope: event|person`, `operator: in|not_in` and string, number or boolean `values`. A missing property fails `in` and passes `not_in`. Property names are literal keys, dots included.\n\nThe same object is `posthogConfig` in REST `PATCH /api/app-profiles/:id`, MCP `upsert_app_profile` and the App Profile JSON used by `sprid init`, where `registration.event` and `registration.since` also live. No credentials or SQL belong in it. After saving, fetch Insights and check its registration notes and totals against your database before trusting a funnel.\n\n## Review suspected automated traffic\n\nRead [Check whether a traffic spike is real](https://sprid.studio/docs/traffic) (`sprid docs traffic`) before saving a rule: a spike or a shared fingerprint alone does not prove bots.\n\nOpen **Web visitors → Review traffic exclusions**, or **Settings → Apps → your app → PostHog → Review traffic**. Pick a UTC date range and a traffic group, preview the effect, then save. Agents use MCP `review_traffic` or `POST /api/app-profiles/:ref/traffic-review?workspaceId=…` with `{\"start\":\"2026-09-18\",\"end\":\"2026-09-19\"}`: end dates exclusive, UTC, at most 31 days. An optional `exclusion` previews a rule against this period and the one before. Save approved rules in `posthogConfig.trafficExclusions` through `upsert_app_profile` or the profile PATCH route, keeping the rest of the configuration. Sprid records who edited it and when.\n\nHow exclusions apply:\n\n- App-specific and date-bounded. Properties inside a rule are AND; enabled rules are OR.\n- Applied to website totals, charts, breakdowns, social attribution and weekly website counts, comparison period included. Remaining visitors are recounted as distinct people, never subtracted.\n- Native app activity and registrations are unchanged, and PostHog’s data is never changed or deleted. **Restore this traffic** disables a rule.\n- Raw connected queries and the marketing-review event inventory stay unfiltered as evidence; Insights shows the corrected figures.\n- Only PostHog is supported today.\n\n## Social traffic and clip comparisons\n\nSave the app’s website URL on its App Profile as well as connecting PostHog. Insights then compares completed-day website sessions with recorded social view gains. Shared bio traffic stays at channel level; only a dedicated tagged link identifies one publish. Older clips with metric activity stay candidates.\n\nCopy the stable bio link and dedicated clip links from Insights. Keep `utm_source`, `utm_medium`, `sprid_account` and, on dedicated links only, `sprid_publish` through any redirects. Never repoint the shared bio link to the newest publish. The optional bio-page HTML export records selections separately and keeps a direct app link; host it on the saved hostname with your existing PostHog setup.\n\nTo capture store-link clicks and website outcomes, add after your PostHog initialization:\n\n```html\n<script src=\"https://sprid.studio/sprid-attribution.js\" defer></script>\n```\n\nIt uses your PostHog client and consent state and sends nothing to Sprid. Call `window.spridAttribution?.track('signup')` or `.track('activation')` only after that action succeeds. `posthogEvents.signup` and `posthogEvents.activation` mappings also count when the event shares the arriving website session. It does not track a native app or join visits to purchases, and a store click is not an install. Missing outcomes stay unmeasured. Redirect-only flows need a beacon before navigating; redirect events are counted apart from sessions. `get_insights` and `sprid insights` return the same evidence as the dashboard.\n\n## If it fails\n\n- **Access denied:** check the key is active, grants your project and has both Read permissions. Reconnect with a corrected key file.\n- **Project not found:** check project number and host together. A US project needs the US host.\n- **Connected but no events:** check the dates and project, then ask your agent to check your app’s tracking is sending events.\n\n## Investigate with this connection\n\nReads `query` (HogQL), `events` and `properties` for the pinned project. Check instrumentation before interpreting events. Event-definition discovery also needs `event_definition:read`, property-definition discovery `property_definition:read`, both restricted to the same project.\n\n```sh\nsprid marketing-review capabilities --app <slug> --source posthog --json\n```\n\nSee [connected queries](https://sprid.studio/docs/queries).\n\n## Sources and verification\n\nSetup and live reads checked 2026-09-09.\n\n- [PostHog personal API keys](https://posthog.com/docs/api/personal-api-keys)\n- [Project identity endpoint](https://posthog.com/docs/api/projects)\n- [HogQL query endpoint](https://posthog.com/docs/api/query)\n- [SDK and framework guides](https://posthog.com/docs/libraries)\n- [React Native screen tracking](https://posthog.com/docs/libraries/react-native)\n- [Identifying users](https://posthog.com/docs/product-analytics/identify)\n- [Funnels](https://posthog.com/docs/product-analytics/funnels)\n",
|
|
373
|
-
"revision": "
|
|
372
|
+
"markdown": "# PostHog\n\n**What Sprid does with this:** See how people use your app and where they stop.\n\n## You need\n\nAccess to your app’s PostHog project and permission to create a **personal API key**. Your app must already send events to PostHog; connecting Sprid adds no tracking. The public key inside your app does not work here.\n\n## Click path (us.posthog.com or eu.posthog.com)\n\n1. **Settings → Account → Personal API keys → Create personal API key**, named `Sprid <app name>`.\n2. Under access, select **Projects** and choose your app’s project. Leave **All access** off.\n3. Select **Query → Read** and **Project → Read**. Leave write access off.\n4. Create the key and keep the dialog open: PostHog shows it once. Use a separate key per app.\n5. Copy the key and leave it on your clipboard.\n\n## Project id and host\n\n- **Project id:** the number after `/project/` in your PostHog address, such as `12345`.\n- **Host:** `eu` for `eu.posthog.com`, `us` for `us.posthog.com`, or your full self-hosted address, whichever you open the dashboard on.\n\n## Then run\n\n```sh\nsprid connect posthog --app myapp --key-from-clipboard --project 12345 --host eu\n```\n\nUse your own app slug, project id and host. Add `--workspace <slug>` if needed. Keep the key out of chat.\n\nSprid reads the key from your clipboard, saves it and clears the clipboard, so it never shows on screen or lands in a file. Working with an agent? Tell it the key is copied and it runs this for you. Typing it yourself, copy the key last: paste the command into your terminal first, then copy the key, then press Enter. If the clipboard still holds the command, Sprid refuses it; copy the key and run it again. Without clipboard access (a remote shell), save the key to a file and pass `--key <file>` instead.\n\n## How to check it worked\n\nAsk your agent: “Check that Sprid can read recent PostHog events for this app, and confirm the project is correct.” The check reports recent activity or explains why there is none; a saved connection alone does not prove events arrive.\n\n## Metric definitions\n\nIn **Settings → Apps → your app → PostHog**, set the account-created event and the first UTC date from which tracking is complete.\n\n| Metric | What counts |\n|---|---|\n| Registrations | First `sprid_signup` event per PostHog person, across all surfaces |\n| New product users | First product activity, including anonymous people |\n| Active product users | Distinct people with product activity in the period |\n| Web visitors | Distinct pageview visitors on the saved website hostname, minus your product |\n\n**Registrations.** Send `sprid_signup` from your server after the account is created, with the account id as `distinct_id`, and identify that id in your clients. Never fire it on login, page load or install. An existing `posthogEvents.signup` mapping also works; `posthogConfig.registration.event` wins over it. No matching event history reads as unavailable; history with no new registrations reads as zero. Periods before the tracking start are unavailable, and comparisons crossing it, or with no confirmed start, are withheld. Backfill with the original creation timestamps.\n\n### What counts as your product\n\nSet this under **What counts as your product** on the same screen. It decides who is an active person, and it is the single setting most likely to make the people card wrong.\n\n| Your product is | Choose | Also give |\n|---|---|---|\n| An iOS or Android app | **A phone app** | Nothing. This is the default |\n| A web app on its own subdomain | **A website or web app** | The hostnames, such as `app.example.com` |\n| A web app under a path on your marketing domain | **A website or web app** | The paths, such as `/app`, `/dashboard` |\n| A phone app with a web client | **Both** | The web hostnames or paths |\n\n**A web product must say where it lives.** Your marketing pages and your product are the same PostHog events; without a hostname or a path there is nothing to tell them apart, and every visitor would be counted as someone using the product. Sprid refuses that configuration rather than reporting it.\n\nWhatever you name here is **subtracted from your website visitor numbers**, so a page inside the product is never also counted as a visit.\n\n**A phone app** needs `posthog-react-native` on iOS, iPadOS or Android and rejects explicit web surfaces: set `app_surface: 'web'` on Expo web and `'native'` on devices. Web counts need a browser and exclude reported bots and crawler user agents. Every metric excludes events or people with `is_internal`, `is_test` or `sprid_test = true`. What remains is observed identities, not guaranteed humans.\n\n### Custom properties\n\nAdvanced rules go under **Custom properties**:\n\n```json\n{\"registration\":{\"identity\":{\"scope\":\"event\",\"property\":\"account_id\"}},\"app\":{\"kind\":\"web\",\"hosts\":[\"app.example.com\"]},\"exclude\":[{\"scope\":\"person\",\"property\":\"staff\",\"operator\":\"in\",\"values\":[true]}]}\n```\n\n- `app.kind` is `native`, `web` or `both`, with `app.hosts` and `app.pathPrefixes` saying where a web product lives. This is what the screen above writes.\n- `app.filters` replaces the whole definition with your own rule; all filters must match. Choosing it shows as **Custom rules** on the screen, and the rule is subtracted from web visitors the same way.\n- `exclude` removes matching traffic from every metric.\n- `registration.filters` narrows the registration event, for example `result = success`.\n- `registration.identity` defaults to `person_id`. A configured property must be present and should never change, because it counts accounts.\n- Each rule takes `scope: event|person`, `operator: in|not_in` and string, number or boolean `values`. A missing property fails `in` and passes `not_in`. Property names are literal keys, dots included.\n\nThe same object is `posthogConfig` in REST `PATCH /api/app-profiles/:id`, MCP `upsert_app_profile` and the App Profile JSON used by `sprid init`, where `registration.event` and `registration.since` also live. No credentials or SQL belong in it. After saving, fetch Insights and check its registration notes and totals against your database before trusting a funnel.\n\n## Review suspected automated traffic\n\nRead [Check whether a traffic spike is real](https://sprid.studio/docs/traffic) (`sprid docs traffic`) before saving a rule: a spike or a shared fingerprint alone does not prove bots.\n\nOpen **Web visitors → Review traffic exclusions**, or **Settings → Apps → your app → PostHog → Review traffic**. Pick a UTC date range and a traffic group, preview the effect, then save. Agents use MCP `review_traffic` or `POST /api/app-profiles/:ref/traffic-review?workspaceId=…` with `{\"start\":\"2026-09-18\",\"end\":\"2026-09-19\"}`: end dates exclusive, UTC, at most 31 days. An optional `exclusion` previews a rule against this period and the one before. Save approved rules in `posthogConfig.trafficExclusions` through `upsert_app_profile` or the profile PATCH route, keeping the rest of the configuration. Sprid records who edited it and when.\n\nHow exclusions apply:\n\n- App-specific and date-bounded. Properties inside a rule are AND; enabled rules are OR.\n- Applied to website totals, charts, breakdowns, social attribution and weekly website counts, comparison period included. Remaining visitors are recounted as distinct people, never subtracted.\n- Native app activity and registrations are unchanged, and PostHog’s data is never changed or deleted. **Restore this traffic** disables a rule.\n- Raw connected queries and the marketing-review event inventory stay unfiltered as evidence; Insights shows the corrected figures.\n- Only PostHog is supported today.\n\n## Social traffic and clip comparisons\n\nSave the app’s website URL on its App Profile as well as connecting PostHog. Insights then compares completed-day website sessions with recorded social view gains. Shared bio traffic stays at channel level; only a dedicated tagged link identifies one publish. Older clips with metric activity stay candidates.\n\nCopy the stable bio link and dedicated clip links from Insights. Keep `utm_source`, `utm_medium`, `sprid_account` and, on dedicated links only, `sprid_publish` through any redirects. Never repoint the shared bio link to the newest publish. The optional bio-page HTML export records selections separately and keeps a direct app link; host it on the saved hostname with your existing PostHog setup.\n\nTo capture store-link clicks and website outcomes, add after your PostHog initialization:\n\n```html\n<script src=\"https://sprid.studio/sprid-attribution.js\" defer></script>\n```\n\nIt uses your PostHog client and consent state and sends nothing to Sprid. Call `window.spridAttribution?.track('signup')` or `.track('activation')` only after that action succeeds. `posthogEvents.signup` and `posthogEvents.activation` mappings also count when the event shares the arriving website session. It does not track a native app or join visits to purchases, and a store click is not an install. Missing outcomes stay unmeasured. Redirect-only flows need a beacon before navigating; redirect events are counted apart from sessions. `get_insights` and `sprid insights` return the same evidence as the dashboard.\n\n## If it fails\n\n- **Access denied:** check the key is active, grants your project and has both Read permissions. Reconnect with a corrected key file.\n- **Project not found:** check project number and host together. A US project needs the US host.\n- **Connected but no events:** check the dates and project, then ask your agent to check your app’s tracking is sending events.\n\n## Investigate with this connection\n\nReads `query` (HogQL), `events` and `properties` for the pinned project. Check instrumentation before interpreting events. Event-definition discovery also needs `event_definition:read`, property-definition discovery `property_definition:read`, both restricted to the same project.\n\n```sh\nsprid marketing-review capabilities --app <slug> --source posthog --json\n```\n\nSee [connected queries](https://sprid.studio/docs/queries).\n\n## Sources and verification\n\nSetup and live reads checked 2026-09-09.\n\n- [PostHog personal API keys](https://posthog.com/docs/api/personal-api-keys)\n- [Project identity endpoint](https://posthog.com/docs/api/projects)\n- [HogQL query endpoint](https://posthog.com/docs/api/query)\n- [SDK and framework guides](https://posthog.com/docs/libraries)\n- [React Native screen tracking](https://posthog.com/docs/libraries/react-native)\n- [Identifying users](https://posthog.com/docs/product-analytics/identify)\n- [Funnels](https://posthog.com/docs/product-analytics/funnels)\n",
|
|
373
|
+
"revision": "c23bf372172c52f7"
|
|
374
374
|
},
|
|
375
375
|
{
|
|
376
376
|
"id": "google-analytics",
|
|
@@ -435,7 +435,7 @@ export const GUIDES = [
|
|
|
435
435
|
"id": "plausible",
|
|
436
436
|
"title": "Plausible",
|
|
437
437
|
"summary": "See how many people visit your website, where they came from and which pages they land on.",
|
|
438
|
-
"command": "sprid connect plausible --key
|
|
438
|
+
"command": "sprid connect plausible --key-from-clipboard --site example.com",
|
|
439
439
|
"url": "https://sprid.studio/docs/connect/plausible",
|
|
440
440
|
"sections": [
|
|
441
441
|
{
|
|
@@ -448,19 +448,19 @@ export const GUIDES = [
|
|
|
448
448
|
"id": "click-path",
|
|
449
449
|
"title": "Click path",
|
|
450
450
|
"kind": "steps",
|
|
451
|
-
"markdown": "1. Open [Plausible](https://plausible.io) and switch to the team that owns the site.\n2. **Settings → API keys → New API key**.\n3. Name it `sprid
|
|
451
|
+
"markdown": "1. Open [Plausible](https://plausible.io) and switch to the team that owns the site.\n2. **Settings → API keys → New API key**.\n3. Name it `sprid` and create it. Plausible shows it once.\n4. Copy the key and leave it on your clipboard.\n5. Your **site id** is the domain exactly as it was added to Plausible: no `https://`, no trailing slash. It is the last part of the site’s URL, `plausible.io/<site id>`."
|
|
452
452
|
},
|
|
453
453
|
{
|
|
454
454
|
"id": "then-run",
|
|
455
455
|
"title": "Then run",
|
|
456
456
|
"kind": "command",
|
|
457
|
-
"markdown": "```\nsprid connect plausible --key
|
|
457
|
+
"markdown": "```\nsprid connect plausible --key-from-clipboard --site example.com\n```\n\nSelf-hosting? Add `--host https://analytics.example.com`. Add `--use` to make Plausible the source Sprid reports website traffic from."
|
|
458
458
|
},
|
|
459
459
|
{
|
|
460
460
|
"id": "only-one-provider-answers",
|
|
461
461
|
"title": "Only one provider answers",
|
|
462
462
|
"kind": "detail",
|
|
463
|
-
"markdown": "PostHog, Google Analytics and Plausible all count the same visits to the same site. Sprid reads **one** of them per app and never adds them together, because adding them would overstate your traffic by roughly the overlap, and the overlap is nearly everything.\n\nWith one connected, that one answers. With several, Sprid uses the one you chose with `--use` (PostHog by default, because it also answers the people and registration cards). The others stay connected and idle, and the traffic card says so."
|
|
463
|
+
"markdown": "PostHog, Google Analytics and Plausible all count the same visits to the same site. Sprid reads **one** of them per app and never adds them together, because adding them would overstate your traffic by roughly the overlap, and the overlap is nearly everything.\n\nWith one connected, that one answers. With several, Sprid uses the one you chose with `--use` (PostHog by default, because it also answers the people and registration cards). The others stay connected and idle, and the traffic card says so.\n\nSprid reads the key from your clipboard, saves it and clears the clipboard, so it never shows on screen or lands in a file. Working with an agent? Tell it the key is copied and it runs this for you. Typing it yourself, copy the key last: paste the command into your terminal first, then copy the key, then press Enter. If the clipboard still holds the command, Sprid refuses it; copy the key and run it again. Without clipboard access (a remote shell), save the key to a file and pass `--key <file>` instead."
|
|
464
464
|
},
|
|
465
465
|
{
|
|
466
466
|
"id": "how-to-check-it-worked",
|
|
@@ -487,14 +487,14 @@ export const GUIDES = [
|
|
|
487
487
|
"markdown": "- [Stats API v2 `query`, and the Business-plan requirement](https://plausible.io/docs/stats-api)"
|
|
488
488
|
}
|
|
489
489
|
],
|
|
490
|
-
"markdown": "# Plausible\n\n**What Sprid does with this:** See how many people visit your website, where they came from and which pages they land on.\n\n## You need\n\nA **Business** plan or higher. The Stats API is a Business-plan feature, so a valid key on a cheaper plan is refused with a plan error rather than a key error. An API key also belongs to **one team**, so create it in the team that owns the site.\n\n## Click path\n\n1. Open [Plausible](https://plausible.io) and switch to the team that owns the site.\n2. **Settings → API keys → New API key**.\n3. Name it `sprid
|
|
491
|
-
"revision": "
|
|
490
|
+
"markdown": "# Plausible\n\n**What Sprid does with this:** See how many people visit your website, where they came from and which pages they land on.\n\n## You need\n\nA **Business** plan or higher. The Stats API is a Business-plan feature, so a valid key on a cheaper plan is refused with a plan error rather than a key error. An API key also belongs to **one team**, so create it in the team that owns the site.\n\n## Click path\n\n1. Open [Plausible](https://plausible.io) and switch to the team that owns the site.\n2. **Settings → API keys → New API key**.\n3. Name it `sprid` and create it. Plausible shows it once.\n4. Copy the key and leave it on your clipboard.\n5. Your **site id** is the domain exactly as it was added to Plausible: no `https://`, no trailing slash. It is the last part of the site’s URL, `plausible.io/<site id>`.\n\n## Then run\n\n```\nsprid connect plausible --key-from-clipboard --site example.com\n```\n\nSelf-hosting? Add `--host https://analytics.example.com`. Add `--use` to make Plausible the source Sprid reports website traffic from.\n\n## Only one provider answers\n\nPostHog, Google Analytics and Plausible all count the same visits to the same site. Sprid reads **one** of them per app and never adds them together, because adding them would overstate your traffic by roughly the overlap, and the overlap is nearly everything.\n\nWith one connected, that one answers. With several, Sprid uses the one you chose with `--use` (PostHog by default, because it also answers the people and registration cards). The others stay connected and idle, and the traffic card says so.\n\nSprid reads the key from your clipboard, saves it and clears the clipboard, so it never shows on screen or lands in a file. Working with an agent? Tell it the key is copied and it runs this for you. Typing it yourself, copy the key last: paste the command into your terminal first, then copy the key, then press Enter. If the clipboard still holds the command, Sprid refuses it; copy the key and run it again. Without clipboard access (a remote shell), save the key to a file and pass `--key <file>` instead.\n\n## How to check it worked\n\nRun `sprid status`, then ask your agent: “Read this website’s visitors for the last 28 days through Sprid and say which provider answered.” A saved key confirms setup; only the live read confirms access.\n\n## If it fails\n\n- **Business plan required:** the key is fine; the plan does not include the Stats API. Move to Business, or use PostHog or Google Analytics for this app instead. Nothing needs re-entering afterwards.\n- **Unauthorized:** the key belongs to a different team from the site. Create the key inside the team that owns the site.\n- **Site not found:** the site id is the domain as Plausible has it, with no scheme and no trailing slash.\n- **Rate limited:** Plausible allows 600 requests an hour per key. One reading is well inside that, so check whether another tool shares the key.\n\n## What Sprid can and cannot read here\n\nVisitors, pageviews, visits, bounce rate and visit duration, plus breakdowns by country, region, city, source, channel, campaign, term, page, entry page, exit page, hostname, browser, operating system and device.\n\nPlausible is **cookieless**: it has no person id, so a visitor is its own daily estimate rather than someone Sprid can follow between days. Window totals therefore come from Plausible directly and are never the daily numbers added up. Bounces and session time arrive as a rate and an average, and Sprid turns them back into counts, so they carry Plausible’s rounding. Days are the site’s own time zone, not UTC.\n\n## Sources\n\n- [Stats API v2 `query`, and the Business-plan requirement](https://plausible.io/docs/stats-api)\n",
|
|
491
|
+
"revision": "5a32f8c4a59285a9"
|
|
492
492
|
},
|
|
493
493
|
{
|
|
494
494
|
"id": "umami",
|
|
495
495
|
"title": "Umami",
|
|
496
496
|
"summary": "See how many people visit your website, where they came from and which pages they land on.",
|
|
497
|
-
"command": "sprid connect umami --key
|
|
497
|
+
"command": "sprid connect umami --key-from-clipboard --site 8f2a1c90-4d1e-4b7a-9f33-2c0b5e7a1d44",
|
|
498
498
|
"url": "https://sprid.studio/docs/connect/umami",
|
|
499
499
|
"sections": [
|
|
500
500
|
{
|
|
@@ -507,19 +507,19 @@ export const GUIDES = [
|
|
|
507
507
|
"id": "click-path",
|
|
508
508
|
"title": "Click path",
|
|
509
509
|
"kind": "steps",
|
|
510
|
-
"markdown": "1. Open [Umami](https://cloud.umami.is), or your own instance.\n2. **Settings → API keys → Create API key**. Name it `sprid
|
|
510
|
+
"markdown": "1. Open [Umami](https://cloud.umami.is), or your own instance.\n2. **Settings → API keys → Create API key**. Name it `sprid`. Umami shows it once.\n3. Copy the key and leave it on your clipboard.\n4. **Settings → Websites →** the site **→ Details**. Copy the **Website ID**. It is a uuid such as `8f2a1c90-4d1e-4b7a-9f33-2c0b5e7a1d44`, **not** the domain."
|
|
511
511
|
},
|
|
512
512
|
{
|
|
513
513
|
"id": "then-run",
|
|
514
514
|
"title": "Then run",
|
|
515
515
|
"kind": "command",
|
|
516
|
-
"markdown": "```\nsprid connect umami --key
|
|
516
|
+
"markdown": "```\nsprid connect umami --key-from-clipboard --site 8f2a1c90-4d1e-4b7a-9f33-2c0b5e7a1d44\n```\n\nSelf-hosting? Add `--host https://analytics.example.com`. Sprid appends the `/api` your instance serves under, so either form works. Add `--use` to make Umami the source Sprid reports website traffic from."
|
|
517
517
|
},
|
|
518
518
|
{
|
|
519
519
|
"id": "only-one-provider-answers",
|
|
520
520
|
"title": "Only one provider answers",
|
|
521
521
|
"kind": "detail",
|
|
522
|
-
"markdown": "PostHog, Google Analytics, Plausible and Umami all count the same visits to the same site. Sprid reads **one** of them per app and never adds them together, because adding them would overstate your traffic by roughly the overlap, and the overlap is nearly everything.\n\nWith one connected, that one answers. With several, Sprid uses the one you chose with `--use` (PostHog by default, because it also answers the people and registration cards). The others stay connected and idle, and the traffic card says so."
|
|
522
|
+
"markdown": "PostHog, Google Analytics, Plausible and Umami all count the same visits to the same site. Sprid reads **one** of them per app and never adds them together, because adding them would overstate your traffic by roughly the overlap, and the overlap is nearly everything.\n\nWith one connected, that one answers. With several, Sprid uses the one you chose with `--use` (PostHog by default, because it also answers the people and registration cards). The others stay connected and idle, and the traffic card says so.\n\nSprid reads the key from your clipboard, saves it and clears the clipboard, so it never shows on screen or lands in a file. Working with an agent? Tell it the key is copied and it runs this for you. Typing it yourself, copy the key last: paste the command into your terminal first, then copy the key, then press Enter. If the clipboard still holds the command, Sprid refuses it; copy the key and run it again. Without clipboard access (a remote shell), save the key to a file and pass `--key <file>` instead."
|
|
523
523
|
},
|
|
524
524
|
{
|
|
525
525
|
"id": "how-to-check-it-worked",
|
|
@@ -540,14 +540,14 @@ export const GUIDES = [
|
|
|
540
540
|
"markdown": "Visitors, pageviews, visits, bounces and total time, plus **every** breakdown this product draws: country, region, city, referrer, channel, campaign, term, page, entry page, exit page, hostname, browser, operating system and device. Umami is the only alternative provider that reports an exit page.\n\nUmami is **cookieless**, so window totals come from Umami directly and are never the daily numbers added up. The daily line counts **sessions** rather than distinct people, so it will not sum to the visitor total beside it."
|
|
541
541
|
}
|
|
542
542
|
],
|
|
543
|
-
"markdown": "# Umami\n\n**What Sprid does with this:** See how many people visit your website, where they came from and which pages they land on.\n\nUmami is the one to reach for if Plausible's Business plan is more than you want to pay for an API, or if you self-host.\n\n## You need\n\nAn **API key**. On Umami Cloud that is any plan. Self-hosting, you need a version new enough to have **Settings → API keys**; an older instance cannot be connected at all, whatever key you paste.\n\n## Click path\n\n1. Open [Umami](https://cloud.umami.is), or your own instance.\n2. **Settings → API keys → Create API key**. Name it `sprid
|
|
544
|
-
"revision": "
|
|
543
|
+
"markdown": "# Umami\n\n**What Sprid does with this:** See how many people visit your website, where they came from and which pages they land on.\n\nUmami is the one to reach for if Plausible's Business plan is more than you want to pay for an API, or if you self-host.\n\n## You need\n\nAn **API key**. On Umami Cloud that is any plan. Self-hosting, you need a version new enough to have **Settings → API keys**; an older instance cannot be connected at all, whatever key you paste.\n\n## Click path\n\n1. Open [Umami](https://cloud.umami.is), or your own instance.\n2. **Settings → API keys → Create API key**. Name it `sprid`. Umami shows it once.\n3. Copy the key and leave it on your clipboard.\n4. **Settings → Websites →** the site **→ Details**. Copy the **Website ID**. It is a uuid such as `8f2a1c90-4d1e-4b7a-9f33-2c0b5e7a1d44`, **not** the domain.\n\n## Then run\n\n```\nsprid connect umami --key-from-clipboard --site 8f2a1c90-4d1e-4b7a-9f33-2c0b5e7a1d44\n```\n\nSelf-hosting? Add `--host https://analytics.example.com`. Sprid appends the `/api` your instance serves under, so either form works. Add `--use` to make Umami the source Sprid reports website traffic from.\n\n## Only one provider answers\n\nPostHog, Google Analytics, Plausible and Umami all count the same visits to the same site. Sprid reads **one** of them per app and never adds them together, because adding them would overstate your traffic by roughly the overlap, and the overlap is nearly everything.\n\nWith one connected, that one answers. With several, Sprid uses the one you chose with `--use` (PostHog by default, because it also answers the people and registration cards). The others stay connected and idle, and the traffic card says so.\n\nSprid reads the key from your clipboard, saves it and clears the clipboard, so it never shows on screen or lands in a file. Working with an agent? Tell it the key is copied and it runs this for you. Typing it yourself, copy the key last: paste the command into your terminal first, then copy the key, then press Enter. If the clipboard still holds the command, Sprid refuses it; copy the key and run it again. Without clipboard access (a remote shell), save the key to a file and pass `--key <file>` instead.\n\n## How to check it worked\n\nRun `sprid status`, then ask your agent: “Read this website’s visitors for the last 28 days through Sprid and say which provider answered.” A saved key confirms setup; only the live read confirms access.\n\n## If it fails\n\n- **Unauthorized:** the key cannot see this website, or your self-hosted Umami predates API keys. Check for the **Settings → API keys** screen; if there is none, upgrade first.\n- **Not found:** you probably saved the domain instead of the website id, or left `/api` off a self-hosted host. Sprid adds `/api` for you when you pass `--host`.\n- **Rate limited:** Umami Cloud is limiting requests. The next hourly read picks it up.\n\n## What Sprid can and cannot read here\n\nVisitors, pageviews, visits, bounces and total time, plus **every** breakdown this product draws: country, region, city, referrer, channel, campaign, term, page, entry page, exit page, hostname, browser, operating system and device. Umami is the only alternative provider that reports an exit page.\n\nUmami is **cookieless**, so window totals come from Umami directly and are never the daily numbers added up. The daily line counts **sessions** rather than distinct people, so it will not sum to the visitor total beside it.\n",
|
|
544
|
+
"revision": "12921506ab4331b9"
|
|
545
545
|
},
|
|
546
546
|
{
|
|
547
547
|
"id": "revenuecat",
|
|
548
548
|
"title": "RevenueCat",
|
|
549
549
|
"summary": "See subscription revenue and how it changes over time.",
|
|
550
|
-
"command": "sprid connect revenuecat --app myapp --key
|
|
550
|
+
"command": "sprid connect revenuecat --app myapp --key-from-clipboard --project proj1ab2c3d4",
|
|
551
551
|
"url": "https://sprid.studio/docs/connect/revenuecat",
|
|
552
552
|
"sections": [
|
|
553
553
|
{
|
|
@@ -560,7 +560,7 @@ export const GUIDES = [
|
|
|
560
560
|
"id": "click-path-apprevenuecatcom",
|
|
561
561
|
"title": "Click path (app.revenuecat.com)",
|
|
562
562
|
"kind": "steps",
|
|
563
|
-
"markdown": "1. Select your project → **API keys → Secret API keys → New secret API key**.\n2. Name it `Sprid analytics` and select **V2**.\n3. **Charts metrics permissions**: set **Overview Configuration Access Level** and **Charts Configuration Access Level** to **Read only**.\n4. **Project configuration permissions**: set **Apps Configuration Access Level** to **Read only**. Leave everything else at **No access**.\n5. Click **Generate
|
|
563
|
+
"markdown": "1. Select your project → **API keys → Secret API keys → New secret API key**.\n2. Name it `Sprid analytics` and select **V2**.\n3. **Charts metrics permissions**: set **Overview Configuration Access Level** and **Charts Configuration Access Level** to **Read only**.\n4. **Project configuration permissions**: set **Apps Configuration Access Level** to **Read only**. Leave everything else at **No access**.\n5. Click **Generate**. Use a separate key per app.\n6. Copy the key and leave it on your clipboard."
|
|
564
564
|
},
|
|
565
565
|
{
|
|
566
566
|
"id": "project-id",
|
|
@@ -572,7 +572,7 @@ export const GUIDES = [
|
|
|
572
572
|
"id": "then-run",
|
|
573
573
|
"title": "Then run",
|
|
574
574
|
"kind": "command",
|
|
575
|
-
"markdown": "```sh\nsprid connect revenuecat --app myapp --key
|
|
575
|
+
"markdown": "```sh\nsprid connect revenuecat --app myapp --key-from-clipboard --project proj1ab2c3d4\n```\n\nUse your own app slug and project id. Add `--workspace <slug>` if needed. Keep the key out of chat.\n\nSprid reads the key from your clipboard, saves it and clears the clipboard, so it never shows on screen or lands in a file. Working with an agent? Tell it the key is copied and it runs this for you. Typing it yourself, copy the key last: paste the command into your terminal first, then copy the key, then press Enter. If the clipboard still holds the command, Sprid refuses it; copy the key and run it again. Without clipboard access (a remote shell), save the key to a file and pass `--key <file>` instead."
|
|
576
576
|
},
|
|
577
577
|
{
|
|
578
578
|
"id": "how-to-check-it-worked",
|
|
@@ -605,14 +605,14 @@ export const GUIDES = [
|
|
|
605
605
|
"markdown": "Setup and live reads checked 2026-09-09.\n\n- [RevenueCat API keys](https://www.revenuecat.com/docs/projects/authentication)\n- [API V2 reference](https://www.revenuecat.com/docs/api-v2)"
|
|
606
606
|
}
|
|
607
607
|
],
|
|
608
|
-
"markdown": "# RevenueCat\n\n**What Sprid does with this:** See subscription revenue and how it changes over time.\n\n## You need\n\n**Admin** access to the RevenueCat project, to create a dedicated **V2 secret API key**. The app’s public key and V1 keys do not work.\n\n## Click path (app.revenuecat.com)\n\n1. Select your project → **API keys → Secret API keys → New secret API key**.\n2. Name it `Sprid analytics` and select **V2**.\n3. **Charts metrics permissions**: set **Overview Configuration Access Level** and **Charts Configuration Access Level** to **Read only**.\n4. **Project configuration permissions**: set **Apps Configuration Access Level** to **Read only**. Leave everything else at **No access**.\n5. Click **Generate
|
|
609
|
-
"revision": "
|
|
608
|
+
"markdown": "# RevenueCat\n\n**What Sprid does with this:** See subscription revenue and how it changes over time.\n\n## You need\n\n**Admin** access to the RevenueCat project, to create a dedicated **V2 secret API key**. The app’s public key and V1 keys do not work.\n\n## Click path (app.revenuecat.com)\n\n1. Select your project → **API keys → Secret API keys → New secret API key**.\n2. Name it `Sprid analytics` and select **V2**.\n3. **Charts metrics permissions**: set **Overview Configuration Access Level** and **Charts Configuration Access Level** to **Read only**.\n4. **Project configuration permissions**: set **Apps Configuration Access Level** to **Read only**. Leave everything else at **No access**.\n5. Click **Generate**. Use a separate key per app.\n6. Copy the key and leave it on your clipboard.\n\n## Project id\n\n**Project settings → General → Project ID**, such as `proj1ab2c3d4`. Not the shorter id in the dashboard address.\n\n## Then run\n\n```sh\nsprid connect revenuecat --app myapp --key-from-clipboard --project proj1ab2c3d4\n```\n\nUse your own app slug and project id. Add `--workspace <slug>` if needed. Keep the key out of chat.\n\nSprid reads the key from your clipboard, saves it and clears the clipboard, so it never shows on screen or lands in a file. Working with an agent? Tell it the key is copied and it runs this for you. Typing it yourself, copy the key last: paste the command into your terminal first, then copy the key, then press Enter. If the clipboard still holds the command, Sprid refuses it; copy the key and run it again. Without clipboard access (a remote shell), save the key to a file and pass `--key <file>` instead.\n\n## How to check it worked\n\nAsk your agent: “Check that Sprid can read this app’s RevenueCat overview and daily revenue history. Report any missing dates.” A saved key alone does not confirm access. The overview can work while history does not; missing dates are not zero sales.\n\n## If you also sell on the web\n\nConnect the services that record the other sales. Sprid withholds a combined total when RevenueCat may already include the same Stripe or Paddle sales, or when currencies or definitions differ. RevenueCat Web Billing and your own Stripe are separate merchant accounts, so they never overlap.\n\n## If it fails\n\n- **Key rejected:** check it is an active V2 secret key for this project.\n- **Access denied or history missing:** open the key’s **More → Edit**, check all three Read only settings, then **Submit**. No new key needed.\n- **Project not found:** copy the full Project ID from **Project settings → General**. Key and id must belong to the same project.\n- **Too many requests:** retry later. Another key will not help.\n\n## Investigate with this connection\n\nReads `chart_options`, `chart` and `subscriptions` for the pinned project. Read `chart_options` before picking dimensions, filters or resolution, and keep the returned units. `subscriptions` also needs `customer_information:subscriptions:read` and defaults to production; ask for sandbox to see test purchases.\n\n```sh\nsprid marketing-review capabilities --app <slug> --source revenuecat --json\n```\n\nSee [connected queries](https://sprid.studio/docs/queries).\n\n## Sources and verification\n\nSetup and live reads checked 2026-09-09.\n\n- [RevenueCat API keys](https://www.revenuecat.com/docs/projects/authentication)\n- [API V2 reference](https://www.revenuecat.com/docs/api-v2)\n",
|
|
609
|
+
"revision": "318e11fbdfcb6dec"
|
|
610
610
|
},
|
|
611
611
|
{
|
|
612
612
|
"id": "stripe",
|
|
613
613
|
"title": "Stripe",
|
|
614
614
|
"summary": "Read revenue and subscriptions from your Stripe account.",
|
|
615
|
-
"command": "sprid connect stripe --app <slug> --key
|
|
615
|
+
"command": "sprid connect stripe --app <slug> --key-from-clipboard",
|
|
616
616
|
"url": "https://sprid.studio/docs/connect/stripe",
|
|
617
617
|
"sections": [
|
|
618
618
|
{
|
|
@@ -625,13 +625,13 @@ export const GUIDES = [
|
|
|
625
625
|
"id": "click-path-dashboardstripecom",
|
|
626
626
|
"title": "Click path (dashboard.stripe.com)",
|
|
627
627
|
"kind": "steps",
|
|
628
|
-
"markdown": "1. Open [Stripe](https://dashboard.stripe.com) → **Developers → API keys → Restricted keys → Create restricted key**, and name it `Sprid`.\n2. Set these to **Read**:\n - **Core → Charges** and **Account**\n - **Billing → Subscriptions**, **Invoices** and **Prices**\n3. Leave every other permission at **None**.\n4. Create the key
|
|
628
|
+
"markdown": "1. Open [Stripe](https://dashboard.stripe.com) → **Developers → API keys → Restricted keys → Create restricted key**, and name it `Sprid`.\n2. Set these to **Read**:\n - **Core → Charges** and **Account**\n - **Billing → Subscriptions**, **Invoices** and **Prices**\n3. Leave every other permission at **None**.\n4. Create the key.\n5. Copy the key and leave it on your clipboard."
|
|
629
629
|
},
|
|
630
630
|
{
|
|
631
631
|
"id": "then-run",
|
|
632
632
|
"title": "Then run",
|
|
633
633
|
"kind": "command",
|
|
634
|
-
"markdown": "```\nsprid connect stripe --app <slug> --key
|
|
634
|
+
"markdown": "```\nsprid connect stripe --app <slug> --key-from-clipboard\n```\n\nUse your Sprid app slug. Keep the key out of chat.\n\nSprid reads the key from your clipboard, saves it and clears the clipboard, so it never shows on screen or lands in a file. Working with an agent? Tell it the key is copied and it runs this for you. Typing it yourself, copy the key last: paste the command into your terminal first, then copy the key, then press Enter. If the clipboard still holds the command, Sprid refuses it; copy the key and run it again. Without clipboard access (a remote shell), save the key to a file and pass `--key <file>` instead."
|
|
635
635
|
},
|
|
636
636
|
{
|
|
637
637
|
"id": "how-to-check-it-worked",
|
|
@@ -670,14 +670,14 @@ export const GUIDES = [
|
|
|
670
670
|
"markdown": "- [Restricted API keys](https://docs.stripe.com/keys/restricted-api-keys)\n- [Analytics API and its write requirement](https://docs.stripe.com/data/analytics)\n- [Subscriptions and charges](https://docs.stripe.com/api)"
|
|
671
671
|
}
|
|
672
672
|
],
|
|
673
|
-
"markdown": "# Stripe\n\n**What Sprid does with this:** Read revenue and subscriptions from your Stripe account.\n\n## You need\n\nPermission to create a **restricted API key** in Stripe: live and read-only, beginning with `rk_live_`.\n\n## Click path (dashboard.stripe.com)\n\n1. Open [Stripe](https://dashboard.stripe.com) → **Developers → API keys → Restricted keys → Create restricted key**, and name it `Sprid`.\n2. Set these to **Read**:\n - **Core → Charges** and **Account**\n - **Billing → Subscriptions**, **Invoices** and **Prices**\n3. Leave every other permission at **None**.\n4. Create the key
|
|
674
|
-
"revision": "
|
|
673
|
+
"markdown": "# Stripe\n\n**What Sprid does with this:** Read revenue and subscriptions from your Stripe account.\n\n## You need\n\nPermission to create a **restricted API key** in Stripe: live and read-only, beginning with `rk_live_`.\n\n## Click path (dashboard.stripe.com)\n\n1. Open [Stripe](https://dashboard.stripe.com) → **Developers → API keys → Restricted keys → Create restricted key**, and name it `Sprid`.\n2. Set these to **Read**:\n - **Core → Charges** and **Account**\n - **Billing → Subscriptions**, **Invoices** and **Prices**\n3. Leave every other permission at **None**.\n4. Create the key.\n5. Copy the key and leave it on your clipboard.\n\n## Then run\n\n```\nsprid connect stripe --app <slug> --key-from-clipboard\n```\n\nUse your Sprid app slug. Keep the key out of chat.\n\nSprid reads the key from your clipboard, saves it and clears the clipboard, so it never shows on screen or lands in a file. Working with an agent? Tell it the key is copied and it runs this for you. Typing it yourself, copy the key last: paste the command into your terminal first, then copy the key, then press Enter. If the clipboard still holds the command, Sprid refuses it; copy the key and run it again. Without clipboard access (a remote shell), save the key to a file and pass `--key <file>` instead.\n\n## How to check it worked\n\nAsk your agent: “Read this app’s Stripe revenue through Sprid and check that it is the right account.” A saved key alone does not confirm access.\n\n## What the numbers mean\n\nMRR is current active and past-due subscription prices, with annual plans spread over 12 months. It excludes trials and ignores discounts, proration and tax, so it differs from Stripe’s dashboard. Revenue covers 28 complete calendar days; refunds restate the original charge date. Currencies stay separate.\n\n## If you also use RevenueCat\n\nIf RevenueCat may already count these Stripe sales, Sprid withholds the combined total until the overlap is resolved and currencies and definitions match.\n\n## If it fails\n\n- **Key rejected:** check the copied value is complete and starts with `rk_live_`.\n- **Permission denied:** check every Read permission above, then reconnect with a corrected key.\n- **MRR is zero but sales appear:** one-off purchases count as revenue, not recurring subscriptions.\n\n## Investigate with this connection\n\nReads `subscriptions` and `charges` for the key’s merchant account. Filter by price or customer when several products share it. Amounts keep currency and minor units; Stripe SQL and Analytics are not exposed.\n\n```sh\nsprid marketing-review capabilities --app <slug> --source stripe --json\n```\n\nSee [connected queries](https://sprid.studio/docs/queries).\n\n## Sources\n\n- [Restricted API keys](https://docs.stripe.com/keys/restricted-api-keys)\n- [Analytics API and its write requirement](https://docs.stripe.com/data/analytics)\n- [Subscriptions and charges](https://docs.stripe.com/api)\n",
|
|
674
|
+
"revision": "91b444790a7165cf"
|
|
675
675
|
},
|
|
676
676
|
{
|
|
677
677
|
"id": "polar",
|
|
678
678
|
"title": "Polar",
|
|
679
679
|
"summary": "Read revenue and subscriptions from your Polar account.",
|
|
680
|
-
"command": "sprid connect polar --app <slug> --key
|
|
680
|
+
"command": "sprid connect polar --app <slug> --key-from-clipboard",
|
|
681
681
|
"url": "https://sprid.studio/docs/connect/polar",
|
|
682
682
|
"sections": [
|
|
683
683
|
{
|
|
@@ -690,13 +690,13 @@ export const GUIDES = [
|
|
|
690
690
|
"id": "click-path-polarsh",
|
|
691
691
|
"title": "Click path (polar.sh)",
|
|
692
692
|
"kind": "steps",
|
|
693
|
-
"markdown": "1. Open your organization → **Settings → Developers → New Organization Access Token**, and name it `Sprid`.\n2. Select **metrics:read** only.\n3. Create the token
|
|
693
|
+
"markdown": "1. Open your organization → **Settings → Developers → New Organization Access Token**, and name it `Sprid`.\n2. Select **metrics:read** only.\n3. Create the token. It is shown once.\n4. Copy the token and leave it on your clipboard."
|
|
694
694
|
},
|
|
695
695
|
{
|
|
696
696
|
"id": "then-run",
|
|
697
697
|
"title": "Then run",
|
|
698
698
|
"kind": "command",
|
|
699
|
-
"markdown": "```\nsprid connect polar --app <slug> --key
|
|
699
|
+
"markdown": "```\nsprid connect polar --app <slug> --key-from-clipboard\n```\n\nUse your Sprid app slug. A personal token covering several organizations also needs `--org <organization-id>`.\n\nSprid reads the token from your clipboard, saves it and clears the clipboard, so it never shows on screen or lands in a file. Working with an agent? Tell it the token is copied and it runs this for you. Typing it yourself, copy the token last: paste the command into your terminal first, then copy the token, then press Enter. If the clipboard still holds the command, Sprid refuses it; copy the token and run it again. Without clipboard access (a remote shell), save the token to a file and pass `--key <file>` instead."
|
|
700
700
|
},
|
|
701
701
|
{
|
|
702
702
|
"id": "how-to-check-it-worked",
|
|
@@ -729,14 +729,14 @@ export const GUIDES = [
|
|
|
729
729
|
"markdown": "- [Metrics endpoint](https://polar.sh/docs/api-reference/metrics/get)"
|
|
730
730
|
}
|
|
731
731
|
],
|
|
732
|
-
"markdown": "# Polar\n\n**What Sprid does with this:** Read revenue and subscriptions from your Polar account.\n\n## You need\n\nAccess to your Polar organization’s developer settings.\n\n## Click path (polar.sh)\n\n1. Open your organization → **Settings → Developers → New Organization Access Token**, and name it `Sprid`.\n2. Select **metrics:read** only.\n3. Create the token
|
|
733
|
-
"revision": "
|
|
732
|
+
"markdown": "# Polar\n\n**What Sprid does with this:** Read revenue and subscriptions from your Polar account.\n\n## You need\n\nAccess to your Polar organization’s developer settings.\n\n## Click path (polar.sh)\n\n1. Open your organization → **Settings → Developers → New Organization Access Token**, and name it `Sprid`.\n2. Select **metrics:read** only.\n3. Create the token. It is shown once.\n4. Copy the token and leave it on your clipboard.\n\n## Then run\n\n```\nsprid connect polar --app <slug> --key-from-clipboard\n```\n\nUse your Sprid app slug. A personal token covering several organizations also needs `--org <organization-id>`.\n\nSprid reads the token from your clipboard, saves it and clears the clipboard, so it never shows on screen or lands in a file. Working with an agent? Tell it the token is copied and it runs this for you. Typing it yourself, copy the token last: paste the command into your terminal first, then copy the token, then press Enter. If the clipboard still holds the command, Sprid refuses it; copy the token and run it again. Without clipboard access (a remote shell), save the token to a file and pass `--key <file>` instead.\n\n## How to check it worked\n\nAsk your agent: “Check that Sprid can read revenue and subscriptions for this Polar organization.” A saved token alone does not confirm access.\n\n## What the numbers mean\n\nPolar’s daily revenue and current monthly recurring revenue. Polar supplies no trial count, so that field stays blank.\n\n## If it fails\n\n- **Access denied:** create a replacement token with **metrics:read**, then reconnect.\n- **Wrong or empty results with a personal token:** add `--org <organization-id>`.\n\n## Investigate with this connection\n\nReads `metrics`, `orders` and `subscriptions` for the pinned organization. Product filters separate apps sold through the same organization.\n\n```sh\nsprid marketing-review capabilities --app <slug> --source polar --json\n```\n\nSee [connected queries](https://sprid.studio/docs/queries).\n\n## Sources\n\n- [Metrics endpoint](https://polar.sh/docs/api-reference/metrics/get)\n",
|
|
733
|
+
"revision": "28cc3ae9fa0ab4d9"
|
|
734
734
|
},
|
|
735
735
|
{
|
|
736
736
|
"id": "lemonsqueezy",
|
|
737
737
|
"title": "Lemon Squeezy",
|
|
738
738
|
"summary": "Read sales and subscriptions from your Lemon Squeezy store.",
|
|
739
|
-
"command": "sprid connect lemonsqueezy --app <slug> --key
|
|
739
|
+
"command": "sprid connect lemonsqueezy --app <slug> --key-from-clipboard --store 12345",
|
|
740
740
|
"url": "https://sprid.studio/docs/connect/lemonsqueezy",
|
|
741
741
|
"sections": [
|
|
742
742
|
{
|
|
@@ -749,13 +749,13 @@ export const GUIDES = [
|
|
|
749
749
|
"id": "click-path-applemonsqueezycom",
|
|
750
750
|
"title": "Click path (app.lemonsqueezy.com)",
|
|
751
751
|
"kind": "steps",
|
|
752
|
-
"markdown": "1. **Settings → API**: click **+** and name the key `Sprid`.\n2.
|
|
752
|
+
"markdown": "1. **Settings → API**: click **+** and name the key `Sprid`.\n2. Copy the key and leave it on your clipboard. It is shown once.\n3. **Settings → Stores**: select your store and copy its numeric id from the page address."
|
|
753
753
|
},
|
|
754
754
|
{
|
|
755
755
|
"id": "then-run",
|
|
756
756
|
"title": "Then run",
|
|
757
757
|
"kind": "command",
|
|
758
|
-
"markdown": "```\nsprid connect lemonsqueezy --app <slug> --key
|
|
758
|
+
"markdown": "```\nsprid connect lemonsqueezy --app <slug> --key-from-clipboard --store 12345\n```\n\nUse your Sprid app slug and your store id. The store id is required.\n\nSprid reads the key from your clipboard, saves it and clears the clipboard, so it never shows on screen or lands in a file. Working with an agent? Tell it the key is copied and it runs this for you. Typing it yourself, copy the key last: paste the command into your terminal first, then copy the key, then press Enter. If the clipboard still holds the command, Sprid refuses it; copy the key and run it again. Without clipboard access (a remote shell), save the key to a file and pass `--key <file>` instead."
|
|
759
759
|
},
|
|
760
760
|
{
|
|
761
761
|
"id": "how-to-check-it-worked",
|
|
@@ -788,14 +788,14 @@ export const GUIDES = [
|
|
|
788
788
|
"markdown": "- [The store object](https://docs.lemonsqueezy.com/api/stores/the-store-object)\n- [Getting started with the API](https://docs.lemonsqueezy.com/guides/developer-guide/getting-started)"
|
|
789
789
|
}
|
|
790
790
|
],
|
|
791
|
-
"markdown": "# Lemon Squeezy\n\n**What Sprid does with this:** Read sales and subscriptions from your Lemon Squeezy store.\n\n## You need\n\nAccess to your store’s **Settings → API** page.\n\n## Click path (app.lemonsqueezy.com)\n\n1. **Settings → API**: click **+** and name the key `Sprid`.\n2.
|
|
792
|
-
"revision": "
|
|
791
|
+
"markdown": "# Lemon Squeezy\n\n**What Sprid does with this:** Read sales and subscriptions from your Lemon Squeezy store.\n\n## You need\n\nAccess to your store’s **Settings → API** page.\n\n## Click path (app.lemonsqueezy.com)\n\n1. **Settings → API**: click **+** and name the key `Sprid`.\n2. Copy the key and leave it on your clipboard. It is shown once.\n3. **Settings → Stores**: select your store and copy its numeric id from the page address.\n\n## Then run\n\n```\nsprid connect lemonsqueezy --app <slug> --key-from-clipboard --store 12345\n```\n\nUse your Sprid app slug and your store id. The store id is required.\n\nSprid reads the key from your clipboard, saves it and clears the clipboard, so it never shows on screen or lands in a file. Working with an agent? Tell it the key is copied and it runs this for you. Typing it yourself, copy the key last: paste the command into your terminal first, then copy the key, then press Enter. If the clipboard still holds the command, Sprid refuses it; copy the key and run it again. Without clipboard access (a remote shell), save the key to a file and pass `--key <file>` instead.\n\n## How to check it worked\n\nAsk your agent: “Read this store’s Lemon Squeezy sales through Sprid and confirm the store is correct.” A saved key alone does not confirm access.\n\n## What the numbers mean\n\nRevenue covers 28 complete calendar days: orders plus renewal and update invoices, without duplicate initial invoices, with refunds deducted on the original purchase date. Currencies stay separate. Incomplete pagination makes the total unavailable.\n\nMRR stays unavailable until a complete priced billing schedule exists, because subscriptions carry price IDs, not prices.\n\n## If it fails\n\n- **Key rejected:** create a replacement and reconnect.\n- **Store not found:** check the store id belongs to the account that created the key.\n- **An unexpected error page appears:** retry. If it persists, send the message to [Sprid support](mailto:hello@sprid.studio).\n\n## Investigate with this connection\n\nReads `orders`, `subscriptions` and `subscription-invoices` for the pinned store. Product and variant filters separate apps in the same store.\n\n```sh\nsprid marketing-review capabilities --app <slug> --source lemonsqueezy --json\n```\n\nSee [connected queries](https://sprid.studio/docs/queries).\n\n## Sources\n\n- [The store object](https://docs.lemonsqueezy.com/api/stores/the-store-object)\n- [Getting started with the API](https://docs.lemonsqueezy.com/guides/developer-guide/getting-started)\n",
|
|
792
|
+
"revision": "eb53db72b6df0ee0"
|
|
793
793
|
},
|
|
794
794
|
{
|
|
795
795
|
"id": "paddle",
|
|
796
796
|
"title": "Paddle",
|
|
797
797
|
"summary": "Read revenue and subscriptions from your Paddle account.",
|
|
798
|
-
"command": "sprid connect paddle --app <slug> --key
|
|
798
|
+
"command": "sprid connect paddle --app <slug> --key-from-clipboard",
|
|
799
799
|
"url": "https://sprid.studio/docs/connect/paddle",
|
|
800
800
|
"sections": [
|
|
801
801
|
{
|
|
@@ -808,13 +808,13 @@ export const GUIDES = [
|
|
|
808
808
|
"id": "click-path-vendorspaddlecom",
|
|
809
809
|
"title": "Click path (vendors.paddle.com)",
|
|
810
810
|
"kind": "steps",
|
|
811
|
-
"markdown": "1. **Developer tools → Authentication → New API key**, named `Sprid`.\n2. Select **transaction.read** and **subscription.read** only.\n3. Create the key
|
|
811
|
+
"markdown": "1. **Developer tools → Authentication → New API key**, named `Sprid`.\n2. Select **transaction.read** and **subscription.read** only.\n3. Create the key. It is shown once.\n4. Copy the key and leave it on your clipboard."
|
|
812
812
|
},
|
|
813
813
|
{
|
|
814
814
|
"id": "then-run",
|
|
815
815
|
"title": "Then run",
|
|
816
816
|
"kind": "command",
|
|
817
|
-
"markdown": "```\nsprid connect paddle --app <slug> --key
|
|
817
|
+
"markdown": "```\nsprid connect paddle --app <slug> --key-from-clipboard\n```\n\nUse your Sprid app slug. Add `--env sandbox` for a sandbox key.\n\nSprid reads the key from your clipboard, saves it and clears the clipboard, so it never shows on screen or lands in a file. Working with an agent? Tell it the key is copied and it runs this for you. Typing it yourself, copy the key last: paste the command into your terminal first, then copy the key, then press Enter. If the clipboard still holds the command, Sprid refuses it; copy the key and run it again. Without clipboard access (a remote shell), save the key to a file and pass `--key <file>` instead."
|
|
818
818
|
},
|
|
819
819
|
{
|
|
820
820
|
"id": "how-to-check-it-worked",
|
|
@@ -847,14 +847,14 @@ export const GUIDES = [
|
|
|
847
847
|
"markdown": "- [List transactions](https://developer.paddle.com/api-reference/transactions/list-transactions)\n- [API authentication](https://developer.paddle.com/api-reference/about/authentication)"
|
|
848
848
|
}
|
|
849
849
|
],
|
|
850
|
-
"markdown": "# Paddle\n\n**What Sprid does with this:** Read revenue and subscriptions from your Paddle account.\n\n## You need\n\n**Paddle Billing** access (Paddle Classic keys do not work), and whether the key is for your live account or sandbox.\n\n## Click path (vendors.paddle.com)\n\n1. **Developer tools → Authentication → New API key**, named `Sprid`.\n2. Select **transaction.read** and **subscription.read** only.\n3. Create the key
|
|
851
|
-
"revision": "
|
|
850
|
+
"markdown": "# Paddle\n\n**What Sprid does with this:** Read revenue and subscriptions from your Paddle account.\n\n## You need\n\n**Paddle Billing** access (Paddle Classic keys do not work), and whether the key is for your live account or sandbox.\n\n## Click path (vendors.paddle.com)\n\n1. **Developer tools → Authentication → New API key**, named `Sprid`.\n2. Select **transaction.read** and **subscription.read** only.\n3. Create the key. It is shown once.\n4. Copy the key and leave it on your clipboard.\n\n## Then run\n\n```\nsprid connect paddle --app <slug> --key-from-clipboard\n```\n\nUse your Sprid app slug. Add `--env sandbox` for a sandbox key.\n\nSprid reads the key from your clipboard, saves it and clears the clipboard, so it never shows on screen or lands in a file. Working with an agent? Tell it the key is copied and it runs this for you. Typing it yourself, copy the key last: paste the command into your terminal first, then copy the key, then press Enter. If the clipboard still holds the command, Sprid refuses it; copy the key and run it again. Without clipboard access (a remote shell), save the key to a file and pass `--key <file>` instead.\n\n## How to check it worked\n\nAsk your agent: “Check that Sprid can read this Paddle account’s completed sales and active subscriptions.” A saved key alone does not confirm access.\n\n## What the numbers mean\n\nRevenue covers 28 complete days of gross completed transactions: tax included, before refunds, credits, chargebacks and Paddle’s fees, so it will exceed your payout. MRR is current active subscription prices over their billing periods. Currencies stay separate; incomplete pagination makes a total unavailable.\n\n## If it fails\n\n- **Access denied:** a sandbox key needs `--env sandbox`.\n- **A permission is missing:** create a replacement key with both permissions, then reconnect.\n- **Revenue exceeds your payout:** expected. Compare against customer payments, not what is left after tax and fees.\n\n## Investigate with this connection\n\nReads `transactions` and `subscriptions` for the key’s merchant account, live or sandbox as saved. Amounts are minor-unit strings. Subscription creation-date filters apply per page, so keep paginating past pages with no matches.\n\n```sh\nsprid marketing-review capabilities --app <slug> --source paddle --json\n```\n\nSee [connected queries](https://sprid.studio/docs/queries).\n\n## Sources\n\n- [List transactions](https://developer.paddle.com/api-reference/transactions/list-transactions)\n- [API authentication](https://developer.paddle.com/api-reference/about/authentication)\n",
|
|
851
|
+
"revision": "ea9a2a09dfef8f80"
|
|
852
852
|
},
|
|
853
853
|
{
|
|
854
854
|
"id": "cloudflare",
|
|
855
855
|
"title": "Cloudflare (optional)",
|
|
856
856
|
"summary": "Read traffic reports for your website.",
|
|
857
|
-
"command": "sprid connect cloudflare --
|
|
857
|
+
"command": "sprid connect cloudflare --key-from-clipboard --zone 0123456789abcdef0123456789abcdef",
|
|
858
858
|
"url": "https://sprid.studio/docs/connect/cloudflare",
|
|
859
859
|
"sections": [
|
|
860
860
|
{
|
|
@@ -867,7 +867,7 @@ export const GUIDES = [
|
|
|
867
867
|
"id": "click-path-dashcloudflarecom",
|
|
868
868
|
"title": "Click path (dash.cloudflare.com)",
|
|
869
869
|
"kind": "steps",
|
|
870
|
-
"markdown": "1. Open [Cloudflare](https://dash.cloudflare.com) → profile icon → **My Profile → API Tokens**.\n2. **Create Token → Custom token → Get started**, named `Sprid`.\n3. **Permissions**:\n - **Zone → Analytics → Read**\n - **Account → Account Analytics → Read**\n4. **Zone Resources → Include → Specific zone**: your domain.\n5. **Account Resources → Include**: your account.\n6. Leave **Client IP Address Filtering** and **TTL** empty. **Continue to summary → Create Token**.\n7.
|
|
870
|
+
"markdown": "1. Open [Cloudflare](https://dash.cloudflare.com) → profile icon → **My Profile → API Tokens**.\n2. **Create Token → Custom token → Get started**, named `Sprid`.\n3. **Permissions**:\n - **Zone → Analytics → Read**\n - **Account → Account Analytics → Read**\n4. **Zone Resources → Include → Specific zone**: your domain.\n5. **Account Resources → Include**: your account.\n6. Leave **Client IP Address Filtering** and **TTL** empty. **Continue to summary → Create Token**.\n7. Copy the token and leave it on your clipboard. It is shown once.\n\nFor a connection that survives you leaving the team, create an account-owned token instead under **Manage Account → Account API Tokens**, with the same permissions."
|
|
871
871
|
},
|
|
872
872
|
{
|
|
873
873
|
"id": "zone-id",
|
|
@@ -879,7 +879,7 @@ export const GUIDES = [
|
|
|
879
879
|
"id": "then-run",
|
|
880
880
|
"title": "Then run",
|
|
881
881
|
"kind": "command",
|
|
882
|
-
"markdown": "```\nsprid connect cloudflare --
|
|
882
|
+
"markdown": "```\nsprid connect cloudflare --key-from-clipboard --zone 0123456789abcdef0123456789abcdef\n```\n\nUse your own Zone ID. Keep the token out of chat.\n\nSprid reads the token from your clipboard, saves it and clears the clipboard, so it never shows on screen or lands in a file. Working with an agent? Tell it the token is copied and it runs this for you. Typing it yourself, copy the token last: paste the command into your terminal first, then copy the token, then press Enter. If the clipboard still holds the command, Sprid refuses it; copy the token and run it again. Without clipboard access (a remote shell), save the token to a file and pass `--token <file>` instead."
|
|
883
883
|
},
|
|
884
884
|
{
|
|
885
885
|
"id": "how-to-check-it-worked",
|
|
@@ -906,8 +906,8 @@ export const GUIDES = [
|
|
|
906
906
|
"markdown": "- [Create an API token](https://developers.cloudflare.com/fundamentals/api/get-started/create-token/)\n- [GraphQL Analytics API token permissions](https://developers.cloudflare.com/analytics/graphql-api/getting-started/authentication/api-token-auth/)\n- [Find zone and account ids](https://developers.cloudflare.com/fundamentals/account/find-account-and-zone-ids/)\n- [Verify a token](https://developers.cloudflare.com/api/resources/user/subresources/tokens/methods/verify/)"
|
|
907
907
|
}
|
|
908
908
|
],
|
|
909
|
-
"markdown": "# Cloudflare (optional)\n\n**What Sprid does with this:** Read traffic reports for your website.\n\n## You need\n\nAccess to your domain’s Cloudflare account and permission to create a token. Enable **Web Analytics** for visitor reports; basic request counts include bots.\n\n## Click path (dash.cloudflare.com)\n\n1. Open [Cloudflare](https://dash.cloudflare.com) → profile icon → **My Profile → API Tokens**.\n2. **Create Token → Custom token → Get started**, named `Sprid`.\n3. **Permissions**:\n - **Zone → Analytics → Read**\n - **Account → Account Analytics → Read**\n4. **Zone Resources → Include → Specific zone**: your domain.\n5. **Account Resources → Include**: your account.\n6. Leave **Client IP Address Filtering** and **TTL** empty. **Continue to summary → Create Token**.\n7.
|
|
910
|
-
"revision": "
|
|
909
|
+
"markdown": "# Cloudflare (optional)\n\n**What Sprid does with this:** Read traffic reports for your website.\n\n## You need\n\nAccess to your domain’s Cloudflare account and permission to create a token. Enable **Web Analytics** for visitor reports; basic request counts include bots.\n\n## Click path (dash.cloudflare.com)\n\n1. Open [Cloudflare](https://dash.cloudflare.com) → profile icon → **My Profile → API Tokens**.\n2. **Create Token → Custom token → Get started**, named `Sprid`.\n3. **Permissions**:\n - **Zone → Analytics → Read**\n - **Account → Account Analytics → Read**\n4. **Zone Resources → Include → Specific zone**: your domain.\n5. **Account Resources → Include**: your account.\n6. Leave **Client IP Address Filtering** and **TTL** empty. **Continue to summary → Create Token**.\n7. Copy the token and leave it on your clipboard. It is shown once.\n\nFor a connection that survives you leaving the team, create an account-owned token instead under **Manage Account → Account API Tokens**, with the same permissions.\n\n## Zone id\n\nYour domain → **Overview** → **API** card: copy **Zone ID** and **Account ID**. The Zone ID goes in the command; ask your agent to save the Account ID in your Sprid app profile.\n\n## Then run\n\n```\nsprid connect cloudflare --key-from-clipboard --zone 0123456789abcdef0123456789abcdef\n```\n\nUse your own Zone ID. Keep the token out of chat.\n\nSprid reads the token from your clipboard, saves it and clears the clipboard, so it never shows on screen or lands in a file. Working with an agent? Tell it the token is copied and it runs this for you. Typing it yourself, copy the token last: paste the command into your terminal first, then copy the token, then press Enter. If the clipboard still holds the command, Sprid refuses it; copy the token and run it again. Without clipboard access (a remote shell), save the token to a file and pass `--token <file>` instead.\n\n## How to check it worked\n\nRun `sprid status`, then ask your agent: “Check that Sprid can read recent Cloudflare traffic for this domain.” The check names the domain and says whether visitor reports or only request counts are available.\n\n## If it fails\n\n- **Access denied:** **API Tokens → ⋯ → Edit**. Check both Read permissions and the selected domain and account.\n- **Domain not found:** use the Zone ID, not the domain name.\n- **Visitor reports are empty:** enable **Analytics & Logs → Web Analytics** and check the tracking snippet is on your site.\n\n## Investigate with this connection\n\nReads `rum` and `http`. RUM is pinned to the saved account and hostname; beacon traffic is sampled and not verified human.\n\n```sh\nsprid marketing-review capabilities --app <slug> --source cloudflare --json\n```\n\nSee [connected queries](https://sprid.studio/docs/queries).\n\n## Sources\n\n- [Create an API token](https://developers.cloudflare.com/fundamentals/api/get-started/create-token/)\n- [GraphQL Analytics API token permissions](https://developers.cloudflare.com/analytics/graphql-api/getting-started/authentication/api-token-auth/)\n- [Find zone and account ids](https://developers.cloudflare.com/fundamentals/account/find-account-and-zone-ids/)\n- [Verify a token](https://developers.cloudflare.com/api/resources/user/subresources/tokens/methods/verify/)\n",
|
|
910
|
+
"revision": "c34f4dd6420981c8"
|
|
911
911
|
},
|
|
912
912
|
{
|
|
913
913
|
"id": "instagram",
|