Reference

Design for Secure Revenue Projection

For: marketing and growth leads, analytics and data engineers, designers who build the consent and account surfaces, and the compliance reviewer who signs off what leaves the store. Genre: design specification.

A revenue signal is only as trustworthy as the least-checked hop it passed through. Design the checks as a component, with states and tests, the same way you design a button.

Document control
StatusPublished; the component is partly built, and each gap is a failing test
Version1.0
OwnerPlatform security lead, with the marketing data owner
Evidence basispersephonepunch/ucp-purchase-agent at b753d6c (main); the production Worker at 282d947
Measured24 October 2026
Review cycleQuarterly, and whenever a channel or a vendor is added

Vocabulary

The same words are used loosely in marketing, engineering and compliance. Here they mean one thing each:

TermMeans hereDoes not mean
Revenue projectionSending a revenue signal about customers from the store's own record to a channel that acts on it: a conversion value for bidding, a predicted value, an audience list, a product feed priceA financial forecast. Forecasting (predicted lifetime value) is one input to a projection
Revenue signalA conversion value, a predicted lifetime value (pLTV) score, an audience membership, or a price in a feedRaw orders or customer records. Those stay in the store's own systems
ChannelThe system that receives the signal and acts: Google Ads, Customer Match, Merchant Center, GA4, Klaviyo, MetaThe plugin that carries it. The plugin is transport
Projection targetA channel's adapter: the code that shapes a signal for one channelThe record. The record has one home; targets get copies
ComponentA designed part with inputs, states, guarantees and tests, reused at every place it appliesA review, a policy document, or a one-off check in one integration
Security componentThe part every projection passes through that decides send, hold or refuse, and leaves a record of the decisionEncryption alone, or TLS alone. Both are inside it, and neither is all of it
PurposeWhy a record exists: marketing, analytics, support, fraud_prevention. It decides which channels may receive itA tag for reporting

The question this answers

Every store already projects revenue. The Google channel sends order values. A Klaviyo flow sends spend. A pLTV model sends predicted values. The question is not whether to do it, but who can prove, afterwards, that each signal was allowed to leave, and was the signal it claimed to be.

Most stacks answer with a review: a privacy assessment before launch, a penetration test once a year. A review checks the design once. Revenue signals leave every few seconds, through plugins that change monthly. The answer that holds up is a component that runs on every signal.

The components of a revenue projection

A projection is built from six components. Five are familiar. The sixth, security, is usually left out of the design and added as a review, which is the gap this document closes.

ComponentJobLives inWhat breaks without it
IdentityOne pseudonymous ID per customer, linked to each vendor's IDXano, behind CloudflareEach plugin's mapping table becomes the identity record, and it leaves with the plugin
ConsentThe customer's current choice for each signal (ad_user_data, ad_personalization, analytics_storage)Xano, written when the customer makes a choiceConsent is inferred from a marketing opt-in, and nobody can show it was granted
ValueThe number sent: order total, or a pLTV scoreBigQuery ML scores, returned to the WorkerBidding learns from first orders only
PurposeWhich uses a record is allowedOn every record on the busA fraud score or a support ticket reaches an ad audience
Channel adapterShapes the signal for one channelWorker, one adapter per channelEach channel's quirks leak into the record
SecurityDecides send, hold or refuse at every hop, and records itWorker and Xano, on every projectionEvery other component can be bypassed, and no one can tell

Identity, consent, value and purpose describe the signal. The adapter shapes it. Security is the only component that checks the other five actually held.

The security component

Contract

The component takes one signal and returns one decision:

input:   signal (value, pseudonymous ID, destination, purpose)
         + the consent state for that ID, read now, not cached from earlier
         + the sender's proof (a webhook signature, a service key)
output:  send | hold | refuse
         + reason
         + a record: what, to where, under which consent and purpose, when

It runs at every hop, not once at the edge. A check that only runs at the edge is bypassed by the first internal job that calls a channel directly.

States

