Authoritative spec for building a ViewFi campaign through the MCP connector.
Served to connected models as viewfi://docs/campaign-requirements.
Keep this file in sync with the create_campaign input schema in
server/mcp/tools/campaigns.ts — that schema is what actually validates, this
document is what the model reads before calling it.
This connector builds and configures campaigns. It does not expose viewer data: there are no tools returning contact details, individual claim rows, or any per-person record. Analytics are aggregate only. Do not attempt to retrieve viewer identities through it.
It also cannot delete anything. Deletions happen in the dashboard.
Settle this before creating or changing anything. An API key has one of two
scopes, and get_connection_info tells you which you have.
Organisation-scoped key — reaches every brand seat in the organisation.
There is no default seat. These tools require an explicit brandSeatId:
get_brand_seat, set_brand_colors, get_brand_seat_wallet,
list_campaigns, create_campaign, list_ad_tags,
get_brand_seat_analytics, get_brand_seat_ad_analytics
Call list_brand_seats and confirm with the user which seat they mean. Do
not guess, and do not assume the first seat in the list. Omitting brandSeatId
returns an error saying so — it never falls back to a default.
Brand seat-scoped key — fixed to one seat. brandSeatId is optional and
supplying a different seat's id is rejected, not ignored.
Tools addressed by campaignId never need brandSeatId: the campaign already
determines its seat, and access is checked against it.
Campaigns are created in the seat you name, and create_campaign echoes back
brandSeatId and brandSeatName — check them against what the user asked for.
Every campaign is created as draft. create_campaign always sets this and
ignores any status passed in.
draft → test → live
Move between states with update_campaign. Do not attempt to create a campaign
directly in live.
Test simulates wallet connections and blockchain transfers — no real value moves. Live distributes real rewards.
| Field | Type | Notes |
|---|---|---|
title |
string | 1–255 chars |
campaignType |
enum | see table below |
videoSource |
youtube | cloudflare_stream | none |
defaults to youtube |
videoId |
string | YouTube video ID or Cloudflare Stream UID. Required unless videoSource: 'none' |
templateId |
integer | FK into the templates table. Call list_templates for valid ids — it is not a slug like minimal. |
campaignExpirationDate |
ISO date string | |
claimDuration |
integer | seconds the viewer must watch; defaults to 4 |
Optional on any type: description, logoUrl, splashImageUrl,
redeemRewardUrl. All URL fields must be absolute URLs.
splashImageUrlThis is the image shown before the video plays. If left empty on a YouTube campaign, the platform auto-fills it with the YouTube thumbnail. Set it explicitly when the brand has its own cover art — otherwise the campaign ships with a YouTube still.
It is not the end-screen image; those are separate fields set in the dashboard.
Pass the short name in the left column. The connector maps three of these to
longer values the database stores; you never need to write the stored value
yourself, and list_campaigns accepts either.
campaignType |
Stored as | Also required |
|---|---|---|
airdrop |
airdrop |
arcBlockchain, arcNetwork, arcAmountPerClaim, arcBudgetAmount |
coupon |
coupon_campaign |
couponType — then see below |
voucher |
voucher_campaign |
voucherType — then see below |
reward_link |
reward_link |
rewardLinkType, plus a URL or a bulk upload |
contest |
contest_campaign |
contestPrizeTitle, contestDrawDate, contestWinnerCount |
ugc_coupon |
ugc_coupon |
robloxTotalRewards, robloxClaimUrlTemplate |
mint_nft |
mint_nft |
rewardConfig (NFT metadata) |
arcBlockchain: arc | base | tempoarcNetwork: testnet | mainnetcouponType: single_use | bulk_uploadvoucherType: single_code | bulk_uploadrewardLinkType: single_link | bulk_upload (single accepted as an alias)couponType: 'single_use' → also requires singleCouponCode and
singleCouponQuantity. One code, issued up to N times.couponType: 'bulk_upload' → create the campaign first, then call
bulk_upload_coupon_codes with the code list. The campaign has no inventory
until that second call succeeds.rewardLinkType: 'single_link' → singleRewardLink (one URL for every
claimant). Not 'single' — the claim path matches single_link, and a
campaign stored as single falls through to the bulk path, finds no uploaded
links and fails at claim. 'single' is accepted and normalised, but prefer the
stored value.rewardLinkType: 'bulk_upload' → totalRewardLinks, then
bulk_upload_reward_links.voucherType: 'single_code' → also requires singleVoucherCode (the code
itself). One code for every claimant.voucherType: 'bulk_upload' → totalVouchers, then bulk_upload_vouchers.voucherValue is a display string, not a number — "$10 OFF", "20% OFF".
Optional extras: voucherDescription (redemption instructions),
voucherProvider, voucherExpirationDate (ISO), voucherTermsConditions.
contestManagerEmails is an optional array of addresses notified when winners
are drawn. contestDrawDate is an ISO date and should be after
campaignExpirationDate in practice — draw after entries close.
ugc_coupon)COPPA compliance is enabled automatically for this type. robloxClaimUrlTemplate
is the deeplink template used to hand the code back to the game.
For any bulk type, the sequence matters:
create_campaign → returns the campaign idbulk_upload_* with that idupdate_campaign to move draft → testupdate_campaign to move test → liveMoving a bulk campaign to live before step 2 produces a live campaign with
zero inventory, which fails at claim time rather than at configuration time.
Read viewfi://templates. It returns every active template and its palette,
alongside the brand seat's brandColors. With an organisation-scoped key it
returns brandColorsByBrandSeat instead — a palette per seat, so use the entry
matching the seat you are building for, not the first one.
Templates are authoritative. A template's palette is an internally consistent set — primary, accent, text, border, glass and shadow are derived together. Brand colours are not layered over it: overriding three keys while the other five stay derived from the template produces a mixed palette, and makes template changes look broken because the brand colours win every time.
So use brandColors to choose the template, not to override it:
brandColors is set, pick the template whose palette is closest to it.brandColors is null, pick the one closest to the brand logo — a red
logo should not get a blue template.Either way, say which template you chose and why, so the operator can correct a wrong guess.
set_brand_colors records the palette on the seat so later campaigns inherit it
instead of re-deriving it each time. It merges, so setting one colour does not
clear the others.
termsAndConditions is the campaign's own terms, shown in a modal when the
viewer ticks the consent box.
Resolution order in the widget:
termsAndConditionsvoucherTermsConditions — voucher campaigns only/termsOnly the first two are labelled "
Set termsAndConditions whenever a brand has supplied its own. Line breaks are
preserved and the modal scrolls, so paste the full text rather than summarising.
get_campaign_share_links returns the public widget URL, the embed iframe, and
the branded subdomain URL when one is configured.
A campaign only becomes reachable once it leaves draft, and the tool says so
rather than handing back a URL that 404s. publicId is assigned by the
platform — a campaign without one has no shareable link yet.
Set with liveModeAuthProviders / testModeAuthProviders on create or update.
Live campaigns support google and email only; everything else is test-mode
only. Defaults to ["google"] when omitted.
Auth providers are additive on live campaigns: they can be added after going live, but not removed, because a viewer who authenticated with a provider that later disappears cannot finish their claim.
upload_campaign_image takes an asset the user already has — a URL or base64 —
and assigns it to a slot. Prefer it always.
| target | Field | When the viewer sees it |
|---|---|---|
splash |
splashImageUrl |
Before the video plays |
end_screen |
endScreenImageUrl |
After the video finishes, before they claim |
reward |
rewardAssetUrl |
After they claim, on the success card |
logo |
logoUrl |
Throughout |
splash and end_screen are 16:9 and displayed full-bleed. reward is
square.
Never put the reward image on the end screen. It spoils the post-claim reveal, and brand artwork for a claimed reward often carries copy that has not been approved for a viewer who has not claimed yet.
If endScreenImageUrl is unset, the player falls back in this order, so most
campaigns need nothing set at all:
endScreenImageUrl — explicit overrideendImageVerticalUrl / endImageHorizontalUrl / endImageUrl — existing dedicated end imagessplashImageUrlSet endScreenImageUrl only when the brand wants a different frame at the end
from the one at the start.
endScreenClaimText overrides the end-screen button. Left unset, it defaults
per campaign type — "UNLOCK VOUCHER", "UNLOCK CODE", "ENTER NOW" and so on.
Do not set it to anything containing "reward": several brands reject that
wording, and the per-type defaults already avoid it.
generate_campaign_image creates new artwork with OpenAI. It costs money per
call and produces unreviewed creative. Do not call it as part of building a
campaign, and do not offer to replace artwork the user has already supplied.
Call it only when the user explicitly asks you to generate an image. It is
rejected on live campaigns.
If it errors with "model does not exist", image generation is misconfigured on the server — say so rather than retrying, and carry on without the image.
Mutating tools require a read_write API key. With a read key, the following
are rejected: create_brand_seat, create_campaign, update_campaign,
duplicate_campaign, bulk_upload_coupon_codes, bulk_upload_reward_links,
bulk_upload_vouchers, create_ad_tag.
Scope is separate from permission — see "Which brand seat am I acting on?" above. A brand seat key reaches one seat; an organisation key reaches every seat in its organisation and can create more. Neither can reach another organisation's seats, regardless of the caller's org membership.