@buygolinks/mcp 0.28.0 → 0.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -16
- package/package.json +5 -7
- package/src/index.js +544 -79
package/src/index.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* Auth: BUYGOLINKS_API_KEY env var (personal access token, format bgl_…).
|
|
10
10
|
* Create one at https://app.buygolinks.com/settings/account.
|
|
11
11
|
*/
|
|
12
|
+
import { readFileSync } from 'node:fs';
|
|
12
13
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
13
14
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
14
15
|
import {
|
|
@@ -39,16 +40,32 @@ async function api(path, init = {}) {
|
|
|
39
40
|
try { body = text ? JSON.parse(text) : null; } catch { body = text; }
|
|
40
41
|
if (!r.ok) {
|
|
41
42
|
const code = body?.error || r.statusText || `http_${r.status}`;
|
|
42
|
-
|
|
43
|
+
const detail = body?.message ? ` (${body.message})` : '';
|
|
44
|
+
throw new Error(`buygolinks API ${r.status}: ${code}${detail}`);
|
|
43
45
|
}
|
|
44
46
|
return body;
|
|
45
47
|
}
|
|
46
48
|
|
|
49
|
+
// Query string from the args that are set, skipping undefined, null and ''.
|
|
50
|
+
function qsFrom(args, keys) {
|
|
51
|
+
const qs = new URLSearchParams();
|
|
52
|
+
for (const k of keys) {
|
|
53
|
+
const v = args?.[k];
|
|
54
|
+
if (v !== undefined && v !== null && v !== '') qs.set(k, String(v));
|
|
55
|
+
}
|
|
56
|
+
const q = qs.toString();
|
|
57
|
+
return q ? `?${q}` : '';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const PKG_VERSION = JSON.parse(
|
|
61
|
+
readFileSync(new URL('../package.json', import.meta.url), 'utf8'),
|
|
62
|
+
).version;
|
|
63
|
+
|
|
47
64
|
const TOOLS = [
|
|
48
65
|
{
|
|
49
66
|
name: 'create_link',
|
|
50
67
|
description:
|
|
51
|
-
'Create a new branded short link on buygolinks.com. The link redirects to exactly the destination URL you provide
|
|
68
|
+
'Create a new branded short link on buygolinks.com. The link redirects to exactly the destination URL you provide. For Amazon links, include your own Associates tag in the URL (e.g. ?tag=yourtag-20); buygolinks passes it through untouched and does not rewrite or geo-route Amazon URLs. By default, the link is created under the user\'s first active custom domain (if any); pass `domain_id` to pin it to a specific one. Returns the short URL and link metadata (including `short_domain`, the hostname the link will resolve at).',
|
|
52
69
|
inputSchema: {
|
|
53
70
|
type: 'object',
|
|
54
71
|
properties: {
|
|
@@ -75,7 +92,7 @@ const TOOLS = [
|
|
|
75
92
|
{
|
|
76
93
|
name: 'update_link',
|
|
77
94
|
description:
|
|
78
|
-
'Update an existing short link. Supports changing where it redirects (destination_url), renaming (title), changing the campaign source label, swapping the branded domain it resolves under (domain_id), setting a memorable custom alias (alias), archiving/unarchiving, and password-protecting the link. Pass `destination_url` to repoint the link at a new target
|
|
95
|
+
'Update an existing short link. Supports changing where it redirects (destination_url), renaming (title, which also renames the offer paired to this link\'s destination URL), changing the campaign source label, swapping the branded domain it resolves under (domain_id), setting a memorable custom alias (alias), archiving/unarchiving, and password-protecting the link. Pass `destination_url` to repoint the link at a new target. The short code, alias, and click history are preserved; only the redirect destination moves (the ASIN is re-extracted automatically for Amazon /dp/ URLs). Pass `alias` to give the link a memorable URL slug on its custom domain (e.g. alias="summer-sale" → https://click.example.com/summer-sale). Aliases require the link to be on a custom domain. Setting `alias` REPLACES any previous alias. The old alias stops resolving immediately and is freed for any other link to claim. The link\'s original random short code (link.code) keeps working forever regardless of alias changes. Pass alias="" or alias=null to remove the alias. Changing the link\'s `domain_id` clears the alias automatically. To set or change the password, pass `password` as a non-empty string. To remove password protection, pass an empty string for `password`. To move the link back to the platform default (buygolinks.com), pass `domain_id: null`. Returns the updated link row including `short_domain`, `alias`, and `password_protected` (boolean; the plaintext is never returned).',
|
|
79
96
|
inputSchema: {
|
|
80
97
|
type: 'object',
|
|
81
98
|
properties: {
|
|
@@ -94,7 +111,7 @@ const TOOLS = [
|
|
|
94
111
|
{
|
|
95
112
|
name: 'refresh_link_image',
|
|
96
113
|
description:
|
|
97
|
-
'Re-attempt fetching the product/preview image for an existing short link. Re-runs the same best-effort scrape used at link creation
|
|
114
|
+
'Re-attempt fetching the product/preview image for an existing short link. Re-runs the same best-effort scrape used at link creation: the Amazon product-image scraper for Amazon /dp/ links, or a generic Open Graph (og:image / twitter:image) scrape for any other URL, then rehosts the result on buygolinks storage. Use this when a link was created without an image (the thumbnail shows a letter/placeholder) or to update a stale image. Returns { found: true, link } with the updated `product_image_url` on success, or { found: false } if no image could be fetched this time (the existing image, if any, is left untouched; try again later).',
|
|
98
115
|
inputSchema: {
|
|
99
116
|
type: 'object',
|
|
100
117
|
properties: {
|
|
@@ -103,6 +120,141 @@ const TOOLS = [
|
|
|
103
120
|
required: ['link_id'],
|
|
104
121
|
},
|
|
105
122
|
},
|
|
123
|
+
{
|
|
124
|
+
name: 'delete_link',
|
|
125
|
+
description:
|
|
126
|
+
'Permanently delete a short link and its click history. The short URL and any alias stop resolving at once. This cannot be undone: to take a link out of use but keep its stats, use update_link with archived=true instead. Confirm with the user before calling this.',
|
|
127
|
+
inputSchema: {
|
|
128
|
+
type: 'object',
|
|
129
|
+
properties: {
|
|
130
|
+
link_id: { type: 'number', description: 'The numeric id of the link (from list_links).' },
|
|
131
|
+
},
|
|
132
|
+
required: ['link_id'],
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
name: 'get_link_pixels',
|
|
137
|
+
description:
|
|
138
|
+
'Get which tracking pixels fire when a short link is clicked. Returns { pixel_ids, events, overridden }. When `overridden` is false the link has no selection of its own and fires ALL of the account\'s active pixels. `events` maps a pixel id to the event this link fires it as, when that differs from the pixel\'s default_event.',
|
|
139
|
+
inputSchema: {
|
|
140
|
+
type: 'object',
|
|
141
|
+
properties: { link_id: { type: 'number' } },
|
|
142
|
+
required: ['link_id'],
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
name: 'set_link_pixels',
|
|
147
|
+
description:
|
|
148
|
+
'Choose which tracking pixels fire when a short link is clicked (ids from list_tracking_pixels). Replaces the link\'s whole selection. With overridden=true the link fires exactly these pixels (an empty list means none). With overridden=false, or an empty list and no overridden, the link goes back to firing every active account pixel. `events` optionally sets the event per pixel for this link, e.g. {"12": "ViewContent"}; it must be valid for that pixel\'s platform.',
|
|
149
|
+
inputSchema: {
|
|
150
|
+
type: 'object',
|
|
151
|
+
properties: {
|
|
152
|
+
link_id: { type: 'number' },
|
|
153
|
+
pixel_ids: { type: 'array', items: { type: 'number' } },
|
|
154
|
+
overridden: { type: 'boolean', description: 'Default: true when pixel_ids is not empty.' },
|
|
155
|
+
events: { type: 'object', additionalProperties: { type: 'string' }, description: 'Per pixel event for this link, keyed by pixel id.' },
|
|
156
|
+
},
|
|
157
|
+
required: ['link_id', 'pixel_ids'],
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
name: 'set_link_collection',
|
|
162
|
+
description: 'Put a short link in a collection (from list_collections), or pass collection_id null to take it out. A link is in at most one collection.',
|
|
163
|
+
inputSchema: {
|
|
164
|
+
type: 'object',
|
|
165
|
+
properties: {
|
|
166
|
+
link_id: { type: 'number' },
|
|
167
|
+
collection_id: { type: ['number', 'null'] },
|
|
168
|
+
},
|
|
169
|
+
required: ['link_id', 'collection_id'],
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
// ─── Collections ────────────────────────────────────────────────────────
|
|
174
|
+
{
|
|
175
|
+
name: 'list_collections',
|
|
176
|
+
description:
|
|
177
|
+
'List the user\'s collections (folders of short links) with each one\'s link and click counts, starred first. A collection can also be a ROTATOR: with rotator_enabled, https://buygolinks.com/c/{rotator_alias} sends each visitor to a random link in the collection.',
|
|
178
|
+
inputSchema: { type: 'object', properties: {} },
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
name: 'get_collection',
|
|
182
|
+
description:
|
|
183
|
+
'Get one collection with its links (each with its short_domain and clicks) and click analytics for the window: totals (links, clicks, unique_clicks, countries), by_day, by_country, by_device, by_browser, by_referrer, by_language and the 25 most recent clicks.',
|
|
184
|
+
inputSchema: {
|
|
185
|
+
type: 'object',
|
|
186
|
+
properties: {
|
|
187
|
+
collection_id: { type: 'number' },
|
|
188
|
+
days: { type: 'number', description: 'Window size in days (1 to 90). Defaults to 14.' },
|
|
189
|
+
},
|
|
190
|
+
required: ['collection_id'],
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
name: 'create_collection',
|
|
195
|
+
description:
|
|
196
|
+
'Create a collection to group short links. Add links to it with set_link_collection. Set rotator_enabled to get a rotator URL (https://buygolinks.com/c/{rotator_alias}) that sends each visitor to a random link in the collection; the alias is generated when not given.',
|
|
197
|
+
inputSchema: {
|
|
198
|
+
type: 'object',
|
|
199
|
+
properties: {
|
|
200
|
+
name: { type: 'string', description: 'Up to 80 characters.' },
|
|
201
|
+
description: { type: 'string', description: 'Optional, up to 280 characters.' },
|
|
202
|
+
color: { type: 'string', description: 'One of #ef4444 #f97316 #f59e0b #eab308 #84cc16 #22c55e #14b8a6 #3b82f6 #1d4ed8 #6d28d9 #8b5cf6 #ec4899 #be185d #92400e #0f766e #000000 #fb7185 #0d9488. Anything else becomes #ef4444.' },
|
|
203
|
+
starred: { type: 'boolean', description: 'Starred collections show in the app\'s Starred menu.' },
|
|
204
|
+
rotator_enabled: { type: 'boolean' },
|
|
205
|
+
rotator_alias: { type: 'string', description: 'Optional rotator path, 8 to 24 letters, numbers, dashes or underscores. 409 alias_taken when in use.' },
|
|
206
|
+
},
|
|
207
|
+
required: ['name'],
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
name: 'update_collection',
|
|
212
|
+
description: 'Rename a collection or change its description, color, star, or rotator. Turning the rotator off keeps its alias, so turning it back on restores the same URL.',
|
|
213
|
+
inputSchema: {
|
|
214
|
+
type: 'object',
|
|
215
|
+
properties: {
|
|
216
|
+
collection_id: { type: 'number' },
|
|
217
|
+
name: { type: 'string' },
|
|
218
|
+
description: { type: 'string' },
|
|
219
|
+
color: { type: 'string' },
|
|
220
|
+
starred: { type: 'boolean' },
|
|
221
|
+
rotator_enabled: { type: 'boolean' },
|
|
222
|
+
rotator_alias: { type: 'string', description: '8 to 24 letters, numbers, dashes or underscores.' },
|
|
223
|
+
},
|
|
224
|
+
required: ['collection_id'],
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
name: 'delete_collection',
|
|
229
|
+
description: 'Delete a collection. Its links are kept and simply leave the collection; its rotator URL stops working.',
|
|
230
|
+
inputSchema: { type: 'object', properties: { collection_id: { type: 'number' } }, required: ['collection_id'] },
|
|
231
|
+
},
|
|
232
|
+
|
|
233
|
+
// ─── Account ────────────────────────────────────────────────────────────
|
|
234
|
+
{
|
|
235
|
+
name: 'get_account',
|
|
236
|
+
description:
|
|
237
|
+
'Get the signed-in account: email, plan and billing period, current_period_end and cancels_at, monthly usage (clicks_used_this_month against plan_click_limit, pageview_usage {used, limit, remaining, over, resets_at}), the report time zone (`timezone`, null means each browser uses its own), the dashboard\'s default range (tracker_range_days) and the universal postback_url.',
|
|
238
|
+
inputSchema: { type: 'object', properties: {} },
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
name: 'list_api_keys',
|
|
242
|
+
description: 'List the account\'s API keys: id, label, prefix (the first 8 characters after bgl_, to tell keys apart), last_used_at and created_at. The keys themselves are never returned.',
|
|
243
|
+
inputSchema: { type: 'object', properties: {} },
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
name: 'create_api_key',
|
|
247
|
+
description: 'Create a new API key. The full key (`token`, bgl_...) is returned ONCE and cannot be shown again, so hand it to the user straight away and tell them to store it.',
|
|
248
|
+
inputSchema: {
|
|
249
|
+
type: 'object',
|
|
250
|
+
properties: { label: { type: 'string', description: 'Name to tell the key apart, e.g. "Cursor on laptop".' } },
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
name: 'delete_api_key',
|
|
255
|
+
description: 'Revoke an API key at once. Anything using it starts getting 401s, including this MCP server if it is the key it runs on. Confirm with the user first.',
|
|
256
|
+
inputSchema: { type: 'object', properties: { api_key_id: { type: 'number' } }, required: ['api_key_id'] },
|
|
257
|
+
},
|
|
106
258
|
{
|
|
107
259
|
name: 'list_links',
|
|
108
260
|
description:
|
|
@@ -139,7 +291,7 @@ const TOOLS = [
|
|
|
139
291
|
{
|
|
140
292
|
name: 'get_overview_stats',
|
|
141
293
|
description:
|
|
142
|
-
'Get account-wide analytics for the user\'s branded short LINKS only: total link CLICKS, clicks by day, top countries, devices, browsers, referrers, peak hour and weekday. This covers the Links section ONLY
|
|
294
|
+
'Get account-wide analytics for the user\'s branded short LINKS only: total link CLICKS, clicks by day, top countries, devices, browsers, referrers, peak hour and weekday. This covers the Links section ONLY. It does NOT include Tracker page views, site-property traffic, or campaign visits/conversions. For PAGE VIEWS (site properties) or campaign traffic use the Tracker tools instead: get_tracker_overview (clicks-vs-pageviews unified rollup), list_properties, or get_property_stats. If the user asks for "clicks AND page views" together, call BOTH this tool and get_tracker_overview and combine them, because they read from separate event streams. Use this for "how are my links doing" type questions.',
|
|
143
295
|
inputSchema: {
|
|
144
296
|
type: 'object',
|
|
145
297
|
properties: {
|
|
@@ -183,6 +335,31 @@ const TOOLS = [
|
|
|
183
335
|
required: ['domain_id'],
|
|
184
336
|
},
|
|
185
337
|
},
|
|
338
|
+
{
|
|
339
|
+
name: 'check_domain_dns',
|
|
340
|
+
description:
|
|
341
|
+
'Detect where a custom domain\'s DNS is hosted (Cloudflare, GoDaddy, Namecheap, Route 53, and others) from its public nameservers, and report the easiest way to point it at buygolinks. `one_click.method` is `domain_connect` (send the user to `one_click.url` to approve the change at their provider), `cloudflare_oauth` (tell the user to open Properties in the app and click Connect Cloudflare on this domain, which needs a browser), or null (the user adds the CNAME by hand, at `dns_url` when known). On Cloudflare, `one_click.token_url` is also set, for connect_domain_cloudflare when the user prefers an agent to do it.',
|
|
342
|
+
inputSchema: {
|
|
343
|
+
type: 'object',
|
|
344
|
+
properties: {
|
|
345
|
+
domain_id: { type: 'number', description: 'The numeric id of the domain (from list_domains / create_domain).' },
|
|
346
|
+
},
|
|
347
|
+
required: ['domain_id'],
|
|
348
|
+
},
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
name: 'connect_domain_cloudflare',
|
|
352
|
+
description:
|
|
353
|
+
'Add the required CNAME to the user\'s own Cloudflare zone without a browser, using an API token they created with Zone Read and DNS Edit (form at `one_click.token_url`). Prefer the Connect Cloudflare button in the app when the user can click it, since it needs no token. The token is used for this one request and is never stored. Returns `action`: `created`, or `already_set` when the record is already correct. Never overwrites an existing record: a different record on the hostname returns error `record_exists`. Other errors: `invalid_token`, `zone_not_found`, `token_missing_dns`. Call verify_domain afterward to watch status move to active.',
|
|
354
|
+
inputSchema: {
|
|
355
|
+
type: 'object',
|
|
356
|
+
properties: {
|
|
357
|
+
domain_id: { type: 'number', description: 'The numeric id of the domain.' },
|
|
358
|
+
api_token: { type: 'string', description: 'The user\'s Cloudflare API token.' },
|
|
359
|
+
},
|
|
360
|
+
required: ['domain_id', 'api_token'],
|
|
361
|
+
},
|
|
362
|
+
},
|
|
186
363
|
{
|
|
187
364
|
name: 'delete_domain',
|
|
188
365
|
description:
|
|
@@ -200,27 +377,36 @@ const TOOLS = [
|
|
|
200
377
|
{
|
|
201
378
|
name: 'get_universal_postback_url',
|
|
202
379
|
description:
|
|
203
|
-
'Get the account\'s UNIVERSAL postback URL (Voluum-style): ONE URL that works for every campaign and every affiliate network. It matches the click by click_id alone and credits whichever campaign the click belongs to, so the user pastes it once into any network\'s S2S settings or their own webhook (n8n, Zapier, a store\'s order webhook) and never updates it again. Format: https://buygolinks.com/postback/u/{token}?clickid={clickid}&payout={payout}¤cy={currency}&txid={txid}
|
|
380
|
+
'Get the account\'s UNIVERSAL postback URL (Voluum-style): ONE URL that works for every campaign and every affiliate network. It matches the click by click_id alone and credits whichever campaign the click belongs to, so the user pastes it once into any network\'s S2S settings or their own webhook (n8n, Zapier, a store\'s order webhook) and never updates it again. Format: https://buygolinks.com/postback/u/{token}?clickid={clickid}&payout={payout}¤cy={currency}&txid={txid} where payout/currency/txid are optional; `cid` is accepted as an alias for `clickid`. It is the account\'s ONLY postback URL: campaigns and networks have no postback URL of their own. Conversions are deduped by txid: at most one conversion per (account, txid). A re-fired postback with the same txid is ignored, and a re-fire with a different payout updates the stored conversion\'s revenue instead of stacking a second one. With no `event` param the postback records a sale. Add `event=refund` or `event=chargeback` to subtract from revenue (matched by click, so the amount can be sent as 67 or -67, and leaving it out reverses everything booked on the click), or `event={slug}` for one of the user\'s custom events (see list_custom_events). An event that is blank or unknown gets a 400 and records nothing.',
|
|
204
381
|
inputSchema: { type: 'object', properties: {} },
|
|
205
382
|
},
|
|
383
|
+
{
|
|
384
|
+
name: 'set_account_timezone',
|
|
385
|
+
description:
|
|
386
|
+
'Set the time zone the account\'s reports count days and hours in: Today and Yesterday on the dashboard, by_day, by_hour, and the day / weekday / hour report dimensions. It applies on every device and to every API call that does not pass its own `tz`. Pass an IANA name such as America/New_York, Europe/London or Asia/Tokyo, or null to go back to each browser using its own zone (API calls then count in UTC). Returns the saved value.',
|
|
387
|
+
inputSchema: {
|
|
388
|
+
type: 'object',
|
|
389
|
+
properties: {
|
|
390
|
+
timezone: { type: ['string', 'null'], description: 'IANA time zone name, or null to clear.' },
|
|
391
|
+
},
|
|
392
|
+
required: ['timezone'],
|
|
393
|
+
},
|
|
394
|
+
},
|
|
206
395
|
{
|
|
207
396
|
name: 'list_affiliate_networks',
|
|
208
397
|
description:
|
|
209
|
-
'List the user\'s affiliate networks (ClickBank, Digistore24, MaxWeb, etc).
|
|
398
|
+
'List the user\'s affiliate networks (ClickBank, Digistore24, MaxWeb, etc). Their sales come back on the account\'s one postback URL (get_universal_postback_url); networks have no postback URL of their own. conversion_param is what the network calls the click id, and is appended to offer URLs that use the network.',
|
|
210
399
|
inputSchema: { type: 'object', properties: {} },
|
|
211
400
|
},
|
|
212
401
|
{
|
|
213
402
|
name: 'create_affiliate_network',
|
|
214
403
|
description:
|
|
215
|
-
'Add a new affiliate network.
|
|
404
|
+
'Add a new affiliate network. Its sales come back on the account\'s one postback URL (get_universal_postback_url), not a URL of its own. Every offer using the network gets ?{conversion_param}={click_id} appended at click time unless the offer URL already has a {clickid} token. For Amazon Associates, set amazon_tag (e.g. "yourtagid-20"): every Amazon destination link the user adds is then auto-tagged with ?tag={amazon_tag} at click time (a tag already on the URL always wins).',
|
|
216
405
|
inputSchema: {
|
|
217
406
|
type: 'object',
|
|
218
407
|
properties: {
|
|
219
408
|
name: { type: 'string', description: 'Display name, e.g. "ClickBank".' },
|
|
220
|
-
conversion_param: { type: 'string', description: 'What the network calls the click id
|
|
221
|
-
payout_param: { type: 'string', description: 'What the network calls the payout amount (default "payout").' },
|
|
222
|
-
currency_param: { type: 'string', description: 'What the network calls the currency code (default "currency").' },
|
|
223
|
-
txid_param: { type: 'string', description: 'What the network calls the transaction/order id in its postback (default "txid"). Stored on the conversion for reconciliation/dedupe.' },
|
|
409
|
+
conversion_param: { type: 'string', description: 'What the network calls the click id (default "clickid"). Appended to offer URLs that use this network.' },
|
|
224
410
|
amazon_tag: { type: 'string', description: 'Amazon Associates tracking tag (e.g. "yourtagid-20"). When set, Amazon links are auto-converted to affiliate links by appending ?tag={amazon_tag} on each click.' },
|
|
225
411
|
},
|
|
226
412
|
required: ['name'],
|
|
@@ -228,16 +414,13 @@ const TOOLS = [
|
|
|
228
414
|
},
|
|
229
415
|
{
|
|
230
416
|
name: 'update_affiliate_network',
|
|
231
|
-
description: 'Update an affiliate network (rename, change
|
|
417
|
+
description: 'Update an affiliate network (rename, change the click id param, or set the Amazon Associates amazon_tag; pass an empty string to clear it). Pass archived=true to soft-archive.',
|
|
232
418
|
inputSchema: {
|
|
233
419
|
type: 'object',
|
|
234
420
|
properties: {
|
|
235
421
|
network_id: { type: 'number' },
|
|
236
422
|
name: { type: 'string' },
|
|
237
423
|
conversion_param: { type: 'string' },
|
|
238
|
-
payout_param: { type: 'string' },
|
|
239
|
-
currency_param: { type: 'string' },
|
|
240
|
-
txid_param: { type: 'string' },
|
|
241
424
|
amazon_tag: { type: 'string', description: 'Amazon Associates tracking tag (e.g. "yourtagid-20"); empty string clears it.' },
|
|
242
425
|
archived: { type: 'boolean' },
|
|
243
426
|
},
|
|
@@ -263,14 +446,14 @@ const TOOLS = [
|
|
|
263
446
|
{
|
|
264
447
|
name: 'create_offer',
|
|
265
448
|
description:
|
|
266
|
-
'Create an offer (a destination URL: affiliate landing page, product page, etc). The URL is used exactly as given when redirecting
|
|
449
|
+
'Create an offer (a destination URL: affiliate landing page, product page, etc). The URL is used exactly as given when redirecting. Include any affiliate tag yourself. An offer gets no short link of its own: the link that goes in an ad comes from a campaign (create_campaign), which also sets the domain. Link the offer to an affiliate_network_id so the resolver knows which postback param name to use when redirecting (e.g. some networks expect ?aff_sub=… instead of ?clickid=…).',
|
|
267
450
|
inputSchema: {
|
|
268
451
|
type: 'object',
|
|
269
452
|
properties: {
|
|
270
453
|
name: { type: 'string' },
|
|
271
454
|
url: { type: 'string', description: 'Destination URL (http/https).' },
|
|
272
455
|
affiliate_network_id: { type: ['number', 'null'] },
|
|
273
|
-
payout: { type: 'number' },
|
|
456
|
+
payout: { type: ['number', 'null'], description: 'Default payout. A sale postback that arrives with no payout books this amount. Leave out or null for none.' },
|
|
274
457
|
payout_currency: { type: 'string', description: 'ISO 4217 currency, default USD.' },
|
|
275
458
|
country: { type: 'string', description: 'ISO2 country code (optional).' },
|
|
276
459
|
},
|
|
@@ -279,7 +462,7 @@ const TOOLS = [
|
|
|
279
462
|
},
|
|
280
463
|
{
|
|
281
464
|
name: 'update_offer',
|
|
282
|
-
description: 'Update an offer\'s name, URL, payout, currency, country, or affiliate network. Pass archived=true to soft-archive.',
|
|
465
|
+
description: 'Update an offer\'s name, URL, payout, currency, country, or affiliate network. An offer has no short link of its own (the link for an ad comes from a campaign); renaming one also renames any older short link left on its URL, so the two keep one name. Pass archived=true to soft-archive.',
|
|
283
466
|
inputSchema: {
|
|
284
467
|
type: 'object',
|
|
285
468
|
properties: {
|
|
@@ -287,7 +470,7 @@ const TOOLS = [
|
|
|
287
470
|
name: { type: 'string' },
|
|
288
471
|
url: { type: 'string' },
|
|
289
472
|
affiliate_network_id: { type: ['number', 'null'] },
|
|
290
|
-
payout: { type: 'number' },
|
|
473
|
+
payout: { type: ['number', 'null'], description: 'Default payout, booked on a sale postback that has none. null clears it.' },
|
|
291
474
|
payout_currency: { type: 'string' },
|
|
292
475
|
country: { type: 'string' },
|
|
293
476
|
archived: { type: 'boolean' },
|
|
@@ -342,12 +525,100 @@ const TOOLS = [
|
|
|
342
525
|
inputSchema: { type: 'object', properties: { lander_id: { type: 'number' } }, required: ['lander_id'] },
|
|
343
526
|
},
|
|
344
527
|
|
|
528
|
+
// ─── Custom postback events ─────────────────────────────────────────────
|
|
529
|
+
{
|
|
530
|
+
name: 'list_custom_events',
|
|
531
|
+
description:
|
|
532
|
+
'List the account\'s custom postback events. Sale, refund and chargeback are built in on every account (`built_in`); everything else (add to cart, upsell, lead, registration, anything) is defined by the user (`events`). Each event\'s `slug` is the value of `event=` on any postback URL. `presets` are common starting points that do not exist until created.',
|
|
533
|
+
inputSchema: { type: 'object', properties: {} },
|
|
534
|
+
},
|
|
535
|
+
{
|
|
536
|
+
name: 'create_custom_event',
|
|
537
|
+
description:
|
|
538
|
+
'Add a custom postback event. Once created, `event={slug}` on any postback URL records it and it appears as its own column in campaign reports. The slug is derived from the label unless given, and cannot be changed later. Built-in names (purchase, sale, conversion, refund, chargeback) and tracker names (visit, offer_click, ...) are refused. With counts_revenue true, the event adds its payout to revenue and is deduped by txid like a sale (use for an upsell). With counts_revenue false, it is counted at most once per click and any payout is ignored (use for a cart or a lead).',
|
|
539
|
+
inputSchema: {
|
|
540
|
+
type: 'object',
|
|
541
|
+
properties: {
|
|
542
|
+
label: { type: 'string', description: 'Display name, e.g. "Add to cart".' },
|
|
543
|
+
slug: { type: 'string', description: 'Optional. The event= value, lower case letters, numbers, dashes and underscores, e.g. "add_to_cart". Defaults to the label, slugified.' },
|
|
544
|
+
counts_revenue: { type: 'boolean', description: 'Whether this event adds its payout to revenue. Default false.' },
|
|
545
|
+
},
|
|
546
|
+
required: ['label'],
|
|
547
|
+
},
|
|
548
|
+
},
|
|
549
|
+
{
|
|
550
|
+
name: 'update_custom_event',
|
|
551
|
+
description: 'Rename a custom postback event or change whether it counts revenue. The slug cannot be changed.',
|
|
552
|
+
inputSchema: {
|
|
553
|
+
type: 'object',
|
|
554
|
+
properties: {
|
|
555
|
+
custom_event_id: { type: 'number' },
|
|
556
|
+
label: { type: 'string' },
|
|
557
|
+
counts_revenue: { type: 'boolean' },
|
|
558
|
+
},
|
|
559
|
+
required: ['custom_event_id'],
|
|
560
|
+
},
|
|
561
|
+
},
|
|
562
|
+
{
|
|
563
|
+
name: 'delete_custom_event',
|
|
564
|
+
description: 'Delete a custom postback event. Recorded events stay and their revenue stays in totals, but postbacks using its slug get a 400 from then on. Adding the same slug back reconnects the history.',
|
|
565
|
+
inputSchema: { type: 'object', properties: { custom_event_id: { type: 'number' } }, required: ['custom_event_id'] },
|
|
566
|
+
},
|
|
567
|
+
|
|
568
|
+
// ─── Report views ───────────────────────────────────────────────────────
|
|
569
|
+
{
|
|
570
|
+
name: 'list_report_views',
|
|
571
|
+
description:
|
|
572
|
+
'List the account\'s saved report views. A view is a name plus the report columns in the order the user wants them (Voluum-style), and `active` is the view every campaign report and the dashboard Overview open on: "fewer" (the short table), "all" (every column), or a view id as a string. Column ids: lander_views, unique_lander_views, visits, unique_visits, clicks, unique_clicks, ctr, unique_ctr, conversions (Sales), cr, refunds, chargebacks, epc, epv, unique_epv, aov, revenue (Net Rev.), plus custom:{slug} for a custom event\'s count and take:{slug} / unique_take:{slug} for the take rate of an event that counts revenue (see list_custom_events). Views only change which columns show; they never change the numbers.',
|
|
573
|
+
inputSchema: { type: 'object', properties: {} },
|
|
574
|
+
},
|
|
575
|
+
{
|
|
576
|
+
name: 'create_report_view',
|
|
577
|
+
description: 'Save a report view: a name and the columns, in order (left to right). By default it becomes the active view. Names are unique per account, case-insensitive (409 name_taken). Up to 50 views.',
|
|
578
|
+
inputSchema: {
|
|
579
|
+
type: 'object',
|
|
580
|
+
properties: {
|
|
581
|
+
name: { type: 'string', description: 'e.g. "Money first".' },
|
|
582
|
+
columns: { type: 'array', items: { type: 'string' }, description: 'Column ids in display order, e.g. ["revenue","conversions","epc","clicks"]. See list_report_views for the ids.' },
|
|
583
|
+
activate: { type: 'boolean', description: 'Make it the active view. Default true.' },
|
|
584
|
+
},
|
|
585
|
+
required: ['name', 'columns'],
|
|
586
|
+
},
|
|
587
|
+
},
|
|
588
|
+
{
|
|
589
|
+
name: 'update_report_view',
|
|
590
|
+
description: 'Rename a saved report view or replace its columns. `columns` replaces the whole list, in order.',
|
|
591
|
+
inputSchema: {
|
|
592
|
+
type: 'object',
|
|
593
|
+
properties: {
|
|
594
|
+
report_view_id: { type: 'number' },
|
|
595
|
+
name: { type: 'string' },
|
|
596
|
+
columns: { type: 'array', items: { type: 'string' } },
|
|
597
|
+
},
|
|
598
|
+
required: ['report_view_id'],
|
|
599
|
+
},
|
|
600
|
+
},
|
|
601
|
+
{
|
|
602
|
+
name: 'delete_report_view',
|
|
603
|
+
description: 'Delete a saved report view. If it was active, reports go back to "fewer".',
|
|
604
|
+
inputSchema: { type: 'object', properties: { report_view_id: { type: 'number' } }, required: ['report_view_id'] },
|
|
605
|
+
},
|
|
606
|
+
{
|
|
607
|
+
name: 'set_active_report_view',
|
|
608
|
+
description: 'Pick the view reports open on: "fewer", "all", or a saved view\'s id.',
|
|
609
|
+
inputSchema: {
|
|
610
|
+
type: 'object',
|
|
611
|
+
properties: { view: { type: 'string', description: '"fewer", "all", or a view id such as "12".' } },
|
|
612
|
+
required: ['view'],
|
|
613
|
+
},
|
|
614
|
+
},
|
|
615
|
+
|
|
345
616
|
// ─── Traffic sources ────────────────────────────────────────────────────
|
|
346
617
|
{
|
|
347
618
|
name: 'list_traffic_sources',
|
|
348
619
|
description:
|
|
349
|
-
'List the user\'s traffic sources (the platforms they buy traffic from
|
|
350
|
-
inputSchema: { type: 'object', properties: {} },
|
|
620
|
+
'List the user\'s traffic sources (the platforms they buy traffic from: Facebook, TikTok, Google, …). Each source has a `name`, an optional `platform` (pixel-platform key), and `token_params`: how the source\'s click macros map onto the Google UTM params the resolver captures (utm_source/medium/campaign/term/content), e.g. Facebook utm_campaign = `{{campaign.id}}`, utm_content = Ad ID `{{ad.id}}`. The response also includes a `presets` catalog of ready-made sources with standard macros. Campaigns reference a source via `traffic_source_id` (see create_campaign / update_campaign).',
|
|
621
|
+
inputSchema: { type: 'object', properties: { include_archived: { type: 'boolean', description: 'Also list archived ones. Default false.' } } },
|
|
351
622
|
},
|
|
352
623
|
{
|
|
353
624
|
name: 'create_traffic_source',
|
|
@@ -399,13 +670,13 @@ const TOOLS = [
|
|
|
399
670
|
{
|
|
400
671
|
name: 'list_tracking_pixels',
|
|
401
672
|
description:
|
|
402
|
-
'List the user\'s tracking pixels (Facebook, Google Ads, TikTok, etc). Each pixel has a platform, pixel_id, a default_event it fires, an active flag, and has_capi_token (whether a server-side Conversions API token is set
|
|
673
|
+
'List the user\'s tracking pixels (Facebook, Google Ads, TikTok, etc). Each pixel has a platform, pixel_id, a default_event it fires, an active flag, and has_capi_token (whether a server-side Conversions API token is set; the token itself is never returned). The response also includes a `platforms` catalog with each platform\'s valid `events`, its `defaultEvent`, and `capi` (whether server-side conversions are supported). Pixels fire on short-link clicks (account-wide by default, or per-link selection) and can be chosen as a campaign\'s conversion pixel. Each pixel also reports test_event_code (test mode when set), last_sent_at / last_sent_event (the last server event the platform accepted) and last_error / last_error_at (the last refusal).',
|
|
403
674
|
inputSchema: { type: 'object', properties: {} },
|
|
404
675
|
},
|
|
405
676
|
{
|
|
406
677
|
name: 'create_tracking_pixel',
|
|
407
678
|
description:
|
|
408
|
-
'Create a tracking pixel. `platform` is one of: facebook, google_ads, google_analytics, google_tag_manager, linkedin, twitter, tiktok, pinterest, snapchat, reddit, quora, bing, adroll. `default_event` must be a valid event for that platform (see the `platforms` catalog from list_tracking_pixels)
|
|
679
|
+
'Create a tracking pixel. `platform` is one of: facebook, google_ads, google_analytics, google_tag_manager, linkedin, twitter, tiktok, pinterest, snapchat, reddit, quora, bing, adroll. `default_event` must be a valid event for that platform (see the `platforms` catalog from list_tracking_pixels) and defaults to the platform default (e.g. Facebook "PageView"). For Facebook and TikTok you can pass a `capi_token` (server-side Conversions API access token) so campaign conversions fire server-to-server.',
|
|
409
680
|
inputSchema: {
|
|
410
681
|
type: 'object',
|
|
411
682
|
properties: {
|
|
@@ -414,6 +685,7 @@ const TOOLS = [
|
|
|
414
685
|
label: { type: 'string', description: 'Optional friendly name.' },
|
|
415
686
|
default_event: { type: 'string', description: 'Event this pixel fires by default (must be valid for the platform).' },
|
|
416
687
|
capi_token: { type: 'string', description: 'Optional server-side Conversions API access token (Facebook/TikTok only).' },
|
|
688
|
+
test_event_code: { type: 'string', description: 'Optional Meta test event code (Events Manager, Test events). While set, every server event this pixel sends lands in Test events and never counts.' },
|
|
417
689
|
active: { type: 'boolean', description: 'Whether the pixel is active. Defaults to true.' },
|
|
418
690
|
},
|
|
419
691
|
required: ['platform', 'pixel_id'],
|
|
@@ -421,7 +693,7 @@ const TOOLS = [
|
|
|
421
693
|
},
|
|
422
694
|
{
|
|
423
695
|
name: 'update_tracking_pixel',
|
|
424
|
-
description: 'Update a tracking pixel\'s pixel_id, label, default_event, active flag, or
|
|
696
|
+
description: 'Update a tracking pixel\'s pixel_id, label, default_event, active flag, CAPI token or test event code. Pass capi_token as an empty string or null to clear the stored token.',
|
|
425
697
|
inputSchema: {
|
|
426
698
|
type: 'object',
|
|
427
699
|
properties: {
|
|
@@ -430,11 +702,24 @@ const TOOLS = [
|
|
|
430
702
|
label: { type: 'string' },
|
|
431
703
|
default_event: { type: 'string', description: 'New default event (must be valid for the pixel\'s platform).' },
|
|
432
704
|
capi_token: { type: ['string', 'null'], description: 'New Conversions API token, or empty/null to clear.' },
|
|
705
|
+
test_event_code: { type: ['string', 'null'], description: 'Meta test event code, or empty/null to clear it and go live.' },
|
|
433
706
|
active: { type: 'boolean' },
|
|
434
707
|
},
|
|
435
708
|
required: ['pixel_id_row'],
|
|
436
709
|
},
|
|
437
710
|
},
|
|
711
|
+
{
|
|
712
|
+
name: 'test_tracking_pixel',
|
|
713
|
+
description: 'Send one test event (a $1 Purchase by default) server-side with the pixel\'s own token, to prove the pixel id and token work. Only runs when the pixel has a test_event_code, so the test lands in Meta\'s Test events and never counts. Returns ok, and the platform\'s error when it refused. The pixel\'s last_sent_at / last_error (list_tracking_pixels) record the result.',
|
|
714
|
+
inputSchema: {
|
|
715
|
+
type: 'object',
|
|
716
|
+
properties: {
|
|
717
|
+
pixel_id_row: { type: 'number', description: 'The id of the tracking_pixels row (from list_tracking_pixels).' },
|
|
718
|
+
event: { type: 'string', description: 'Optional event name, valid for the platform. Defaults to Purchase.' },
|
|
719
|
+
},
|
|
720
|
+
required: ['pixel_id_row'],
|
|
721
|
+
},
|
|
722
|
+
},
|
|
438
723
|
{
|
|
439
724
|
name: 'delete_tracking_pixel',
|
|
440
725
|
description: 'Delete a tracking pixel. It is removed from any per-link selections and any campaign that used it as its conversion pixel (the campaign\'s conversion_pixel_id is cleared).',
|
|
@@ -445,19 +730,19 @@ const TOOLS = [
|
|
|
445
730
|
{
|
|
446
731
|
name: 'list_campaigns',
|
|
447
732
|
description:
|
|
448
|
-
'List the user\'s campaigns with quick-look stats (visits / offer_clicks / conversions / revenue). Each campaign has a short_url to paste into traffic sources,
|
|
449
|
-
inputSchema: { type: 'object', properties: {} },
|
|
733
|
+
'List the user\'s campaigns with quick-look stats (visits / offer_clicks / conversions / revenue). Each campaign has a short_url to paste into traffic sources, and a pixel_snippet to install on landers.',
|
|
734
|
+
inputSchema: { type: 'object', properties: { include_archived: { type: 'boolean', description: 'Also list archived ones. Default false.' } } },
|
|
450
735
|
},
|
|
451
736
|
{
|
|
452
737
|
name: 'create_campaign',
|
|
453
738
|
description:
|
|
454
|
-
'Create a campaign. A campaign mints one short link (campaign.code) that the user pastes into Facebook / TikTok / etc. The resolver mints a click_id per visit, optionally routes through a lander (weighted), then redirects to an offer (weighted). The response also includes a `click_url` (`…/click`)
|
|
739
|
+
'Create a campaign. A campaign mints one short link (campaign.code) that the user pastes into Facebook / TikTok / etc. The resolver mints a click_id per visit, optionally routes through a lander (weighted), then redirects to an offer (weighted). The response also includes a `click_url` (`…/click`): the single, campaign-agnostic DEFINED lander click URL the user pastes into their lander\'s CTA button. It\'s the same on every campaign, so one lander can be reused across many; the campaign is resolved at click time from the visitor\'s `bgl_clk_{code}` cookie (short-link traffic) or the ad\'s `?utm_campaign={direct_slug}` that the lander forwards onto the CTA (direct-link). It always resolves to an offer (chosen by the flow graph), never a lander, never a dead page. For DIRECT-LINK campaigns (ad points straight at the lander, no tracker hop), set `direct_slug`, put `?utm_campaign={direct_slug}` on the ad URL, and have the lander forward that utm_campaign onto the CTA. `/click` resolves the campaign from it and the property tag stitches the page view too. `fallback_url` guarantees the click never dead-ends. Conversions come back on the account\'s one postback URL (get_universal_postback_url), which credits each sale to the campaign its click came from. You can optionally attach offer_ids / lander_ids inline with matching offer_weights / lander_weights arrays for a one-call setup.',
|
|
455
740
|
inputSchema: {
|
|
456
741
|
type: 'object',
|
|
457
742
|
properties: {
|
|
458
743
|
name: { type: 'string' },
|
|
459
744
|
traffic_source_id: { type: ['number', 'null'], description: 'Id of one of the user\'s saved traffic sources (from list_traffic_sources) to attribute this campaign to. Preferred over the legacy `traffic_source` string. Mirrors the source name into the display label.' },
|
|
460
|
-
traffic_source: { type: 'string', description: 'Legacy structured traffic source key
|
|
745
|
+
traffic_source: { type: 'string', description: 'Legacy structured traffic source key, one of: facebook, instagram, google, youtube, tiktok, microsoft, linkedin, twitter, pinterest, snapchat, reddit, native, email, organic, other. Prefer `traffic_source_id`.' },
|
|
461
746
|
traffic_source_label: { type: 'string', description: 'Free text display label.' },
|
|
462
747
|
country: { type: 'string', description: 'ISO2 default geo.' },
|
|
463
748
|
cost_model: { type: 'string', enum: ['not_tracked', 'cpc', 'cpm'] },
|
|
@@ -465,14 +750,19 @@ const TOOLS = [
|
|
|
465
750
|
transition_mode: { type: 'string', enum: ['302', 'meta_refresh'] },
|
|
466
751
|
domain_id: { type: ['number', 'null'], description: 'Optional id of one of the user\'s active custom domains (from list_domains). The campaign\'s short link will resolve at that hostname. If omitted, defaults to the user\'s first active custom domain, or buygolinks.com if they have none. Pass null to force the platform default.' },
|
|
467
752
|
conversion_pixel_id: { type: ['number', 'null'], description: 'Optional id of one of the user\'s tracking pixels (from list_tracking_pixels) to fire when this campaign converts (server-side via the pixel\'s CAPI token when set). Pass null for no conversion pixel.' },
|
|
468
|
-
conversion_event: { type: 'string', description: 'Event name to fire on conversion (e.g. "Purchase", "Lead", "CompletePayment"). Must be a valid event for the conversion pixel\'s platform; defaults to that pixel\'s default_event.' },
|
|
753
|
+
conversion_event: { type: 'string', description: 'Event name to fire on conversion (e.g. "Purchase", "Lead", "CompletePayment"). Must be a valid event for the conversion pixel\'s platform; defaults to that pixel\'s default_event. Prefer pixel_events, which also covers upsells, checkouts and carts.' },
|
|
754
|
+
pixel_events: {
|
|
755
|
+
type: ['object', 'null'],
|
|
756
|
+
description: 'Which of this campaign\'s events reach its conversion pixel, and as what. Keys are "conversion" (a sale) or one of the account\'s custom event slugs (list_custom_events), values {event, first_only?}. `event` must be valid for the pixel\'s platform (e.g. Facebook "Purchase", "InitiateCheckout", "AddToCart"); an empty event means "don\'t send". first_only: true sends only the click\'s first event of that kind (e.g. the first sale, not rebills). A missing key is not sent. Example: {"conversion":{"event":"Purchase","first_only":true},"upsell":{"event":"Purchase"},"initiate_checkout":{"event":"InitiateCheckout"}}. Null keeps the old rule (sales only, as conversion_event). Events are sent server-side the moment their postback arrives; refunds and re-fired orders never are. Add &email= to the postback URL so the platform can match the buyer (hashed, never stored).',
|
|
757
|
+
},
|
|
469
758
|
report_value_mode: { type: 'string', enum: ['payout', 'fixed', 'adjusted'], description: 'What revenue VALUE to report back to the ad platform on a CAPI conversion. "payout" (default) = the real conversion payout; "fixed" = always report report_value_fixed (e.g. 1); "adjusted" = real payout minus report_value_adjust_pct % then minus report_value_adjust_flat (so the user\'s margin/cost is withheld from what the platform optimizes against). Floored at 0.' },
|
|
470
759
|
report_value_fixed: { type: ['number', 'null'], description: 'Used when report_value_mode = "fixed": the exact value (USD) reported on every conversion regardless of real payout.' },
|
|
471
760
|
report_value_adjust_pct: { type: ['number', 'null'], description: 'Used when report_value_mode = "adjusted": percentage (0–100) subtracted from the real payout first (e.g. 10 reports payout − 10%).' },
|
|
472
761
|
report_value_adjust_flat: { type: ['number', 'null'], description: 'Used when report_value_mode = "adjusted": flat amount (USD) subtracted after the percentage.' },
|
|
473
|
-
direct_slug: { type: ['string', 'null'], description: 'DEPRECATED single-tag form (still accepted)
|
|
474
|
-
direct_slugs: { type: 'array', description: 'Direct-link tags this campaign claims, for running traffic STRAIGHT to a lander with no tracker hop. A campaign can claim MANY utm_campaign values (an older campaign being migrated in, a lander turned into a campaign, etc.)
|
|
475
|
-
fallback_url: { type: ['string', 'null'], description: 'Optional URL the click falls back to when the flow yields no live offer
|
|
762
|
+
direct_slug: { type: ['string', 'null'], description: 'DEPRECATED single-tag form (still accepted). Prefer direct_slugs. Optional direct-link tag, unique per user (lowercased alnum + dashes, e.g. "bsu-fb-cold"). Returns 409 (direct_slug_taken) if another campaign already uses it. Pass null to clear.' },
|
|
763
|
+
direct_slugs: { type: 'array', description: 'Direct-link tags this campaign claims, for running traffic STRAIGHT to a lander with no tracker hop. A campaign can claim MANY utm_campaign values (an older campaign being migrated in, a lander turned into a campaign, etc.). Each routes through this campaign\'s flow. Put `?utm_campaign={slug}` on the ad URL; the lander forwards it onto the /click CTA and the property tag stitches the page view. Each item is either a string (used as both slug + label) or `{ slug, label }` where slug is sanitized (lowercased alnum+dashes, what gets matched) and label is the friendly display value (e.g. "ABO Testing Campaign"). Each slug is unique per user → 409 (direct_slug_taken) on collision. Pass [] to clear all tags.', items: { oneOf: [ { type: 'string' }, { type: 'object', properties: { slug: { type: 'string' }, label: { type: 'string' } }, required: ['slug'] } ] } },
|
|
764
|
+
fallback_url: { type: ['string', 'null'], description: 'Optional URL the click falls back to when the flow yields no live offer: the never-dead-end guarantee. Pass null to use the platform default.' },
|
|
765
|
+
open_in_browser: { type: 'boolean', description: 'On a phone, send the lander-to-offer tap out of Facebook\'s, Instagram\'s or TikTok\'s own in-app browser to the phone\'s real browser (Safari or Chrome) before the offer loads. Default false. Trade-off: the visitor leaves the browser holding Meta\'s cookies, so a Meta pixel on the offer page may match fewer sales (the fbclid still rides on the URL). Offers that belong to an app (Amazon and the other apps short links open) always open that app on a phone, leaving the in-app browser first when they need to, whatever this says.' },
|
|
476
766
|
offer_ids: { type: 'array', items: { type: 'number' } },
|
|
477
767
|
offer_weights: { type: 'array', items: { type: 'number' } },
|
|
478
768
|
lander_ids: { type: 'array', items: { type: 'number' } },
|
|
@@ -483,7 +773,7 @@ const TOOLS = [
|
|
|
483
773
|
},
|
|
484
774
|
{
|
|
485
775
|
name: 'get_campaign',
|
|
486
|
-
description: 'Get a single campaign with its attached offers, landers,
|
|
776
|
+
description: 'Get a single campaign with its attached offers, landers, short URL, and pixel snippet. `direct_lander` is true on a direct link campaign: its ads link straight to a lander, so `short_url` goes on that page\'s button and not in the ads.',
|
|
487
777
|
inputSchema: { type: 'object', properties: { campaign_id: { type: 'number' } }, required: ['campaign_id'] },
|
|
488
778
|
},
|
|
489
779
|
{
|
|
@@ -503,14 +793,20 @@ const TOOLS = [
|
|
|
503
793
|
transition_mode: { type: 'string', enum: ['302', 'meta_refresh'] },
|
|
504
794
|
domain_id: { type: ['number', 'null'], description: 'Id of one of the user\'s active custom domains, or null to move the campaign back to buygolinks.com.' },
|
|
505
795
|
conversion_pixel_id: { type: ['number', 'null'], description: 'Id of a tracking pixel to fire on conversion, or null to clear.' },
|
|
506
|
-
conversion_event: { type: 'string', description: 'Event
|
|
796
|
+
conversion_event: { type: 'string', description: 'Event a sale is sent as (must be valid for that pixel\'s platform). Moves the sale\'s pixel_events rule with it. Prefer pixel_events.' },
|
|
797
|
+
pixel_events: {
|
|
798
|
+
type: ['object', 'null'],
|
|
799
|
+
description: 'Which of this campaign\'s events reach its conversion pixel, and as what. Keys are "conversion" (a sale) or one of the account\'s custom event slugs (list_custom_events), values {event, first_only?}. `event` must be valid for the pixel\'s platform (e.g. Facebook "Purchase", "InitiateCheckout", "AddToCart"); an empty event means "don\'t send". first_only: true sends only the click\'s first event of that kind (e.g. the first sale, not rebills). A missing key is not sent. Example: {"conversion":{"event":"Purchase","first_only":true},"upsell":{"event":"Purchase"},"initiate_checkout":{"event":"InitiateCheckout"}}. Null keeps the old rule (sales only, as conversion_event). Events are sent server-side the moment their postback arrives; refunds and re-fired orders never are. Add &email= to the postback URL so the platform can match the buyer (hashed, never stored).',
|
|
800
|
+
},
|
|
507
801
|
report_value_mode: { type: 'string', enum: ['payout', 'fixed', 'adjusted'], description: 'What revenue value to report back to the ad platform on conversion: "payout" (real), "fixed" (report_value_fixed), or "adjusted" (real payout − report_value_adjust_pct% − report_value_adjust_flat, floored at 0).' },
|
|
508
802
|
report_value_fixed: { type: ['number', 'null'], description: 'Fixed value (USD) reported on every conversion when report_value_mode = "fixed". Null to clear.' },
|
|
509
803
|
report_value_adjust_pct: { type: ['number', 'null'], description: 'Percentage (0–100) subtracted from real payout when report_value_mode = "adjusted". Null to clear.' },
|
|
510
804
|
report_value_adjust_flat: { type: ['number', 'null'], description: 'Flat USD amount subtracted (after the %) when report_value_mode = "adjusted". Null to clear.' },
|
|
511
|
-
direct_slug: { type: ['string', 'null'], description: 'DEPRECATED single-tag form (still accepted)
|
|
805
|
+
direct_slug: { type: ['string', 'null'], description: 'DEPRECATED single-tag form (still accepted). Prefer direct_slugs. Direct-link tag for utm_campaign-based stitching (unique per user), or null to clear. Returns 409 (direct_slug_taken) on collision.' },
|
|
512
806
|
direct_slugs: { type: 'array', description: 'Replace this campaign\'s direct-link tags. A campaign can claim MANY utm_campaign values, each routing through its flow. Each item is a string (slug+label) or `{ slug, label }`. Slugs are unique per user → 409 (direct_slug_taken) on collision. Pass [] to clear all.', items: { oneOf: [ { type: 'string' }, { type: 'object', properties: { slug: { type: 'string' }, label: { type: 'string' } }, required: ['slug'] } ] } },
|
|
513
807
|
fallback_url: { type: ['string', 'null'], description: 'Never-dead-end fallback URL when no live offer exists, or null to use the platform default.' },
|
|
808
|
+
open_in_browser: { type: 'boolean', description: 'On a phone, send the lander-to-offer tap out of Facebook\'s, Instagram\'s or TikTok\'s own in-app browser to the phone\'s real browser (Safari or Chrome) before the offer loads. Default false. Trade-off: the visitor leaves the browser holding Meta\'s cookies, so a Meta pixel on the offer page may match fewer sales (the fbclid still rides on the URL). Offers that belong to an app (Amazon and the other apps short links open) always open that app on a phone, leaving the in-app browser first when they need to, whatever this says.' },
|
|
809
|
+
dim_labels: { type: 'object', additionalProperties: { type: 'string' }, description: 'The user\'s names for this campaign\'s tokens, keyed by dimension: { "utm_medium": "Ad Name", "param:adset_name": "Ad Set" }. Replaces the whole set, so send the names to keep too. Only utm_* and param:<name> keys are kept, and a blank name removes that one. The report keeps showing the URL\'s name beside it.' },
|
|
514
810
|
archived: { type: 'boolean' },
|
|
515
811
|
},
|
|
516
812
|
required: ['campaign_id'],
|
|
@@ -523,7 +819,7 @@ const TOOLS = [
|
|
|
523
819
|
},
|
|
524
820
|
{
|
|
525
821
|
name: 'duplicate_campaign',
|
|
526
|
-
description: 'Deep-copy a campaign: clones the campaign settings plus its full routing flow (source → landers → offers graph and split-test weights) and its offer/lander pools. The copy gets a fresh short link
|
|
822
|
+
description: 'Deep-copy a campaign: clones the campaign settings plus its full routing flow (source → landers → offers graph and split-test weights) and its offer/lander pools. The copy gets a fresh short link and a name like "COPY - {original}" (or "COPY (1) - {original}", "COPY (2) - …" when copying a copy). Stats are NOT copied; the direct-link slug is left blank (it must be unique). Returns the new campaign, including its short_url/click_url. Useful for spinning up a new geo/angle variant of a working campaign without rebuilding the flow.',
|
|
527
823
|
inputSchema: { type: 'object', properties: { campaign_id: { type: 'number' } }, required: ['campaign_id'] },
|
|
528
824
|
},
|
|
529
825
|
{
|
|
@@ -567,7 +863,7 @@ const TOOLS = [
|
|
|
567
863
|
{
|
|
568
864
|
name: 'get_campaign_flow',
|
|
569
865
|
description:
|
|
570
|
-
'Get the campaign\'s routing flow graph (n8n-style). Returns { flow: { nodes, edges }, offers, landers }. Nodes are { id, kind: "source"|"lander"|"offer", ref_id, pos_x, pos_y, offer?, lander? }
|
|
866
|
+
'Get the campaign\'s routing flow graph (n8n-style). Returns { flow: { nodes, edges }, offers, landers }. Nodes are { id, kind: "source"|"lander"|"offer", ref_id, pos_x, pos_y, direct, offer?, lander? }, with exactly one "source" node (the traffic entry). Edges are { id, from_node_id, to_node_id, weight } and only go source→lander, source→offer (direct), or lander→offer; weight is the split-test weight. This graph is what the resolver walks per click, letting a specific lander route to its own specific offers.',
|
|
571
867
|
inputSchema: {
|
|
572
868
|
type: 'object',
|
|
573
869
|
properties: { campaign_id: { type: 'number' } },
|
|
@@ -577,7 +873,7 @@ const TOOLS = [
|
|
|
577
873
|
{
|
|
578
874
|
name: 'set_campaign_flow',
|
|
579
875
|
description:
|
|
580
|
-
'Replace the campaign\'s routing flow graph. Body: { nodes: [{ tmp_id, kind, ref_id, pos_x, pos_y }], edges: [{ from, to, weight }] }. tmp_id is any client-stable string key; edges reference nodes by tmp_id. Provide exactly one node with kind="source" (ref_id null) as the traffic entry; "lander" / "offer" nodes set ref_id to a lander/offer id you own. Edges must go source→lander, source→offer, or lander→offer. weight is an integer split-test weight (higher = more traffic). Saving also re-derives the flat offer/lander pools, so legacy reports stay consistent. Use this to add a second offer for a split test, divert a lander to a different offer, or insert a landing page before an offer.',
|
|
876
|
+
'Replace the campaign\'s routing flow graph. Body: { nodes: [{ tmp_id, kind, ref_id, pos_x, pos_y, direct? }], edges: [{ from, to, weight }] }. Set direct=true on a lander node when the ad links straight to that page and the campaign short link or `/click` sits on its button: the link then goes on to that lander\'s offers instead of sending the visitor to the page, and direct landers win over every other edge on the source. No ad URL has to change. tmp_id is any client-stable string key; edges reference nodes by tmp_id. Provide exactly one node with kind="source" (ref_id null) as the traffic entry; "lander" / "offer" nodes set ref_id to a lander/offer id you own. Edges must go source→lander, source→offer, or lander→offer. weight is an integer split-test weight (higher = more traffic). Saving also re-derives the flat offer/lander pools, so legacy reports stay consistent. Use this to add a second offer for a split test, divert a lander to a different offer, or insert a landing page before an offer.',
|
|
581
877
|
inputSchema: {
|
|
582
878
|
type: 'object',
|
|
583
879
|
properties: {
|
|
@@ -592,6 +888,7 @@ const TOOLS = [
|
|
|
592
888
|
ref_id: { type: ['number', 'null'] },
|
|
593
889
|
pos_x: { type: 'number' },
|
|
594
890
|
pos_y: { type: 'number' },
|
|
891
|
+
direct: { type: 'boolean', description: 'Landers only. True when the ad links straight to this page, so the campaign link goes on to its offers instead of back to the page.' },
|
|
595
892
|
},
|
|
596
893
|
required: ['tmp_id', 'kind'],
|
|
597
894
|
},
|
|
@@ -615,15 +912,20 @@ const TOOLS = [
|
|
|
615
912
|
{
|
|
616
913
|
name: 'get_campaign_stats',
|
|
617
914
|
description:
|
|
618
|
-
'Get a campaign\'s full report: totals (visits /
|
|
915
|
+
'Get a campaign\'s full report: totals (visits / unique_visits / lander_views / unique_lander_views / offer_clicks / unique_offer_clicks / conversions / refunds / chargebacks / revenue / gross_revenue / reversed_revenue / custom / cost / profit / CTR / CR / EPC / AOV / ROI). `conversions` counts sales. `aov` is average order value: net revenue per sale (0 with no sale). `revenue` is NET: sales and revenue-counting custom events, minus refunds and chargebacks. `custom` is keyed by event slug, each {count, revenue}, and `custom_events` lists the account\'s definitions with their labels. The rest of the report: by_day / by_country / by_device / by_browser / by_referrer / by_hour, per-offer and per-lander performance, the 50 most recent events, and `breakdowns`: Voluum-style grouped report rows for 10 dimensions (utm_campaign / utm_medium / utm_source / utm_content / utm_term / lander / offer / country / device / day), each row {key, visits, unique_visits, clicks, unique_clicks, lander_views, unique_lander_views, conversions, refunds, chargebacks, revenue, gross_revenue, reversed_revenue, custom, ctr, cr, epc, aov}. UNIQUES count people, not rows: unique_visits and unique_clicks (unique_offer_clicks in totals) go by the IP and user agent of the visit that minted the click, unique_lander_views by the property tag\'s visitor cookie, then IP and device. A person who taps the button three times is 3 clicks and 1 unique click. `totals.unique_events` counts the people behind each sale and custom event, keyed `conversion` or the event slug (refunds and chargebacks are left out). Offer clicks and conversions are attributed to the utm values of the visit that minted their click_id (postback conversions landing after the visit included); the `offer` dimension attributes each visit to the offer its click routed to (keyed by offer NAME), so split-test rows read correctly; the `lander` dimension is the page the click came FROM (visit referrer normalized to host+path; full paths for landers running the property tag, ad-platform hosts like m.facebook.com for straight-from-ad clicks); traffic with no value groups under "(none)". DRILL-DOWN: pass any dimension as a filter (e.g. utm_campaign="Summer Sale", lander="promo.example.com/lander-a", or offer="My Offer") and the WHOLE report (totals, by_day, breakdowns) narrows to that traffic slice; combine filters to drill deeper (e.g. utm_campaign + group the result\'s `breakdowns.offer` to see which offer converts for that ad). Pass "(none)" to match events with no value. DIRECT LANDERS: when the flow has a direct lander, `direct_lander` is true, `totals.lander_views` and each row\'s `lander_views` count that page\'s property-tag page views, and `lander_ctr` is visits per lander view. Page views have no offer, so `lander_views` is 0 on the offer dimension and under an offer filter. A campaign with direct-link tags counts tagged traffic only. STACKED REPORT: pass `group` (up to 3 dimensions, e.g. ["utm_campaign", "utm_medium", "offer"]) and `report.rows` comes back as a Binom-style tree, each row carrying its slice\'s metrics and `children` for the next level. With no `group` the report groups by offer. `group` also accepts region / city / browser / language / weekday / hour (day, weekday and hour count in the account time zone, see `timezone` on the response) and custom tokens as `param:<name>`: every extra param on the inbound link (adset_name=..., sub3=..., utm_id=...) is kept on the visit, except click ids and personal fields. `passed_dims` lists what the URLs really carried in the window: the utm_* tokens that had a value, then the `param:<name>` ones. On a direct lander the ad\'s URL counts even if the page\'s button drops it: the visit fills in from that visitor\'s page view, and `lander_views` split by token too. `dim_labels` holds the names the user gave this campaign\'s tokens (e.g. utm_medium: "Ad Name"), set with update_campaign. Read those before describing a dimension to the user, and never assume what a slot holds: setups differ even between two campaigns on one offer, so with no name on a token, call it by its URL name.',
|
|
619
916
|
inputSchema: {
|
|
620
917
|
type: 'object',
|
|
621
918
|
properties: {
|
|
622
919
|
campaign_id: { type: 'number' },
|
|
623
920
|
days: { type: 'number', description: 'Rolling window size in days (1–90). Defaults to 14. Ignored when `from` is set.' },
|
|
624
|
-
|
|
625
|
-
|
|
921
|
+
tz: { type: 'string', description: 'IANA time zone (e.g. America/New_York) to count days, weekdays and hours in. Defaults to the account time zone (set_account_timezone), else UTC. The response echoes it as `timezone`.' },
|
|
922
|
+
group: { type: 'array', items: { type: 'string' }, maxItems: 3, description: 'Up to 3 dimensions to stack into the `report.rows` tree, outermost first. Custom tokens are written param:<name>.' },
|
|
923
|
+
breakdowns: { type: 'boolean', description: 'Default true. false skips the ten fixed `breakdowns` (returned empty) and keeps everything else, which makes a long window on a busy campaign several times faster. Use `group` for the dimensions you need.' },
|
|
924
|
+
filters: { type: 'object', additionalProperties: { type: 'string' }, description: 'Extra drill-down filters by dimension, for anything without its own argument, e.g. {"param:adset_name": "Broad 35+", "browser": "safari"}.' },
|
|
925
|
+
from: { type: 'string', description: 'ISO date/datetime: start of an explicit closed range (overrides days).' },
|
|
926
|
+
to: { type: 'string', description: 'ISO date/datetime: end of the explicit range. Defaults to now when omitted.' },
|
|
626
927
|
utm_campaign: { type: 'string', description: 'Drill-down filter: only traffic whose minting visit carried this utm_campaign.' },
|
|
928
|
+
utm_medium: { type: 'string', description: 'Drill-down filter by utm_medium.' },
|
|
627
929
|
utm_source: { type: 'string', description: 'Drill-down filter by utm_source.' },
|
|
628
930
|
utm_content: { type: 'string', description: 'Drill-down filter by utm_content.' },
|
|
629
931
|
utm_term: { type: 'string', description: 'Drill-down filter by utm_term.' },
|
|
@@ -631,7 +933,7 @@ const TOOLS = [
|
|
|
631
933
|
offer: { type: 'string', description: 'Drill-down filter by offer NAME (as returned in breakdowns.offer keys).' },
|
|
632
934
|
country: { type: 'string', description: 'Drill-down filter by ISO2 country of the minting visit.' },
|
|
633
935
|
device: { type: 'string', description: 'Drill-down filter by device of the minting visit.' },
|
|
634
|
-
day: { type: 'string', description: 'Drill-down filter by
|
|
936
|
+
day: { type: 'string', description: 'Drill-down filter by day (YYYY-MM-DD) of the minting visit, in the report time zone.' },
|
|
635
937
|
},
|
|
636
938
|
required: ['campaign_id'],
|
|
637
939
|
},
|
|
@@ -642,12 +944,12 @@ const TOOLS = [
|
|
|
642
944
|
name: 'list_properties',
|
|
643
945
|
description:
|
|
644
946
|
'List the user\'s properties with quick-look stats (page_views / unique_visitors / conversions / revenue). Best one-call tool for "page views per property" / "page views for each site". A property is a whole-site container: the user installs ONE universal tag (returned as `snippet`) on every page of their site and every page auto-reports pageviews, time-on-page, scroll depth, outbound clicks, and conversions. Page events auto-attribute to a campaign when a visitor arrives carrying that campaign\'s click.',
|
|
645
|
-
inputSchema: { type: 'object', properties: {} },
|
|
947
|
+
inputSchema: { type: 'object', properties: { include_archived: { type: 'boolean', description: 'Also list archived ones. Default false.' } } },
|
|
646
948
|
},
|
|
647
949
|
{
|
|
648
950
|
name: 'create_property',
|
|
649
951
|
description:
|
|
650
|
-
'Create a property (a GTM/GA-style site container). Returns the property with its public property_key, the `tracking_domain` it\'s bound to, and an install `snippet
|
|
952
|
+
'Create a property (a GTM/GA-style site container). Returns the property with its public property_key, the `tracking_domain` it\'s bound to, and an install `snippet`: a single GA/Facebook-style inline <script> block (a self-invoking function that sets up a conversion-queue stub and injects the async tag) the user pastes into the <head> of every page of their site. REQUIRES an active custom domain: the tag is served from the user\'s own domain so it stays first-party to their site, which is required for tracking to work on iOS/Safari (a tag loaded cross-site from buygolinks.com gets its visitor cookie partitioned/blocked). Defaults to the user\'s first active custom domain; pass `domain_id` (from list_domains) to pin a specific one. If the user has no active custom domain, returns error `domain_required`. Have them add one via create_domain first. To record conversions, the site sets window.bglConversion = true (optionally window.bglRevenue) on a thank-you page, or calls window.bglFireConversion(revenue). The queue stub makes that call safe even before the async tag finishes loading.',
|
|
651
953
|
inputSchema: {
|
|
652
954
|
type: 'object',
|
|
653
955
|
properties: {
|
|
@@ -665,7 +967,7 @@ const TOOLS = [
|
|
|
665
967
|
},
|
|
666
968
|
{
|
|
667
969
|
name: 'update_property',
|
|
668
|
-
description: 'Update a property\'s name, site_url, tracking domain (domain_id), or archive state. Pass `domain_id` to re-bind the install snippet to a different active custom domain
|
|
970
|
+
description: 'Update a property\'s name, site_url, tracking domain (domain_id), or archive state. Pass `domain_id` to re-bind the install snippet to a different active custom domain. It must be one of the user\'s active custom domains (a property can never fall back to buygolinks.com, since that breaks iOS tracking). Returns the updated property with its `tracking_domain` and regenerated `snippet`.',
|
|
669
971
|
inputSchema: {
|
|
670
972
|
type: 'object',
|
|
671
973
|
properties: {
|
|
@@ -711,6 +1013,7 @@ const TOOLS = [
|
|
|
711
1013
|
properties: {
|
|
712
1014
|
property_id: { type: 'number' },
|
|
713
1015
|
days: { type: 'number', description: 'Window size in days (1–90). Defaults to 14.' },
|
|
1016
|
+
tz: { type: 'string', description: 'IANA time zone (e.g. America/New_York) to count days, weekdays and hours in. Defaults to the account time zone (set_account_timezone), else UTC. The response echoes it as `timezone`.' },
|
|
714
1017
|
},
|
|
715
1018
|
required: ['property_id'],
|
|
716
1019
|
},
|
|
@@ -718,43 +1021,54 @@ const TOOLS = [
|
|
|
718
1021
|
{
|
|
719
1022
|
name: 'list_property_pages',
|
|
720
1023
|
description:
|
|
721
|
-
'List the distinct pages observed across ALL the user\'s properties (the pageview stream), most-viewed first. Each entry is `{ url, path, title, property_id, property_name, views }`. Any of these pages can be used as a campaign LANDER
|
|
1024
|
+
'List the distinct pages observed across ALL the user\'s properties (the pageview stream), most-viewed first. Each entry is `{ url, path, title, property_id, property_name, views }`. Any of these pages can be used as a campaign LANDER: pass its `url` to `create_lander` (the page title makes a good lander name) and then wire the resulting lander into a campaign flow via `set_campaign_flow`. Use this to turn a real page on the user\'s site into a lander without retyping its URL.',
|
|
722
1025
|
inputSchema: { type: 'object', properties: {} },
|
|
723
1026
|
},
|
|
724
1027
|
{
|
|
725
1028
|
name: 'list_observed_campaign_tags',
|
|
726
1029
|
description:
|
|
727
|
-
'List the distinct `utm_campaign` values that have actually been SEEN arriving on the user\'s properties/visits over the last 90 days, most-active first. Each entry is `{ utm_campaign, slug, events, utm_source, utm_medium, lander_url, claimed_by }` where `utm_campaign` is the raw value (for display, e.g. "ABO Testing Campaign"), `slug` is its sanitized form (what gets stored + matched), and `claimed_by` is `{ id, name }` of the BGL campaign that already owns that slug, or null if unclaimed. Use this to WIRE UP AN ALREADY-RUNNING DIRECT-LINK CAMPAIGN: when an ad on FB/Google points straight at a lander (no tracker hop) with `?utm_campaign={tag}`, that tag shows up here once the universal property tag reports a page view. Pick an unclaimed tag and add it to a campaign\'s `direct_slugs` (via `update_campaign`, passing `{ slug, label: utm_campaign }`) so BGL attributes that traffic
|
|
1030
|
+
'List the distinct `utm_campaign` values that have actually been SEEN arriving on the user\'s properties/visits over the last 90 days, most-active first. Each entry is `{ utm_campaign, slug, events, utm_source, utm_medium, lander_url, claimed_by }` where `utm_campaign` is the raw value (for display, e.g. "ABO Testing Campaign"), `slug` is its sanitized form (what gets stored + matched), and `claimed_by` is `{ id, name }` of the BGL campaign that already owns that slug, or null if unclaimed. Use this to WIRE UP AN ALREADY-RUNNING DIRECT-LINK CAMPAIGN: when an ad on FB/Google points straight at a lander (no tracker hop) with `?utm_campaign={tag}`, that tag shows up here once the universal property tag reports a page view. Pick an unclaimed tag and add it to a campaign\'s `direct_slugs` (via `update_campaign`, passing `{ slug, label: utm_campaign }`) so BGL attributes that traffic, and its `/click` CTA, to the right campaign + offer. A campaign can claim several tags. Skip tags whose `claimed_by` is non-null (another campaign already owns them).',
|
|
728
1031
|
inputSchema: { type: 'object', properties: {} },
|
|
729
1032
|
},
|
|
730
1033
|
{
|
|
731
1034
|
name: 'get_tracker_overview',
|
|
732
1035
|
description:
|
|
733
|
-
'PAGE VIEWS live here. Get the unified Tracker dashboard rollup across BOTH event streams (site properties + campaigns) in one call. Returns totals (page_views / unique_visitors / visits / offer_clicks / conversions / revenue), a merged daily activity series (by_day), dimension breakdowns (by_country / by_device / by_browser), per-property and per-campaign rollups (the per-property rollup gives page_views for each site/property), a per-UTM-campaign rollup (utm_campaigns
|
|
1036
|
+
'PAGE VIEWS live here. Get the unified Tracker dashboard rollup across BOTH event streams (site properties + campaigns) in one call. Returns totals (page_views / unique_visitors / visits / offer_clicks / conversions / revenue), a merged daily activity series (by_day), dimension breakdowns (by_country / by_device / by_browser), per-property and per-campaign rollups (the per-property rollup gives page_views for each site/property; each campaign row has visits, lander_views, offer_clicks, conversions, net revenue and direct_lander, and its EPV is revenue / lander_views when direct_lander is true, else revenue / visits, the same as the campaign Report), a per-UTM-campaign rollup (utm_campaigns: auto-detected Google utm_source/medium/campaign/content tags with events/conversions/revenue), a per-source rollup (utm_sources: one row per utm_source with events/conversions/net revenue, uncapped). In both, a sale is credited to the UTMs of the visit on its click, since a postback carries none, and events counts traffic only. and the 50 most recent events across both streams. This is the go-to tool for "page views", "page views per property", "campaign traffic", and "is everything running and getting traffic?". NOTE: page_views here is Tracker site traffic and is SEPARATE from branded short-link clicks (get_overview_stats). If the user asks for "clicks AND page views" together, call BOTH this tool and get_overview_stats and combine them.',
|
|
734
1037
|
inputSchema: {
|
|
735
1038
|
type: 'object',
|
|
736
1039
|
properties: {
|
|
737
|
-
days: { type: 'number', description: 'Window size in days (1–90). Defaults to 14.' },
|
|
1040
|
+
days: { type: 'number', description: 'Window size in days (1–90). Defaults to 14. Ignored when `from` is set.' },
|
|
1041
|
+
from: { type: 'string', description: 'ISO date/datetime: start of an explicit range (overrides days). For "today" pass local midnight; leave `to` out to stay open to now.' },
|
|
1042
|
+
to: { type: 'string', description: 'ISO date/datetime: end of the explicit range. Defaults to now.' },
|
|
1043
|
+
tz: { type: 'string', description: 'IANA time zone (e.g. America/New_York) to count days, weekdays and hours in. Defaults to the account time zone (set_account_timezone), else UTC. The response echoes it as `timezone`.' },
|
|
1044
|
+
offer_id: { type: 'number', description: 'Scope to one offer: its campaign traffic and the sales on its clicks. Drops the property stream.' },
|
|
1045
|
+
property_id: { type: 'number', description: 'Scope to one property\'s page views. Drops the campaign stream.' },
|
|
738
1046
|
},
|
|
739
1047
|
},
|
|
740
1048
|
},
|
|
741
1049
|
{
|
|
742
1050
|
name: 'get_tracker_events',
|
|
743
1051
|
description:
|
|
744
|
-
'Query the unified Tracker event log across BOTH streams (site properties + campaigns), with full search / filter / sort over auto-detected Google UTM tags. Every event carries utm_source / utm_medium / utm_campaign / utm_term / utm_content. Use `q` for free-text search (matches any utm value, country, device, browser, referrer, source name, event type); `stream` to scope to all|property|campaign; `event` to filter by event_type; the utm_* params for exact-match filtering; and `sort`+`dir` to order by
|
|
1052
|
+
'Query the unified Tracker event log across BOTH streams (site properties + campaigns), with full search / filter / sort over auto-detected Google UTM tags. Every event carries utm_source / utm_medium / utm_campaign / utm_term / utm_content. Use `q` for free-text search (matches any utm value, country, device, browser, referrer, source name, event type); `stream` to scope to all|property|campaign; `event` to filter by event_type; the utm_* params for exact-match filtering; `campaign_id` / `offer_id` / `property_id` / `link_id` to scope it; and `sort`+`dir` to order by any column. Rows from a campaign carry the location, IP and UTMs of the visit on their click, so a sale row shows where its buyer came from. Bot traffic (Meta ad reviewers, crawlers) is left out. Returns the matching page of events plus `facets` (distinct utm values present in the window) to drive filter pickers, and `total` for the full match count.',
|
|
745
1053
|
inputSchema: {
|
|
746
1054
|
type: 'object',
|
|
747
1055
|
properties: {
|
|
748
|
-
days: { type: 'number', description: 'Window size in days (1–90). Defaults to 14.' },
|
|
749
|
-
|
|
750
|
-
|
|
1056
|
+
days: { type: 'number', description: 'Window size in days (1–90). Defaults to 14. Ignored when `from` is set.' },
|
|
1057
|
+
from: { type: 'string', description: 'ISO date/datetime: start of an explicit range (overrides days).' },
|
|
1058
|
+
to: { type: 'string', description: 'ISO date/datetime: end of the explicit range. Defaults to now.' },
|
|
1059
|
+
q: { type: 'string', description: 'Free-text search across utm_*, country, city, device, browser, referrer, source name, event type.' },
|
|
1060
|
+
stream: { type: 'string', enum: ['all', 'property', 'campaign'], description: 'Scope to one stream. Defaults to all. Ignored when a campaign, offer, property or link scope is set.' },
|
|
1061
|
+
campaign_id: { type: 'number', description: 'Only this campaign\'s events (visits, offer clicks, sales, custom events, refunds).' },
|
|
1062
|
+
offer_id: { type: 'number', description: 'Only events on clicks that went to this offer.' },
|
|
1063
|
+
property_id: { type: 'number', description: 'Only this property\'s page events.' },
|
|
1064
|
+
link_id: { type: 'number', description: 'Only this plain short link\'s clicks (shown as event_type "click").' },
|
|
751
1065
|
event: { type: 'string', description: 'Exact event_type filter, e.g. page_view, outbound, conversion, visit, lander_view, offer_click.' },
|
|
752
1066
|
utm_source: { type: 'string', description: 'Exact utm_source filter.' },
|
|
753
1067
|
utm_medium: { type: 'string', description: 'Exact utm_medium filter.' },
|
|
754
1068
|
utm_campaign: { type: 'string', description: 'Exact utm_campaign filter.' },
|
|
755
1069
|
utm_term: { type: 'string', description: 'Exact utm_term filter.' },
|
|
756
1070
|
utm_content: { type: 'string', description: 'Exact utm_content filter.' },
|
|
757
|
-
sort: { type: 'string', enum: ['ts', 'revenue', '
|
|
1071
|
+
sort: { type: 'string', enum: ['ts', 'revenue', 'stream', 'source_name', 'event_type', 'country', 'region', 'city', 'ip', 'device', 'browser', 'referrer', 'page_path', 'utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content'], description: 'Sort field. Defaults to ts.' },
|
|
758
1072
|
dir: { type: 'string', enum: ['asc', 'desc'], description: 'Sort direction. Defaults to desc.' },
|
|
759
1073
|
limit: { type: 'number', description: 'Page size (1–500). Defaults to 200.' },
|
|
760
1074
|
offset: { type: 'number', description: 'Pagination offset. Defaults to 0.' },
|
|
@@ -764,7 +1078,7 @@ const TOOLS = [
|
|
|
764
1078
|
];
|
|
765
1079
|
|
|
766
1080
|
const server = new Server(
|
|
767
|
-
{ name: 'buygolinks', version:
|
|
1081
|
+
{ name: 'buygolinks', version: PKG_VERSION },
|
|
768
1082
|
{ capabilities: { tools: {} } },
|
|
769
1083
|
);
|
|
770
1084
|
|
|
@@ -813,6 +1127,76 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
813
1127
|
});
|
|
814
1128
|
break;
|
|
815
1129
|
}
|
|
1130
|
+
case 'delete_link': {
|
|
1131
|
+
result = await api(`/api/links/${Number(args.link_id)}`, { method: 'DELETE' });
|
|
1132
|
+
break;
|
|
1133
|
+
}
|
|
1134
|
+
case 'get_link_pixels': {
|
|
1135
|
+
result = await api(`/api/links/${Number(args.link_id)}/pixels`);
|
|
1136
|
+
break;
|
|
1137
|
+
}
|
|
1138
|
+
case 'set_link_pixels': {
|
|
1139
|
+
const body = { pixel_ids: (args.pixel_ids || []).map(Number) };
|
|
1140
|
+
if (typeof args.overridden === 'boolean') body.overridden = args.overridden;
|
|
1141
|
+
if (args.events && typeof args.events === 'object') body.events = args.events;
|
|
1142
|
+
result = await api(`/api/links/${Number(args.link_id)}/pixels`, { method: 'PUT', body: JSON.stringify(body) });
|
|
1143
|
+
break;
|
|
1144
|
+
}
|
|
1145
|
+
case 'set_link_collection': {
|
|
1146
|
+
const cid = args.collection_id == null ? null : Number(args.collection_id);
|
|
1147
|
+
result = await api(`/api/links/${Number(args.link_id)}/collection`, {
|
|
1148
|
+
method: 'POST', body: JSON.stringify({ collection_id: cid }),
|
|
1149
|
+
});
|
|
1150
|
+
break;
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
// ── Collections ──
|
|
1154
|
+
case 'list_collections': {
|
|
1155
|
+
result = await api('/api/collections');
|
|
1156
|
+
break;
|
|
1157
|
+
}
|
|
1158
|
+
case 'get_collection': {
|
|
1159
|
+
result = await api(`/api/collections/${Number(args.collection_id)}${qsFrom(args, ['days'])}`);
|
|
1160
|
+
break;
|
|
1161
|
+
}
|
|
1162
|
+
case 'create_collection':
|
|
1163
|
+
case 'update_collection': {
|
|
1164
|
+
const body = {};
|
|
1165
|
+
for (const k of ['name', 'description', 'color', 'rotator_alias']) {
|
|
1166
|
+
if (typeof args[k] === 'string') body[k] = args[k];
|
|
1167
|
+
}
|
|
1168
|
+
for (const k of ['starred', 'rotator_enabled']) {
|
|
1169
|
+
if (typeof args[k] === 'boolean') body[k] = args[k];
|
|
1170
|
+
}
|
|
1171
|
+
result = name === 'create_collection'
|
|
1172
|
+
? await api('/api/collections', { method: 'POST', body: JSON.stringify(body) })
|
|
1173
|
+
: await api(`/api/collections/${Number(args.collection_id)}`, { method: 'PATCH', body: JSON.stringify(body) });
|
|
1174
|
+
break;
|
|
1175
|
+
}
|
|
1176
|
+
case 'delete_collection': {
|
|
1177
|
+
result = await api(`/api/collections/${Number(args.collection_id)}`, { method: 'DELETE' });
|
|
1178
|
+
break;
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
// ── Account ──
|
|
1182
|
+
case 'get_account': {
|
|
1183
|
+
result = await api('/api/me');
|
|
1184
|
+
break;
|
|
1185
|
+
}
|
|
1186
|
+
case 'list_api_keys': {
|
|
1187
|
+
result = await api('/api/api-keys');
|
|
1188
|
+
break;
|
|
1189
|
+
}
|
|
1190
|
+
case 'create_api_key': {
|
|
1191
|
+
const body = typeof args?.label === 'string' ? { label: args.label } : {};
|
|
1192
|
+
result = await api('/api/api-keys', { method: 'POST', body: JSON.stringify(body) });
|
|
1193
|
+
break;
|
|
1194
|
+
}
|
|
1195
|
+
case 'delete_api_key': {
|
|
1196
|
+
result = await api(`/api/api-keys/${Number(args.api_key_id)}`, { method: 'DELETE' });
|
|
1197
|
+
break;
|
|
1198
|
+
}
|
|
1199
|
+
|
|
816
1200
|
case 'list_links': {
|
|
817
1201
|
const qs = args?.include_archived ? '?include_archived=1' : '';
|
|
818
1202
|
result = await api(`/api/links${qs}`);
|
|
@@ -843,19 +1227,36 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
843
1227
|
result = await api(`/api/domains/${Number(args.domain_id)}/verify`, { method: 'POST' });
|
|
844
1228
|
break;
|
|
845
1229
|
}
|
|
1230
|
+
case 'check_domain_dns': {
|
|
1231
|
+
result = await api(`/api/domains/${Number(args.domain_id)}/dns`);
|
|
1232
|
+
break;
|
|
1233
|
+
}
|
|
1234
|
+
case 'connect_domain_cloudflare': {
|
|
1235
|
+
result = await api(`/api/domains/${Number(args.domain_id)}/connect-cloudflare`, {
|
|
1236
|
+
method: 'POST',
|
|
1237
|
+
body: JSON.stringify({ api_token: args.api_token }),
|
|
1238
|
+
});
|
|
1239
|
+
break;
|
|
1240
|
+
}
|
|
846
1241
|
case 'delete_domain': {
|
|
847
1242
|
result = await api(`/api/domains/${Number(args.domain_id)}`, { method: 'DELETE' });
|
|
848
1243
|
break;
|
|
849
1244
|
}
|
|
850
1245
|
|
|
851
1246
|
// ── Tracker: networks ──
|
|
1247
|
+
case 'set_account_timezone': {
|
|
1248
|
+
const tz = args?.timezone === null || args?.timezone === '' ? null : String(args.timezone);
|
|
1249
|
+
await api('/api/me', { method: 'PATCH', body: JSON.stringify({ timezone: tz }) });
|
|
1250
|
+
result = { timezone: tz };
|
|
1251
|
+
break;
|
|
1252
|
+
}
|
|
852
1253
|
case 'get_universal_postback_url': {
|
|
853
1254
|
const me = await api('/api/me');
|
|
854
1255
|
const base = me.user && me.user.postback_url;
|
|
855
1256
|
result = {
|
|
856
1257
|
postback_url: base,
|
|
857
1258
|
template: base ? `${base}?clickid=REPLACE&payout=OPTIONAL¤cy=OPTIONAL&txid=OPTIONAL` : null,
|
|
858
|
-
note: 'Universal: works for every campaign and every network. payout/currency/txid are optional; cid is accepted as an alias for clickid.',
|
|
1259
|
+
note: 'Universal: works for every campaign and every network. payout/currency/txid are optional; a sale sent with no payout books the payout of the offer its click went to, when the offer has one. cid is accepted as an alias for clickid.',
|
|
859
1260
|
};
|
|
860
1261
|
break;
|
|
861
1262
|
}
|
|
@@ -866,9 +1267,6 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
866
1267
|
case 'create_affiliate_network': {
|
|
867
1268
|
const body = { name: args.name };
|
|
868
1269
|
if (typeof args.conversion_param === 'string') body.conversion_param = args.conversion_param;
|
|
869
|
-
if (typeof args.payout_param === 'string') body.payout_param = args.payout_param;
|
|
870
|
-
if (typeof args.currency_param === 'string') body.currency_param = args.currency_param;
|
|
871
|
-
if (typeof args.txid_param === 'string') body.txid_param = args.txid_param;
|
|
872
1270
|
if (typeof args.amazon_tag === 'string') body.amazon_tag = args.amazon_tag;
|
|
873
1271
|
result = await api('/api/affiliate-networks', { method: 'POST', body: JSON.stringify(body) });
|
|
874
1272
|
break;
|
|
@@ -877,9 +1275,6 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
877
1275
|
const body = {};
|
|
878
1276
|
if (typeof args.name === 'string') body.name = args.name;
|
|
879
1277
|
if (typeof args.conversion_param === 'string') body.conversion_param = args.conversion_param;
|
|
880
|
-
if (typeof args.payout_param === 'string') body.payout_param = args.payout_param;
|
|
881
|
-
if (typeof args.currency_param === 'string') body.currency_param = args.currency_param;
|
|
882
|
-
if (typeof args.txid_param === 'string') body.txid_param = args.txid_param;
|
|
883
1278
|
if (typeof args.amazon_tag === 'string') body.amazon_tag = args.amazon_tag;
|
|
884
1279
|
if (typeof args.archived === 'boolean') body.archived = args.archived;
|
|
885
1280
|
result = await api(`/api/affiliate-networks/${Number(args.network_id)}`, { method: 'PATCH', body: JSON.stringify(body) });
|
|
@@ -898,7 +1293,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
898
1293
|
case 'create_offer': {
|
|
899
1294
|
const body = { name: args.name, url: args.url };
|
|
900
1295
|
if ('affiliate_network_id' in args) body.affiliate_network_id = args.affiliate_network_id;
|
|
901
|
-
if (
|
|
1296
|
+
if ('payout' in args) body.payout = args.payout;
|
|
902
1297
|
if (typeof args.payout_currency === 'string') body.payout_currency = args.payout_currency;
|
|
903
1298
|
if (typeof args.country === 'string') body.country = args.country;
|
|
904
1299
|
result = await api('/api/offers', { method: 'POST', body: JSON.stringify(body) });
|
|
@@ -909,7 +1304,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
909
1304
|
if (typeof args.name === 'string') body.name = args.name;
|
|
910
1305
|
if (typeof args.url === 'string') body.url = args.url;
|
|
911
1306
|
if ('affiliate_network_id' in args) body.affiliate_network_id = args.affiliate_network_id;
|
|
912
|
-
if (
|
|
1307
|
+
if ('payout' in args) body.payout = args.payout;
|
|
913
1308
|
if (typeof args.payout_currency === 'string') body.payout_currency = args.payout_currency;
|
|
914
1309
|
if (typeof args.country === 'string') body.country = args.country;
|
|
915
1310
|
if (typeof args.archived === 'boolean') body.archived = args.archived;
|
|
@@ -946,9 +1341,60 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
946
1341
|
break;
|
|
947
1342
|
}
|
|
948
1343
|
|
|
1344
|
+
// ── Custom postback events ──
|
|
1345
|
+
case 'list_custom_events': {
|
|
1346
|
+
result = await api('/api/custom-events');
|
|
1347
|
+
break;
|
|
1348
|
+
}
|
|
1349
|
+
case 'create_custom_event': {
|
|
1350
|
+
const body = { label: args.label };
|
|
1351
|
+
if (typeof args.slug === 'string') body.slug = args.slug;
|
|
1352
|
+
if (typeof args.counts_revenue === 'boolean') body.counts_revenue = args.counts_revenue;
|
|
1353
|
+
result = await api('/api/custom-events', { method: 'POST', body: JSON.stringify(body) });
|
|
1354
|
+
break;
|
|
1355
|
+
}
|
|
1356
|
+
case 'update_custom_event': {
|
|
1357
|
+
const body = {};
|
|
1358
|
+
if (typeof args.label === 'string') body.label = args.label;
|
|
1359
|
+
if (typeof args.counts_revenue === 'boolean') body.counts_revenue = args.counts_revenue;
|
|
1360
|
+
result = await api(`/api/custom-events/${Number(args.custom_event_id)}`, { method: 'PATCH', body: JSON.stringify(body) });
|
|
1361
|
+
break;
|
|
1362
|
+
}
|
|
1363
|
+
case 'delete_custom_event': {
|
|
1364
|
+
result = await api(`/api/custom-events/${Number(args.custom_event_id)}`, { method: 'DELETE' });
|
|
1365
|
+
break;
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
// ── Report views ──
|
|
1369
|
+
case 'list_report_views': {
|
|
1370
|
+
result = await api('/api/report-views');
|
|
1371
|
+
break;
|
|
1372
|
+
}
|
|
1373
|
+
case 'create_report_view': {
|
|
1374
|
+
const body = { name: args.name, columns: args.columns };
|
|
1375
|
+
if (typeof args.activate === 'boolean') body.activate = args.activate;
|
|
1376
|
+
result = await api('/api/report-views', { method: 'POST', body: JSON.stringify(body) });
|
|
1377
|
+
break;
|
|
1378
|
+
}
|
|
1379
|
+
case 'update_report_view': {
|
|
1380
|
+
const body = {};
|
|
1381
|
+
if (typeof args.name === 'string') body.name = args.name;
|
|
1382
|
+
if (Array.isArray(args.columns)) body.columns = args.columns;
|
|
1383
|
+
result = await api(`/api/report-views/${Number(args.report_view_id)}`, { method: 'PATCH', body: JSON.stringify(body) });
|
|
1384
|
+
break;
|
|
1385
|
+
}
|
|
1386
|
+
case 'delete_report_view': {
|
|
1387
|
+
result = await api(`/api/report-views/${Number(args.report_view_id)}`, { method: 'DELETE' });
|
|
1388
|
+
break;
|
|
1389
|
+
}
|
|
1390
|
+
case 'set_active_report_view': {
|
|
1391
|
+
result = await api('/api/report-views/active', { method: 'PUT', body: JSON.stringify({ view: String(args.view) }) });
|
|
1392
|
+
break;
|
|
1393
|
+
}
|
|
1394
|
+
|
|
949
1395
|
// ── Traffic sources ──
|
|
950
1396
|
case 'list_traffic_sources': {
|
|
951
|
-
result = await api(
|
|
1397
|
+
result = await api(`/api/traffic-sources${args?.include_archived ? '?include_archived=1' : ''}`);
|
|
952
1398
|
break;
|
|
953
1399
|
}
|
|
954
1400
|
case 'create_traffic_source': {
|
|
@@ -982,6 +1428,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
982
1428
|
if (typeof args.label === 'string') body.label = args.label;
|
|
983
1429
|
if (typeof args.default_event === 'string') body.default_event = args.default_event;
|
|
984
1430
|
if (typeof args.capi_token === 'string') body.capi_token = args.capi_token;
|
|
1431
|
+
if (typeof args.test_event_code === 'string') body.test_event_code = args.test_event_code;
|
|
985
1432
|
if (typeof args.active === 'boolean') body.active = args.active;
|
|
986
1433
|
result = await api('/api/tracking-pixels', { method: 'POST', body: JSON.stringify(body) });
|
|
987
1434
|
break;
|
|
@@ -992,10 +1439,16 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
992
1439
|
if (typeof args.label === 'string') body.label = args.label;
|
|
993
1440
|
if (typeof args.default_event === 'string') body.default_event = args.default_event;
|
|
994
1441
|
if ('capi_token' in args) body.capi_token = args.capi_token;
|
|
1442
|
+
if ('test_event_code' in args) body.test_event_code = args.test_event_code;
|
|
995
1443
|
if (typeof args.active === 'boolean') body.active = args.active;
|
|
996
1444
|
result = await api(`/api/tracking-pixels/${Number(args.pixel_id_row)}`, { method: 'PATCH', body: JSON.stringify(body) });
|
|
997
1445
|
break;
|
|
998
1446
|
}
|
|
1447
|
+
case 'test_tracking_pixel': {
|
|
1448
|
+
const body = typeof args.event === 'string' ? { event: args.event } : {};
|
|
1449
|
+
result = await api(`/api/tracking-pixels/${Number(args.pixel_id_row)}/test`, { method: 'POST', body: JSON.stringify(body) });
|
|
1450
|
+
break;
|
|
1451
|
+
}
|
|
999
1452
|
case 'delete_tracking_pixel': {
|
|
1000
1453
|
result = await api(`/api/tracking-pixels/${Number(args.pixel_id_row)}`, { method: 'DELETE' });
|
|
1001
1454
|
break;
|
|
@@ -1003,7 +1456,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
1003
1456
|
|
|
1004
1457
|
// ── Tracker: campaigns ──
|
|
1005
1458
|
case 'list_campaigns': {
|
|
1006
|
-
result = await api(
|
|
1459
|
+
result = await api(`/api/campaigns${args?.include_archived ? '?include_archived=1' : ''}`);
|
|
1007
1460
|
break;
|
|
1008
1461
|
}
|
|
1009
1462
|
case 'create_campaign': {
|
|
@@ -1015,6 +1468,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
1015
1468
|
if ('domain_id' in args) body.domain_id = args.domain_id;
|
|
1016
1469
|
if ('traffic_source_id' in args) body.traffic_source_id = args.traffic_source_id;
|
|
1017
1470
|
if ('conversion_pixel_id' in args) body.conversion_pixel_id = args.conversion_pixel_id;
|
|
1471
|
+
if ('pixel_events' in args) body.pixel_events = args.pixel_events;
|
|
1018
1472
|
if (typeof args.report_value_mode === 'string') body.report_value_mode = args.report_value_mode;
|
|
1019
1473
|
if ('report_value_fixed' in args) body.report_value_fixed = args.report_value_fixed;
|
|
1020
1474
|
if ('report_value_adjust_pct' in args) body.report_value_adjust_pct = args.report_value_adjust_pct;
|
|
@@ -1022,6 +1476,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
1022
1476
|
if ('direct_slug' in args) body.direct_slug = args.direct_slug;
|
|
1023
1477
|
if (Array.isArray(args.direct_slugs)) body.direct_slugs = args.direct_slugs;
|
|
1024
1478
|
if ('fallback_url' in args) body.fallback_url = args.fallback_url;
|
|
1479
|
+
if ('open_in_browser' in args) body.open_in_browser = args.open_in_browser;
|
|
1025
1480
|
if (Array.isArray(args.offer_ids)) body.offer_ids = args.offer_ids;
|
|
1026
1481
|
if (Array.isArray(args.offer_weights)) body.offer_weights = args.offer_weights;
|
|
1027
1482
|
if (Array.isArray(args.lander_ids)) body.lander_ids = args.lander_ids;
|
|
@@ -1043,6 +1498,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
1043
1498
|
if (typeof args.cost_value === 'number') body.cost_value = args.cost_value;
|
|
1044
1499
|
if ('domain_id' in args) body.domain_id = args.domain_id;
|
|
1045
1500
|
if ('conversion_pixel_id' in args) body.conversion_pixel_id = args.conversion_pixel_id;
|
|
1501
|
+
if ('pixel_events' in args) body.pixel_events = args.pixel_events;
|
|
1046
1502
|
if (typeof args.report_value_mode === 'string') body.report_value_mode = args.report_value_mode;
|
|
1047
1503
|
if ('report_value_fixed' in args) body.report_value_fixed = args.report_value_fixed;
|
|
1048
1504
|
if ('report_value_adjust_pct' in args) body.report_value_adjust_pct = args.report_value_adjust_pct;
|
|
@@ -1050,6 +1506,8 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
1050
1506
|
if ('direct_slug' in args) body.direct_slug = args.direct_slug;
|
|
1051
1507
|
if (Array.isArray(args.direct_slugs)) body.direct_slugs = args.direct_slugs;
|
|
1052
1508
|
if ('fallback_url' in args) body.fallback_url = args.fallback_url;
|
|
1509
|
+
if ('open_in_browser' in args) body.open_in_browser = args.open_in_browser;
|
|
1510
|
+
if ('dim_labels' in args) body.dim_labels = args.dim_labels;
|
|
1053
1511
|
if (typeof args.archived === 'boolean') body.archived = args.archived;
|
|
1054
1512
|
result = await api(`/api/campaigns/${Number(args.campaign_id)}`, { method: 'PATCH', body: JSON.stringify(body) });
|
|
1055
1513
|
break;
|
|
@@ -1089,16 +1547,22 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
1089
1547
|
if (args?.from) qs.set('from', String(args.from));
|
|
1090
1548
|
if (args?.to) qs.set('to', String(args.to));
|
|
1091
1549
|
if (args?.days && !args?.from) qs.set('days', String(Number(args.days)));
|
|
1092
|
-
|
|
1550
|
+
if (args?.tz) qs.set('tz', String(args.tz));
|
|
1551
|
+
for (const dim of ['utm_campaign', 'utm_medium', 'utm_source', 'utm_content', 'utm_term', 'lander', 'offer', 'country', 'device', 'day']) {
|
|
1093
1552
|
if (args?.[dim]) qs.set(dim, String(args[dim]));
|
|
1094
1553
|
}
|
|
1554
|
+
for (const [dim, v] of Object.entries(args?.filters || {})) {
|
|
1555
|
+
if (v) qs.set(dim, String(v));
|
|
1556
|
+
}
|
|
1557
|
+
if (Array.isArray(args?.group) && args.group.length) qs.set('group', args.group.join(','));
|
|
1558
|
+
if (args?.breakdowns === false) qs.set('breakdowns', '0');
|
|
1095
1559
|
const q = qs.toString();
|
|
1096
1560
|
result = await api(`/api/campaigns/${Number(args.campaign_id)}/stats${q ? `?${q}` : ''}`);
|
|
1097
1561
|
break;
|
|
1098
1562
|
}
|
|
1099
1563
|
|
|
1100
1564
|
case 'list_properties': {
|
|
1101
|
-
result = await api(
|
|
1565
|
+
result = await api(`/api/properties${args?.include_archived ? '?include_archived=1' : ''}`);
|
|
1102
1566
|
break;
|
|
1103
1567
|
}
|
|
1104
1568
|
case 'create_property': {
|
|
@@ -1132,8 +1596,11 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
1132
1596
|
break;
|
|
1133
1597
|
}
|
|
1134
1598
|
case 'get_property_stats': {
|
|
1135
|
-
const
|
|
1136
|
-
|
|
1599
|
+
const qs = new URLSearchParams();
|
|
1600
|
+
if (args?.days) qs.set('days', String(Number(args.days)));
|
|
1601
|
+
if (args?.tz) qs.set('tz', String(args.tz));
|
|
1602
|
+
const q = qs.toString();
|
|
1603
|
+
result = await api(`/api/properties/${Number(args.property_id)}/stats${q ? `?${q}` : ''}`);
|
|
1137
1604
|
break;
|
|
1138
1605
|
}
|
|
1139
1606
|
case 'list_property_pages': {
|
|
@@ -1146,17 +1613,15 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
1146
1613
|
}
|
|
1147
1614
|
|
|
1148
1615
|
case 'get_tracker_overview': {
|
|
1149
|
-
const
|
|
1150
|
-
|
|
1616
|
+
const a = { ...(args || {}) };
|
|
1617
|
+
if (a.from) delete a.days;
|
|
1618
|
+
result = await api(`/api/tracker/overview${qsFrom(a, ['days', 'from', 'to', 'tz', 'offer_id', 'property_id'])}`);
|
|
1151
1619
|
break;
|
|
1152
1620
|
}
|
|
1153
1621
|
case 'get_tracker_events': {
|
|
1154
|
-
const
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
}
|
|
1158
|
-
const q = qs.toString();
|
|
1159
|
-
result = await api(`/api/tracker/events${q ? `?${q}` : ''}`);
|
|
1622
|
+
const a = { ...(args || {}) };
|
|
1623
|
+
if (a.from) delete a.days;
|
|
1624
|
+
result = await api(`/api/tracker/events${qsFrom(a, ['days', 'from', 'to', 'q', 'stream', 'event', 'campaign_id', 'offer_id', 'property_id', 'link_id', 'utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'sort', 'dir', 'limit', 'offset'])}`);
|
|
1160
1625
|
break;
|
|
1161
1626
|
}
|
|
1162
1627
|
|