Sift Sift
Breathe first. Scroll later.

Dashboard

What happened each day — and whether it is trending up or down.

→
A tile with no vs prior pill has nothing to compare against — the earlier window totalled zero, so the change is undefined, not 0%. On Blended CPA and Blended TTR the pill is a relative change, not a change in points, and a — means the window had no installs (or no impressions) to divide by.

Subscriptions · flows over 28 days, balances as of now

RevenueCat-parity tiles. Each carries how its number was derived — a tile marked estimate is measured off a proxy, and its own caption says which way it reads against RevenueCat. A tile reading not connected has no data at all; that is not the same as zero. The first three count what happened over a fixed 28-day window; the last three are balances as of now, so their sparkline ends at the window's last complete day while the headline is current. Independent of the Range control above.

Experiments

Your ASA lab notebook — every theory, the change that tests it, and the verdict.

This is the layer Apple's console and a plain dashboard don't have: a hypothesis ledger that survives the ~24h reporting lag and a 7-day runway, and turns each finding into a reusable rule.

🌱 Learnings & rules adopted

The durable knowledge experiments leave behind — these constrain every future recommendation.

    Campaigns

    One row per campaign. Click a header to sort.

    →
    TOTALS is the account total, not the sum of the rows above — Apple masks low-volume terms, so per-campaign rows can under-count. — means we hold no data for that cell; 0 means we hold data and it is zero. Columns up to Spend are Apple; Tracked onward are RevenueCat, and Tracked is never the same number as Inst. Serving is Apple's answer to “is this actually running?”, not the status you set — the two disagree routinely, and a badge with no reason chips means Apple gave us no reason.

    Purchases

    Every RevenueCat purchase, newest first, with the campaign each is attributed to.

    →
    unattributed — resolving is the honest label while AdServices has not answered yet — it self-fills once the install resolves (or stays, for an organic payer). A sandbox row is a test purchase: visible here so testing is verifiable end-to-end, but never counted as revenue. Dates are inclusive UTC days. Purchases arrive live, so a sale landing while you page can shift a row across a page boundary.

    Keywords

    Last 7 days · band by TTR · click a header to sort.

    →
    🟢 ≥4%   🟡 2–4%   🔴 <2% TTR — banded on the unrounded rate, so a value shown as 4.00% can sit in the amber band. A keyword with no impressions has no rate to band, so it carries no dot; the ○ No impressions chip filters to those rows. Rows under ~100 impressions are directional only. CPA shown where installs > 0. Match, Status and the band chips compose as an AND, and the chip counts are the rows inside the two selects — so a chip's number is what clicking it shows. Status opens on Active + Paused: keywords Apple has deleted are still reported here, and they are one selection away.

    Search Terms

    What people actually typed — the source of negative-keyword discovery.

    Reports

    Pick a dimension, a window and a timezone; filter it; sort any column. Save it and it comes back.

    →
    TOTALS is the account total, not the sum of the rows above — Apple masks low-volume terms, so per-entity rows can under-count. — means we hold no data for that cell; 0 means we hold data and it is zero. TTR, CR, CPA and CPT are re-derived from the summed counts, never averaged from the daily rows.

    All reports

    A saved report stores the query, never the numbers — opening one re-runs it against today's data.

    Timeline

    Every account change on one axis — so you can read results against the change that caused them.

    Blended TTR, with change marker

    Change log

      AI Usage

      Every call we made to a model, what it cost, and why.

      Cost is computed at the moment of the call and stored — never re-derived from today's price list, so a price change cannot rewrite what you already paid. Shown to six decimals because a single call routinely costs less than a cent.

      Data & Sync

      Where this account's data came from, and when.

      Sync history

      Every run this account has made — scheduled and manual alike.

      One tick of the schedule writes three rows — sync, experiment eval, alert — so “did we pull fresh data?” and “did we raise anything?” stay separately answerable. Rows is what that job wrote: metrics for a sync, read-outs for an experiment eval, alerts for an alert. A fourth job, eval, ran between the sync and the experiment eval until the suggestion queue was removed; its runs are still listed, because Sift deletes nothing.

      The pipeline

      ① Apple Ads API v5
      Read-only pull: campaigns, ad groups, keywords, search-term reports.
      →
      ② MySQL
      Per-day metrics, reconciled against Apple's own grand totals before they land.
      →
      ③ Read out & alert
      Due experiments get their read-out, then deterministic rules raise alerts. No model is called in this phase.
      →
      ④ This view
      Dashboard, experiment ledger, scorecard.

      Webhook deliveries

      Every inbound RevenueCat delivery, newest first — and what Sift did with it.

      Admin-only. A rejected (401) delivery is logged with no body — so a mis-set REVENUECAT_WEBHOOK_SECRET shows up here, not only in the server logs. The Payload of an accepted delivery is the body as received, secret-scrubbed. Nothing is ever deleted from this log — there is no retention window and no nightly sweep.

      Live payload

      What the shipped app is told at launch — changeable without a release.

      Exactly what GET /api/v1/app-config returns for this account right now, from the same code path — so it cannot disagree with what the app receives. A disabled key is absent, never null: absence means the binary uses its compiled default, and {} is a complete, correct answer for an account with nothing turned on.

      Loading…

      Config keys

      Every key on the account, and what each one is serving — enabled or not.

      Every key on the account, enabled or not — a staged-off key is the thing you came here to turn on, so hiding it would hide the workflow. Enabled is the staging mechanism: there is no draft/publish step, so turning a key on is what puts it in the Live payload on every client's next launch.

      Deleting a key does not restore a previous value — it reverts every client to its compiled default on the next launch, the same blast radius as a value change and easier to do by accident. Only the audit trail keeps the last value. Reordering rules is a config change too: first match wins, so moving one above another can invert the answer for a whole storefront.

      Audit trail

      Who changed this account's configuration, when, and what it used to be.

      Newest first. Keys and rules both — a rule is named key#priority, and on a rule that moved, the priority shown is the one it left.

      Nothing can edit or delete a row here — the database refuses UPDATE and DELETE on this table for every user, including root. This is where a deleted key's last value survives.
      ⚠ Two things this does not do. Deleting the account destroys the whole trail: that foreign key cascades, and a cascading delete does not fire the triggers that make this table append-only. And it is not a safety net for a secret pasted into a value — that value is already on the wire to every device and already visible in the Keys table, so hiding it here would only remove the place you could discover it happened.
      ⚠ A change with no name against it is ambiguous, not anonymous. The column that records the actor is cleared when a user is deleted, and it is also how an automated change is recorded — so those rows read “system, or a user who has since been deleted”. There is no field that tells the two apart.

      Config deliveries

      Every launch that asked this account for its config, newest first.

      A 304 is a successful delivery — a warm cache revalidating — and it means that device is actively running that version. Click a version to see the exact document it carried.

      Every delivery is recorded — no sampling, and nothing is ever deleted. So an empty result means no such request was served, provided the delivery log has been on and healthy for the window you are looking at. A version with no payload to expand was still served: the account's snapshot budget was exhausted, or that write failed.

      Alerts

      What the pipeline noticed — and what has already been acknowledged.

      Acknowledging is one-way. There is no way to un-acknowledge an alert and no manual resolve. Only the rule-driven types — CPA spike, budget capped, keyword drag, TTR drop, spend anomaly — are ever auto-resolved, and only while they are still open, so sync failure, sync stale and experiment due stay acknowledged for good: the Acknowledged list only grows. That is the design, not a sweep that has stopped running.
      Opening this screen is what raises — and what clears — the pipeline heartbeat. sync stale is the one alert no background job can raise, because the failure it reports is the job not running, so it is evaluated when this screen reads the inbox. The check runs both ways: while the data is stale a row is raised, and the moment the pipeline is healthy again every still-open sync stale row is moved to resolved — by the read itself, on the spot. So a sync-stale row that appears the moment you look, or that reads resolved with a timestamp from a second ago, is the check working rather than a coincidence. It is the one status change here that nobody clicked, and All statuses is the view it is visible in. Acknowledging needs the operator role or higher; a viewer sees the inbox without the button.

      Settings

      Your account and password, the provider keys Sift holds, and the API reference for Claude Code.

      Credentials

      Provider API keys for the vault. The secret travels inbound only — it is verified against the provider before it is stored, and what comes back is a masked fingerprint, never the key itself. Submitting for a provider that already has a key rotates it.

      Deleting destroys Sift's copy of the key; the row is kept for audit and can be replaced by adding the provider again. It does not revoke the key at the provider — do that in their console too.

      Change password

      Changing your password signs out every other device immediately. You will stay signed in here.

      At least 12 characters, and not one that appears in a breach list.

      API reference for Claude Code

      A single Markdown file describing every Sift endpoint and the conventions a client needs — auth, money units, the report filter grammar, rate limits, and what it can and cannot do. Hand it to the Claude Code CLI so it can drive Sift over HTTP instead of guessing.

      New experiment

      A theory is only real once it's written down with the change that tests it.

      Change a value

      Type the value the way JSON carries it: a string as-is with no quotes, an int or float as a number, a json value as JSON. The line above quotes a string value so you can tell "5" from 5 — the field never does.

      Delete

      Add a key

      Create a typed default. The new key is staged off until you enable it from the table.

      The key and its type are immutable after creation — a key that changes type mid-flight is a decoding crash in a binary already on people's phones that cannot be patched. To change a type, add a new key and retire the old one. Type the value the way JSON carries it: a string as-is with no quotes, an int or float as a number, a json value as JSON.

      Add a rule

      A default plus an ordered list of overrides — first match wins. ⚠ Do not write the negative. country ≠ CA → false is not a rule, it is the key's value. Encoding both directions is how a rule set acquires two rules that disagree and a serving order nobody can reconstruct.

      Conditions

      All conditions must match.