StateWhenWhat happens to the signalWhat the record says
sendSender verified, purpose allows the destination, consent granted for this signalShaped by the adapter and deliveredSent, with the consent state and purpose
holdConsent unknown, or the consent service didn't answerQueued; nothing leavesHeld, with the reason. A held signal is retried, never assumed granted
refuseSender not verified, purpose doesn't allow the destination, consent denied, or the tenant's own gate isn't set upDroppedRefused, with the reason
degradedThe decision was made, but the record couldn't be signed (signing key expired or unavailable)The decision standsWritten unsigned and marked, so a later job can sign it. The record is never lost to save the signature

The rule across all four: every failure fails toward less exposure. A gate that can't reach the consent record holds; it doesn't send.

Checks at each hop

HopSecurity checkWhy it is here, not elsewhere
Order or event arrives (Shopify, Stripe, a vendor webhook)Verify the sender's signature in constant time before reading the bodyAn unsigned or forged event would become a revenue signal
Record joins the busParse into a declared shape; refuse unknown fields and oversized bodiesInjection happens at transfer points, in whatever code reads the payload
Identity is attachedUse the pseudonymous ID; never put the email or phone on the busA leak of the bus then leaks no contact details
Value is computedThe model only reads and scores; it never decides accessA model that decides who may be used can't be audited the way a rule can
Gate before a channelAsk the release gate per person, per destination: purpose, then consent, then the tenant's own gateConsent is per person. A batch stamped "granted" is the most common silent failure
Adapter sendsOne Google account per tenant; hashed contacts only; keys held as Worker secretsA shared or mistyped account ID sends one store's customers to another's audience
Channel acknowledgesRecord the result on the hash-chained ledger, signed with a key the merchant holdsThe record must outlive the plugin, the vendor and the vendor's logs
Domain and transportTLS everywhere; CAA names the CAs that may issue for the domainA distrusted CA lets someone impersonate an endpoint the channel calls back
ErasureFan out from the identity spine to every channel copy, each with its own recordErasure that reaches the app it was sent to, and not the audiences, isn't erasure

Failure modes, as they look from outside

Each of these looks exactly like a working system until someone checks the record:

The two shapes the component checks

A security component can only check what it can read. Two shapes carry nearly every revenue signal: the product shape Google Merchant reads, and the customer event shape on the user tags bus. Both are fixed shapes, not free-form payloads, which is what lets the component, and an AI caller, rely on them.

The Google Merchant data layer shape

A product's price, availability and identifiers are revenue signals too: they decide what an ad or an AI shopping agent offers, and at what price. CRM Sync composes them in the Merchant API products_v1 shape, from one composer that feeds three surfaces: the public catalog at /merchant/products, three read-only Merchant tools on the MCP endpoint (merchant_get_product_by_name, merchant_list_products, merchant_list_aggregate_product_statuses), and a JSON block inside each product page. One composer means the three can't disagree.

One product, as served today:

{
  "name": "accounts/example.myshopify.com/products/en~US~<offerId>",
  "offerId": "<offerId>",
  "contentLanguage": "en",
  "feedLabel": "US",
  "productAttributes": {
    "title": "…", "description": "…", "link": "https://…", "imageLink": "https://…",
    "price": { "amountMicros": "129000000", "currencyCode": "USD" },
    "availability": "IN_STOCK", "condition": "NEW", "brand": "…",
    "identifierExists": false,
    "productTypes": ["…"],
    "shipping": [{ "country": "US" }],
    "shoppingAdsExcludedCountries": ["KP", "IR", "SY"]
  },
  "productStatus": { "itemLevelIssues": [
    { "code": "missing_identifier", "severity": "warning", "attribute": "gtins",
      "description": "Neither GTIN nor MPN present" } ] }
}
FieldSecurity ruleWhat goes wrong without it
name, offerIdThe offer ID is part of the resource name and never changes or gets reusedA reused ID moves one product's history, reviews and bids onto another
price.amountMicros + currencyCodeRead from the live store, in the market's own currency, never convertedAn exchange-rate move changes what an ad promises
availability, conditionEnum values (IN_STOCK, NEW), not free textA typo is ignored by Google, and the offer silently stops serving
gtins or identifierExists: falseA real identifier, or a plain statement that there isn't one. Never an invented oneA made-up GTIN is a false claim to Google and to every agent that trusts it
shipping, shoppingAdsExcludedCountriesShipping countries come from the store's country rules; sanctioned countries are excludedAn ad reaches a market the store may not sell to
productStatus.itemLevelIssuesMissing image, price, identifier or description reported, not hiddenA broken offer looks healthy until it is disapproved
The JSON block in the page< is escaped, and the block is stripped and rewritten as a unitA product description becomes a script on the page

The walk from a flat product CSV to this shape, including why a missing Handle makes the same file read as 4 products or 169, is in From product.csv to Consented Conversions.

The user tags event bus

The customer side moves as events, not as copies of the customer record. Three tables carry it:

TableHoldsSecurity property
Tag catalogEach tag's key, kind, label, scope, whether it emits events, its event topic, and whether it is consent-linkedThe catalog has no personal data. It is mirrored to Shopify as user_tag metaobjects, so a theme can read tag definitions and never membership
Tag membershipWhich user holds which tag, with status, a version number, timestamps, and what fired the changeWithdrawing consent ends consent-linked memberships, bumps the version and records fired_by: consent_withdrawn
Event bus (ucp_funnel_history)One row per event: an event ID, an idempotency key, the user, the domain, the event type, status and version, a payload, the consent basis, the region, what fired it, its source, and the channels it may reachA redelivered event with the same idempotency key is a no-op

Seven domains write to the bus today: tag, consent, order, return, agentic, omnibus_discount and audience. An order event carries a conversion record in its payload, built for revenue projection rather than as a log line:

Conversion record fieldWhy it is in the record
value_minor + currency + marketValue in minor units in the market's currency, never converted, so a past conversion's worth can't drift
Identity per scopeThe person's account ID is recorded separately from a browser's cookie ID, so one customer on two devices isn't trained as two
consent + consent_version, as at captureConsent is a fact about a moment. Exporting under today's consent would claim a basis that didn't exist when the data was collected
pltv_score, pltv_model_version, pltv_scored_atThe prediction as it stood, with the model that made it, so it can be checked against what actually happened
realized_value, realized_atWhat the customer actually spent, recorded later, so the prediction can be marked right or wrong

What the bus doesn't do yet. It is an event log, not yet an evidence chain:

Setting up an AI caller against both shapes

An AI agent, a chat assistant or an automated job reads these shapes to decide what to offer and what a customer has agreed to. The security component treats it like any other caller:

RuleState
The agent reads products through the Merchant shape only: the public catalog or the three read-only Merchant tools, never the store admin APIBuilt
The agent's actions are written to the bus under the agentic domain, each with an idempotency keyBuilt
Orders an agent places pass through the same release gate before any conversion or audience is sentBuilt for the destinations listed above
The agent spends only inside a signed mandate, and revoking the mandate stops itBuilt (mandate enforcement has its own test suite)
The agent reads a customer's tags only through a tool scoped by its mandate, and can't set a consent-linked tagPattern, not built
Bus events an agent relies on are chained and signed, so it can prove what it acted onNot built

What is built, and how it is tested

The component is partly built. Each part is pinned by a test in the CRM Sync test harness: green tests guard what is built, and red tests fail on purpose until a gap is built.

CheckStateEvidence
Webhook signatures compared in constant timeBuiltGreen test: no plain === on the signature
Release gate asked per person before Customer Match, GA4 purchase and user properties, and Merchant reviewsBuiltGreen tests in the release-gate suite; the gate fails closed when a tenant's own gate isn't provisioned
One Google account per tenantBuiltGreen tests in the Google-bindings suite: an account already bound to another store is refused
Consent and erasure records chained by seq and prev_hash, signed with Ed25519, kept when signing failsBuiltGreen tests: chain constraint, signing, degraded path
Erasure fan-out to Shopify, Klaviyo, Salesforce, Adobe, Google Customer Match, the pLTV score and agent mandatesBuiltGreen test; the Google removal is written to the chain
CAA names the permitted CAs, with a valid reporting address; no emSign in any served chainBuiltGreen tests, run live against DNS and the served certificates
No private key in the source; the public key list carries public keys onlyBuiltGreen tests
Purpose tags that keep fraud and support data out of ad destinationsNot builtRed test
Erasure to helpdesk, fraud and consent-platform vendors (Gorgias, Sprinklr, Signifyd, OneTrust)Not builtRed test
Arrival time recorded and clock skew flagged on inbound vendor eventsNot builtRed test
A Rust/WebAssembly shape gate on inbound vendor payloadsNot builtRed test
One table linking every vendor's customer ID to the identity spineNot builtRed test
The per-destination consent mapper called on every sendWritten and tested, not yet called; the release gate decides today—
One Merchant composer for the catalog, the Merchant tools and the page block, in the products_v1 shapeBuiltLive: /merchant/products returns productAttributes, enum values and the en~US~<offerId> name
Bus events idempotent per key, consent-linked memberships ended on withdrawalBuiltCode; no harness test yet
Bus writes retried through an outboxNot builtNo test yet
Bus rows chained and signedNot builtNo test yet

The vendor-by-vendor detail behind the red rows is in The plugin in the middle, and which channel data may feed conversions, audiences, pLTV or model training is in Google audiences and conversions with Vertex AI, and where LoRA fits. The CAA steps, and a script that tests a domain red then green, are in CAA records.

Designing the visible part

Most of the component is invisible. Three parts reach a screen, and they are designed like any other component, with states:

SurfaceStates to designWhat it must never do
Consent banner and preferencesNot yet asked; granted per signal; denied per signal; withdrawnOffer one "accept" that grants ad signals the customer never saw named
Account page ("what we share")Shared with each channel; held; erased, with the date of eachShow a channel as connected after an erasure was sent but not confirmed
Merchant dashboard (projection health)Sent, held and refused counts per channel, with reasonsShow only sent. A dashboard without held and refused hides the gate

The account and dashboard states come straight from the component's records, so the screen can't disagree with what was sent.

What it costs

What to do next

  1. List every revenue signal that leaves the store today, with the channel, the plugin that carries it, and the purpose of the data. Most teams find signals no one owns.
  2. Write the security component's checks as tests before changing anything. Each check you can't pass yet is a red test, and the list of red tests is the plan.
  3. Put the gate in front of the highest-volume signal first, usually the audience upload or the conversion value, and make it decide per person.
  4. Move identity and consent to a record you own, so the gate reads the current choice, not the plugin's copy.
  5. Add the visible states to the banner, the account page and the dashboard from the same records.
  6. Turn one red row green at a time, and add each new channel with its adapter, its erasure call and its test together.

Resources and downloads

This document:

FormatLink
Web articledesign-secure-revenue-projection.html
PDF downloaddesign-secure-revenue-projection.pdf
In the CRM Sync knowledge baseKnowledge base → Design for Secure Revenue Projection
Book chapterSecurity → Design for Secure Revenue Projection
The two data shapesThe two shapes the component checks

Companion brief: AI Security: Challenges, Benefits, and Tools (24 October 2026, 5 pages). It pairs five AI security risks (memory-unsafe native stacks, prompt injection, model supply chain, data leakage, over-privileged agents) with the defence each one gets from AI, shows a reference architecture for a secure AI data layer, and lists open-source and commercial tools and the frameworks auditors use (OWASP Top 10 for LLM Applications, MITRE ATLAS, NIST AI RMF, Google SAIF).

The brief's architecture is the target design. Its Rust Worker trust boundary corresponds to the Rust shape gate, which is a red row in What is built, and how it is tested: read the brief for where the design is going, and this document for what holds today.

Related sections: