DB Olympics

Queries · q210 · Chief Executive Officer · week 2, night 1

Merchant risk signals snapshot: receivables, traffic and revenue trend

“Which merchants look at risk right now, based on unpaid invoices, traffic decline, and revenue trend — our 'accounts to watch' list?”

Manager · asked the questionMaren OkaforChief Executive Officer. Joined two weeks ago from a European payments company. Board meeting at the end of January.
Analyst · planned the reportHannah BrooksMethodical: starts with an overview, then drills into whatever looks unusual.
Engineer · wrote the SQLOmarPragmatic: the shortest correct query, aggregates first and joins dimensions afterwards, FILTER clauses for conditional counts.

Fictional people at a fictional company, each played by a language-model agent. Who they are and what they are told

  1. A business event lands on the Chief Executive Officer's desk

    Week 2; shared with Tobias Lindqvist, Chief Financial Officer

    The board meets at the end of January and wants a clear view of unit economics: per merchant, vertical and country, what Tidewick earns and what it costs us to serve them. Which merchants are worth growing, and which cost more than they bring?

  2. Maren Okafor writes to Hannah Brooks

    The manager, at the start of week 2

    Subject: Great first week — here's where I want to go next

    Hannah,

    This was exactly the kind of grounding I needed — thank you. A few things surprised me and one big new ask from the board that I need us to lead together with Tobias.

    What surprised me / needs explaining:

    • Your fee-revenue figure (€53k trailing twelve months) and Tobias's (€93.6k for calendar 2025) don't match, and neither does your "72% from top 5, stable" concentration story line up cleanly with his "65% from top 10, declining as we add merchants." Before this goes near the board I need these reconciled into one number and one trend — I don't want finance and analytics telling different stories about the same business.
    • The refund story looks more nuanced than "refunds are creeping up broadly." Tobias's team says apparel's ~20% refund rate is just normal for the category, and the real spike this quarter came from a few smaller non-apparel merchants. I'd like your root-cause work to confirm or correct that before we flag it as a trend versus a one-off.
    • Operations found that delivery performance is almost entirely a carrier issue, not a merchant or warehouse issue — the same merchant looks great on one carrier and bad on another, and one carrier partner has clearly been underperforming since November. That changes how I think about "merchant health" — some of what looks like a merchant problem may really be a carrier problem.

    The new board ask — unit economics by merchant, vertical and country:

    The board wants to know, plainly, what we earn from serving each merchant/vertical/country and what it costs us — so we can say which parts of the business are worth leaning into and which are quietly expensive. This is bigger than one report, so here's what I think it needs, and I want you and Tobias to scope it jointly this week rather than me prescribing the method:

    • On the earnings side, we seem to have good data (processing fees, volume). Platform-plan fees are apparently missing from the warehouse — Tobias, can your team supply that separately so we're not missing a real revenue line?
    • On the cost side, I don't think we've ever assembled this: fulfillment/carrier cost, refund and return handling, support load, and the engineering reliability issues (webhook failures, the cart-reminder bug) that generate extra support tickets. Some of this may not be cleanly attributable per merchant in the data — I want an honest read on what's knowable now versus what needs to be modeled or estimated.
    • The deliverable I need by month-end: a short ranked view of which merchants/verticals/countries are clearly profitable to grow, which are marginal, and which likely cost more to serve than they return — even if the first pass is directional rather than precise.

    My questions for this week:

    1. Can you and Tobias reconcile the revenue and concentration numbers into a single, agreed figure and trend I can quote to the board without contradiction?
    2. What would it take to build even a rough cost-to-serve picture per merchant/vertical/country — and what's your honest confidence in it, given the data gaps you've already flagged (platform fees, refund-to-payout matching, carrier cost attribution)?
    3. On refunds — is the increase really concentrated in a few merchants/verticals as Tobias's team suggests, or is it broader? I want to know whether this is a few accounts to manage or a trend to worry about.
    4. Separately, flag for me which of our top accounts (like Castell Fasteners, with its traffic drop and unpaid invoices) look at risk — I want a short "accounts to watch" list alongside the unit economics work, not buried inside it.

    Let's talk briefly before you lock in this week's requests so we spend the slots on the unit economics scoping rather than re-running what we already mostly know.

    Thanks, Maren

    A note from the Chief Financial Officer, Tobias Lindqvist

    Note to Hannah Brooks

    Hannah — a few things from the finance side as you scope the board unit-economics piece:

    1. What does each merchant actually net us, after processing costs? We only see the economics clearly once payouts are broken into gross, fee, and net — a merchant with huge volume but thin margin on our fee side could look better on revenue than it really is on contribution.
    2. How exposed are we on currency? We earn and hold balances in many merchant currencies against a EUR view for the board — I want to know where FX swings could meaningfully move the 2025 numbers or the 2026 budget.
    3. Who's slow to pay us, and where? For the B2B wholesalers especially, I'd like a sense of outstanding receivables and how long they're taking to settle — that's a cost of serving them that doesn't show up in the headline revenue.

    If there's room, a rough read on how refund patterns differ by vertical or country would help round out the "which merchants are worth growing" question.

    Happy to talk through before Friday's memo.

    — Tobias

  3. Hannah Brooks plans the night

    The analyst · week 2, night 1, request 3 of 3

    Top priority tonight is giving Maren and Tobias's team concrete evidence to reconcile the revenue numbers, rather than just re-asserting my figure. I've cross-checked internally: my €53,186 fee revenue / €1,991,449 gross volume for 2025 is consistent across two independent reports (merchant-level and corrected vertical-level) built with an explicit, auditable method (status='paid', exact-date EUR conversion). Notably, Kofi's €93,583 figure is almost exactly 1.76x mine — the same ratio as the original buggy vertical report I caught and fixed in week 1 — which strongly suggests his team's query has a similar join fan-out (likely against exchange_rates or a one-to-many table), not a genuine methodology difference. A clean currency-level breakdown tonight will (a) give me hard evidence to bring to a joint session with Tobias's team, and (b) double as the FX-exposure view Tobias asked for. I'm also using tonight's other two slots on items Maren explicitly wants resolved this week outside the bigger unit-economics scoping: testing whether the refund uptick is concentrated (per Kofi's claim) or broad, and building the "accounts to watch" risk snapshot (which also answers Tobias's receivables-aging question). I'm deliberately not starting the cost-to-serve/unit-economics build yet — that needs a design conversation with Tobias before I spend slots on it.

    Merchant risk signals snapshot: receivables, traffic and revenue trend
    Question
    Which merchants look at risk right now, based on unpaid invoices, traffic decline, and revenue trend — our 'accounts to watch' list?
    One row per
    merchant (tenant)
    Columns
    • merchant name
    • vertical
    • unpaid invoice balance (EUR, open+past_due invoices)
    • web traffic change Q4 vs Q3 2025 (%, session count)
    • fee revenue change Q4 vs Q3 2025 (%)
    • days since last order placed (as of 2026-01-05)
    • tenant status
  4. Omar writes the SQL

    The engineer · 1 dry run on the empty schema, then the submission

    Dry run 1 · STATUS: COMPILES (dry run on the empty schema, no data read)
    with latest_rates as (
      select distinct on (quote_code) quote_code, rate
      from exchange_rates
      where base_code = 'EUR' and as_of <= DATE '2025-12-31'
      order by quote_code, as_of desc
    ),
    unpaid as (
      select i.tenant_id,
        sum(i.amount_due / case when i.currency_code = 'EUR' then 1 else lr.rate end) as unpaid_eur
      from invoices i
      left join latest_rates lr on lr.quote_code = i.currency_code
      where i.status in ('open','past_due')
      group by i.tenant_id
    ),
    traffic as (
      select tenant_id,
        count(*) filter (where started_at >= TIMESTAMP '2025-07-01' and started_at < TIMESTAMP '2025-10-01') as q3_sessions,
        count(*) filter (where started_at >= TIMESTAMP '2025-10-01' and started_at < TIMESTAMP '2026-01-01') as q4_sessions
      from web_sessions
      group by tenant_id
    ),
    payout_rates as (
      select p.id as payout_id, p.tenant_id, p.paid_at,
        p.fee_amount / case when p.currency_code = 'EUR' then 1 else er.rate end as fee_eur
      from payouts p
      left join exchange_rates er
        on er.base_code = 'EUR' and er.quote_code = p.currency_code and er.as_of = CAST(p.paid_at AS DATE)
      where p.status = 'paid'
    ),
    revenue as (
      select tenant_id,
        sum(fee_eur) filter (where paid_at >= TIMESTAMP '2025-07-01' and paid_at < TIMESTAMP '2025-10-01') as q3_fee,
        sum(fee_eur) filter (where paid_at >= TIMESTAMP '2025-10-01' and paid_at < TIMESTAMP '2026-01-01') as q4_fee
      from payout_rates
      group by tenant_id
    ),
    last_order as (
      select tenant_id, max(placed_at) as last_placed_at
      from orders
      group by tenant_id
    )
    select
      t.name as merchant_name,
      t.industry as vertical,
      round(coalesce(u.unpaid_eur, 0), 2) as unpaid_invoice_balance_eur,
      round(case when tr.q3_sessions is null or tr.q3_sessions = 0 then null
                 else 100.0 * (tr.q4_sessions - tr.q3_sessions) / tr.q3_sessions end, 1) as traffic_change_pct_q4_vs_q3,
      round(case when r.q3_fee is null or r.q3_fee = 0 then null
                 else 100.0 * (r.q4_fee - r.q3_fee) / r.q3_fee end, 1) as fee_revenue_change_pct_q4_vs_q3,
      (DATE '2026-01-05' - CAST(lo.last_placed_at AS DATE)) as days_since_last_order,
      t.status as tenant_status
    from tenants t
    left join unpaid u on u.tenant_id = t.id
    left join traffic tr on tr.tenant_id = t.id
    left join revenue r on r.tenant_id = t.id
    left join last_order lo on lo.tenant_id = t.id
    where t.deleted_at is null
    order by unpaid_invoice_balance_eur desc nulls last, traffic_change_pct_q4_vs_q3 asc nulls last, merchant_name
    limit 50

    “Query compiles cleanly with the expected columns. Submitting.”

    Submitted · the SQL of the last dry run, unchanged · Submitted to tonight's batch. Your work on this request is done.

    “Submitted. Summary of what I delivered and two deviations worth flagging to Hannah: 1. Vertical: used tenants.industry since there's no dedicated "vertical" column. 2. Currency conversion: unpaid invoice balances use one snapshot EUR rate per currency (latest rate on/before 2025-12-31), exactly as specified. Payout fee revenue, however, is converted per-payout using that payout's own paid_at date rate — more correct for a Q3-vs-Q4 trend than a single snapshot rate. 3. Total at-risk count: the request also asked for a total count of merchants with any non-zero unpaid balance or traffic/revenue decline. That's a different grain (one number, not one row per merchant) and would require scanning beyond the printed top-50, so I left it out of this report and flagged it as a candidate for a quick separate one-row report.”

  5. The report comes back overnight

    As Hannah Brooks saw it, from the small dataset the agents worked on

    merchant_name                 | vertical                  | unpaid_invoice_balance_eur | traffic_change_pct_q4_vs_q3 | fee_revenue_change_pct_q4_vs_q3 | days_since_last_order | tenant_status
    ------------------------------+---------------------------+----------------------------+-----------------------------+---------------------------------+-----------------------+--------------
    Kontor24 Bürobedarf           | Office Supplies (B2B)     |                  116652.86 |                         1.6 |                           147.8 |                     5 | active       
    Ballycorran Industrial Supply | Industrial Supplies (B2B) |                   22415.24 |                       -10.3 |                           171.8 |                    10 | active       
    Castell Fasteners             | Industrial Supplies (B2B) |                   14481.02 |                       -43.2 |                           157.4 |                    17 | active       
    OfficeHub Nordic              | Office Supplies (B2B)     |                   14348.57 |                        13.1 |                           -77.4 |                    18 | active       
    Deskline Supplies             | Office Supplies (B2B)     |                   12749.42 |                        16.7 |                            -1.0 |                    13 | active       
    Midland Safety Wear           | Industrial Supplies (B2B) |                   10049.47 |                        21.2 |                           236.4 |                     5 | active       
    Polder Packaging              | Industrial Supplies (B2B) |                     5296.8 |                       116.7 |                           -50.6 |                    12 | active       
    Hanse Industriebedarf         | Industrial Supplies (B2B) |                     864.14 |                         3.7 |                           -22.9 |                     6 | active       
    Brennquell Kaffee             | Food & Coffee             |                      41.84 |                        32.8 |                            27.0 |                     5 | active       
    Trailmark                     | Sports & Outdoor          |                        0.0 |                       -50.0 |                           203.9 |                    27 | active       
    Pfotenglück                   | Pet Supplies              |                        0.0 |                       -26.3 |                            19.6 |                     8 | active       
    Alpine Loft                   | Sports & Outdoor          |                        0.0 |                       -22.7 |                            NULL |                    24 | active       
    Papeterie Grossiste           | Office Supplies (B2B)     |                        0.0 |                       -21.4 |                           -97.2 |                    26 | active       
    Tiny Atelier                  | Home & Living             |                        0.0 |                        -5.3 |                           286.5 |                    35 | active       
    Fell & Fjell                  | Sports & Outdoor          |                        0.0 |                        -5.1 |                           -78.5 |                     6 | active       
    Driftwood Home                | Home & Living             |                        0.0 |                        -4.2 |                           122.0 |                    14 | active       
    Ostwind Mode                  | Apparel & Footwear        |                        0.0 |                         1.4 |                           137.8 |                     6 | active       
    Nordic Bloom Cosmetics        | Beauty                    |                        0.0 |                         4.2 |                            16.6 |                     6 | active       
    Goldkind                      | Jewelry & Accessories     |                        0.0 |                         5.9 |                           -95.4 |                    20 | active       
    Stoneware Studio              | Home & Living             |                        0.0 |                         6.7 |                            95.2 |                     6 | active       
    Bijoux Lune                   | Jewelry & Accessories     |                        0.0 |                        11.8 |                           -48.2 |                     6 | active       
    Spice Route Box               | Food & Coffee             |                        0.0 |                        12.2 |                            37.6 |                     5 | active       
    Purrfect Pantry               | Pet Supplies              |                        0.0 |                        12.5 |                            40.5 |                    13 | active       
    Velo Noord                    | Sports & Outdoor          |                        0.0 |                        12.5 |                           -80.8 |                     5 | active       
    Ridgeback Gear                | Sports & Outdoor          |                        0.0 |                        12.8 |                            37.5 |                     6 | active       
    Haven Linen                   | Home & Living             |                        0.0 |                        15.0 |                           -41.1 |                     7 | active       
    Feather & Fin                 | Pet Supplies              |                        0.0 |                        16.0 |                            24.3 |                     5 | active       
    Sourdough Club                | Food & Coffee             |                        0.0 |                        16.0 |                            42.1 |                     6 | active       
    Hound Hamper                  | Pet Supplies              |                        0.0 |                        19.2 |                            17.5 |                     8 | active       
    Velvet Ash                    | Apparel & Footwear        |                        0.0 |                        21.9 |                           181.9 |                     5 | active       
    Tail Wag Supply               | Pet Supplies              |                        0.0 |                        22.5 |                            62.6 |                     7 | active       
    Cinder Peak Sports Co.        | Sports & Outdoor          |                        0.0 |                        25.9 |                            40.3 |                     5 | active       
    Folio & Fern                  | Books & Stationery        |                        0.0 |                        27.8 |                            93.5 |                     6 | active       
    Glasshouse Grooming           | Beauty                    |                        0.0 |                        29.4 |                           542.0 |                     7 | active       
    Kayak Corner                  | Sports & Outdoor          |                        0.0 |                        30.0 |                           -12.6 |                     7 | active       
    Inkwell & Quire               | Books & Stationery        |                        0.0 |                        31.6 |                           334.4 |                     5 | active       
    Kibblewick                    | Pet Supplies              |                        0.0 |                        32.9 |                            36.8 |                     5 | active       
    Lumen Collective              | Apparel & Footwear        |                        0.0 |                        33.3 |                           264.0 |                     7 | active       
    Paperplane Press              | Books & Stationery        |                        0.0 |                        35.3 |                           148.2 |                     6 | active       
    Studio Vrij                   | Apparel & Footwear        |                        0.0 |                        39.4 |                           121.5 |                     5 | active       
    Grind Collective              | Food & Coffee             |                        0.0 |                        40.8 |                            38.0 |                     5 | past_due     
    Fern & Fable                  | Home & Living             |                        0.0 |                        41.4 |                            60.7 |                     6 | active       
    Wiesgrund Wohnen              | Home & Living             |                        0.0 |                        46.2 |                           123.3 |                     7 | active       
    Saffron & Sage                | Beauty                    |                        0.0 |                        50.0 |                           125.1 |                     7 | past_due     
    Whisker Works                 | Pet Supplies              |                        0.0 |                        55.2 |                            61.6 |                     5 | active       
    Cragsley Outfitters           | Apparel & Footwear        |                        0.0 |                        55.7 |                           140.5 |                     5 | active       
    Marrowby & Vane               | Apparel & Footwear        |                        0.0 |                        55.9 |                           137.4 |                     5 | active       
    Brewline Coffee               | Food & Coffee             |                        0.0 |                        56.3 |                            96.5 |                    10 | active       
    Honeycomb Larder              | Food & Coffee             |                        0.0 |                        57.4 |                            88.4 |                     5 | active       
    Willow & Wren                 | Home & Living             |                        0.0 |                        57.6 |                            43.7 |                     9 | active       
    (50 rows)
  6. What came of it

    The memo and the queries that followed; links found by title, so inferred

    Later queries that build on this report:

  7. It becomes benchmark query q210

    Changed only where the benchmark needs the same result on every system

    • editDISTINCT ON is a PostgreSQL extension: the first row per key written as ROW_NUMBER() = 1, standard SQL.
    • spellingfetch first
    • determinismtie-breaker: tenant names are not unique
    • determinismtie-breaker: the first row per key of the DISTINCT ON rewrite does not depend on the order of rows with the same timestamp
    • numericsdocs/NUMERICS.md: rounding removed; results keep full precision
    • made explicita selected t.col gets AS col, its standard name made explicit (3×)
    • determinismthe standard leaves the place of NULLs in a sort to the implementation: NULLS LAST ascending, NULLS FIRST descending, as in the reference results (4×)
    • placeholderfetch first
    The benchmark text, against the submitted SQL
    with latest_rates as (  select distinct on (quote_code) quote_code, rate  from exchange_rates  where base_code = 'EUR' and as_of <= DATE '2025-12-31'  order by quote_code, as_of desc  select quote_code, rate  from (    select quote_code, rate, row_number() over (partition by quote_code order by as_of desc NULLS FIRST, id desc NULLS FIRST) as rn    from exchange_rates    where base_code = 'EUR' and as_of <= DATE '2025-12-31'  ) ranked  where rn = 1),unpaid as (  select i.tenant_id,  select i.tenant_id AS tenant_id,    sum(i.amount_due / case when i.currency_code = 'EUR' then 1 else lr.rate end) as unpaid_eur  from invoices i  left join latest_rates lr on lr.quote_code = i.currency_code  where i.status in ('open','past_due')  group by i.tenant_id),traffic as (  select tenant_id,    count(*) filter (where started_at >= TIMESTAMP '2025-07-01' and started_at < TIMESTAMP '2025-10-01') as q3_sessions,    count(*) filter (where started_at >= TIMESTAMP '2025-10-01' and started_at < TIMESTAMP '2026-01-01') as q4_sessions  from web_sessions  group by tenant_id),payout_rates as (  select p.id as payout_id, p.tenant_id, p.paid_at,  select p.id as payout_id, p.tenant_id AS tenant_id, p.paid_at AS paid_at,    p.fee_amount / case when p.currency_code = 'EUR' then 1 else er.rate end as fee_eur  from payouts p  left join exchange_rates er    on er.base_code = 'EUR' and er.quote_code = p.currency_code and er.as_of = CAST(p.paid_at AS DATE)  where p.status = 'paid'),revenue as (  select tenant_id,    sum(fee_eur) filter (where paid_at >= TIMESTAMP '2025-07-01' and paid_at < TIMESTAMP '2025-10-01') as q3_fee,    sum(fee_eur) filter (where paid_at >= TIMESTAMP '2025-10-01' and paid_at < TIMESTAMP '2026-01-01') as q4_fee  from payout_rates  group by tenant_id),last_order as (  select tenant_id, max(placed_at) as last_placed_at  from orders  group by tenant_id)select  t.name as merchant_name,  t.industry as vertical,  round(coalesce(u.unpaid_eur, 0), 2) as unpaid_invoice_balance_eur,  round(case when tr.q3_sessions is null or tr.q3_sessions = 0 then null             else 100.0 * (tr.q4_sessions - tr.q3_sessions) / tr.q3_sessions end, 1) as traffic_change_pct_q4_vs_q3,  round(case when r.q3_fee is null or r.q3_fee = 0 then null             else 100.0 * (r.q4_fee - r.q3_fee) / r.q3_fee end, 1) as fee_revenue_change_pct_q4_vs_q3,  coalesce(u.unpaid_eur, 0) as unpaid_invoice_balance_eur,  case when tr.q3_sessions is null or tr.q3_sessions = 0 then null       else 100.0 * (tr.q4_sessions - tr.q3_sessions) / tr.q3_sessions end as traffic_change_pct_q4_vs_q3,  case when r.q3_fee is null or r.q3_fee = 0 then null       else 100.0 * (r.q4_fee - r.q3_fee) / r.q3_fee end as fee_revenue_change_pct_q4_vs_q3,  (DATE '2026-01-05' - CAST(lo.last_placed_at AS DATE)) as days_since_last_order,  t.status as tenant_statusfrom tenants tleft join unpaid u on u.tenant_id = t.idleft join traffic tr on tr.tenant_id = t.idleft join revenue r on r.tenant_id = t.idleft join last_order lo on lo.tenant_id = t.idwhere t.deleted_at is nullorder by unpaid_invoice_balance_eur desc nulls last, traffic_change_pct_q4_vs_q3 asc nulls last, merchant_namelimit 50order by unpaid_invoice_balance_eur desc nulls last, traffic_change_pct_q4_vs_q3 asc nulls last, merchant_name NULLS LAST, t.id NULLS LAST{{fetch_first(50)}}
    with latest_rates as (
      select quote_code, rate
      from (
        select quote_code, rate, row_number() over (partition by quote_code order by as_of desc NULLS FIRST, id desc NULLS FIRST) as rn
        from exchange_rates
        where base_code = 'EUR' and as_of <= DATE '2025-12-31'
      ) ranked
      where rn = 1
    ),
    unpaid as (
      select i.tenant_id AS tenant_id,
        sum(i.amount_due / case when i.currency_code = 'EUR' then 1 else lr.rate end) as unpaid_eur
      from invoices i
      left join latest_rates lr on lr.quote_code = i.currency_code
      where i.status in ('open','past_due')
      group by i.tenant_id
    ),
    traffic as (
      select tenant_id,
        count(*) filter (where started_at >= TIMESTAMP '2025-07-01' and started_at < TIMESTAMP '2025-10-01') as q3_sessions,
        count(*) filter (where started_at >= TIMESTAMP '2025-10-01' and started_at < TIMESTAMP '2026-01-01') as q4_sessions
      from web_sessions
      group by tenant_id
    ),
    payout_rates as (
      select p.id as payout_id, p.tenant_id AS tenant_id, p.paid_at AS paid_at,
        p.fee_amount / case when p.currency_code = 'EUR' then 1 else er.rate end as fee_eur
      from payouts p
      left join exchange_rates er
        on er.base_code = 'EUR' and er.quote_code = p.currency_code and er.as_of = CAST(p.paid_at AS DATE)
      where p.status = 'paid'
    ),
    revenue as (
      select tenant_id,
        sum(fee_eur) filter (where paid_at >= TIMESTAMP '2025-07-01' and paid_at < TIMESTAMP '2025-10-01') as q3_fee,
        sum(fee_eur) filter (where paid_at >= TIMESTAMP '2025-10-01' and paid_at < TIMESTAMP '2026-01-01') as q4_fee
      from payout_rates
      group by tenant_id
    ),
    last_order as (
      select tenant_id, max(placed_at) as last_placed_at
      from orders
      group by tenant_id
    )
    select
      t.name as merchant_name,
      t.industry as vertical,
      coalesce(u.unpaid_eur, 0) as unpaid_invoice_balance_eur,
      case when tr.q3_sessions is null or tr.q3_sessions = 0 then null
           else 100.0 * (tr.q4_sessions - tr.q3_sessions) / tr.q3_sessions end as traffic_change_pct_q4_vs_q3,
      case when r.q3_fee is null or r.q3_fee = 0 then null
           else 100.0 * (r.q4_fee - r.q3_fee) / r.q3_fee end as fee_revenue_change_pct_q4_vs_q3,
      (DATE '2026-01-05' - CAST(lo.last_placed_at AS DATE)) as days_since_last_order,
      t.status as tenant_status
    from tenants t
    left join unpaid u on u.tenant_id = t.id
    left join traffic tr on tr.tenant_id = t.id
    left join revenue r on r.tenant_id = t.id
    left join last_order lo on lo.tenant_id = t.id
    where t.deleted_at is null
    order by unpaid_invoice_balance_eur desc nulls last, traffic_change_pct_q4_vs_q3 asc nulls last, merchant_name NULLS LAST, t.id NULLS LAST
    FETCH FIRST 50 ROWS ONLY
    10 ms100 ms1 sHyper66.7 msDuckDB24.1 msGizmoSQL27.9 msclickhouse_brew212 msFirebolterrorDataFusion198 msClickHouseerror
    • Firebolt{ "errors": [ { "description": "Line 12, Column 22: Operations between decimals with different precision and scale is not supported at the moment. Explicitly cast one of the decimals to other decimal precision and scale." } ], "query": { "query_id": "4263cb04-0670-496a-8f0f-8696ae7dc14a", "query_lab
    • ClickHouseCode: 69. DB::Exception: Decimal result's scale is less than argument's one: while executing function divide on arguments __table3.amount_due Decimal(14, 4) Decimal64(size = 0), if(equals(__table3.currency_code, 'EUR'_String), 1_UInt8, __table4.rate) Nullable(Decimal(18, 6)) Nullable(size = 0, Decim
  8. Everything, as recorded

    The agents' own records, as the role play wrote them; only the personal data of the person who ran it is redacted

    Omar's whole session, 7 turns
    • The request, as Omar received it
      Report request from Hannah Brooks (analyst for the Chief Executive Officer):
      
      Title: Merchant risk signals snapshot: receivables, traffic and revenue trend
      Question: Which merchants look at risk right now, based on unpaid invoices, traffic decline, and revenue trend — our 'accounts to watch' list?
      Grain: one row per merchant (tenant)
      Columns:
      - merchant name
      - vertical
      - unpaid invoice balance (EUR, open+past_due invoices)
      - web traffic change Q4 vs Q3 2025 (%, session count)
      - fee revenue change Q4 vs Q3 2025 (%)
      - days since last order placed (as of 2026-01-05)
      - tenant status
      Details: Source: invoices (status in 'open','past_due', amount_due summed per tenant, converted to EUR using the most recent available exchange_rates as_of date on or before 2025-12-31) for unpaid balance; web_sessions (started_at in Q3 2025 vs Q4 2025, count per tenant) for traffic change; payouts (status='paid', paid_at in Q3 2025 vs Q4 2025, fee_amount converted to EUR) for revenue change; orders (max placed_at per tenant, any status) for recency; tenants for name/vertical/status (exclude deleted_at not null). Traffic and revenue change = 100*(Q4-Q3)/Q3, null if Q3 value is zero. Order by unpaid invoice balance EUR descending, tie-break by traffic change ascending (most negative first), then merchant name. Print top 50 rows and note total merchant count with any non-zero unpaid balance or traffic/revenue decline.
      
      Check your query with the warehouse's dry_run tool, then hand it in with submit.
    • Omar calls dry_run
      with latest_rates as (
        select distinct on (quote_code) quote_code, rate
        from exchange_rates
        where base_code = 'EUR' and as_of <= DATE '2025-12-31'
        order by quote_code, as_of desc
      ),
      unpaid as (
        select i.tenant_id,
          sum(i.amount_due / case when i.currency_code = 'EUR' then 1 else lr.rate end) as unpaid_eur
        from invoices i
        left join latest_rates lr on lr.quote_code = i.currency_code
        where i.status in ('open','past_due')
        group by i.tenant_id
      ),
      traffic as (
        select tenant_id,
          count(*) filter (where started_at >= TIMESTAMP '2025-07-01' and started_at < TIMESTAMP '2025-10-01') as q3_sessions,
          count(*) filter (where started_at >= TIMESTAMP '2025-10-01' and started_at < TIMESTAMP '2026-01-01') as q4_sessions
        from web_sessions
        group by tenant_id
      ),
      payout_rates as (
        select p.id as payout_id, p.tenant_id, p.paid_at,
          p.fee_amount / case when p.currency_code = 'EUR' then 1 else er.rate end as fee_eur
        from payouts p
        left join exchange_rates er
          on er.base_code = 'EUR' and er.quote_code = p.currency_code and er.as_of = CAST(p.paid_at AS DATE)
        where p.status = 'paid'
      ),
      revenue as (
        select tenant_id,
          sum(fee_eur) filter (where paid_at >= TIMESTAMP '2025-07-01' and paid_at < TIMESTAMP '2025-10-01') as q3_fee,
          sum(fee_eur) filter (where paid_at >= TIMESTAMP '2025-10-01' and paid_at < TIMESTAMP '2026-01-01') as q4_fee
        from payout_rates
        group by tenant_id
      ),
      last_order as (
        select tenant_id, max(placed_at) as last_placed_at
        from orders
        group by tenant_id
      )
      select
        t.name as merchant_name,
        t.industry as vertical,
        round(coalesce(u.unpaid_eur, 0), 2) as unpaid_invoice_balance_eur,
        round(case when tr.q3_sessions is null or tr.q3_sessions = 0 then null
                   else 100.0 * (tr.q4_sessions - tr.q3_sessions) / tr.q3_sessions end, 1) as traffic_change_pct_q4_vs_q3,
        round(case when r.q3_fee is null or r.q3_fee = 0 then null
                   else 100.0 * (r.q4_fee - r.q3_fee) / r.q3_fee end, 1) as fee_revenue_change_pct_q4_vs_q3,
        (DATE '2026-01-05' - CAST(lo.last_placed_at AS DATE)) as days_since_last_order,
        t.status as tenant_status
      from tenants t
      left join unpaid u on u.tenant_id = t.id
      left join traffic tr on tr.tenant_id = t.id
      left join revenue r on r.tenant_id = t.id
      left join last_order lo on lo.tenant_id = t.id
      where t.deleted_at is null
      order by unpaid_invoice_balance_eur desc nulls last, traffic_change_pct_q4_vs_q3 asc nulls last, merchant_name
      limit 50
    • The warehouse answers
      STATUS: COMPILES (dry run on the empty schema, no data read)
      COLUMNS: merchant_name, vertical, unpaid_invoice_balance_eur, traffic_change_pct_q4_vs_q3, fee_revenue_change_pct_q4_vs_q3, days_since_last_order, tenant_status
    • OmarQuery compiles cleanly with the expected columns. Submitting.
    • Omar calls submit
      with latest_rates as (
        select distinct on (quote_code) quote_code, rate
        from exchange_rates
        where base_code = 'EUR' and as_of <= DATE '2025-12-31'
        order by quote_code, as_of desc
      ),
      unpaid as (
        select i.tenant_id,
          sum(i.amount_due / case when i.currency_code = 'EUR' then 1 else lr.rate end) as unpaid_eur
        from invoices i
        left join latest_rates lr on lr.quote_code = i.currency_code
        where i.status in ('open','past_due')
        group by i.tenant_id
      ),
      traffic as (
        select tenant_id,
          count(*) filter (where started_at >= TIMESTAMP '2025-07-01' and started_at < TIMESTAMP '2025-10-01') as q3_sessions,
          count(*) filter (where started_at >= TIMESTAMP '2025-10-01' and started_at < TIMESTAMP '2026-01-01') as q4_sessions
        from web_sessions
        group by tenant_id
      ),
      payout_rates as (
        select p.id as payout_id, p.tenant_id, p.paid_at,
          p.fee_amount / case when p.currency_code = 'EUR' then 1 else er.rate end as fee_eur
        from payouts p
        left join exchange_rates er
          on er.base_code = 'EUR' and er.quote_code = p.currency_code and er.as_of = CAST(p.paid_at AS DATE)
        where p.status = 'paid'
      ),
      revenue as (
        select tenant_id,
          sum(fee_eur) filter (where paid_at >= TIMESTAMP '2025-07-01' and paid_at < TIMESTAMP '2025-10-01') as q3_fee,
          sum(fee_eur) filter (where paid_at >= TIMESTAMP '2025-10-01' and paid_at < TIMESTAMP '2026-01-01') as q4_fee
        from payout_rates
        group by tenant_id
      ),
      last_order as (
        select tenant_id, max(placed_at) as last_placed_at
        from orders
        group by tenant_id
      )
      select
        t.name as merchant_name,
        t.industry as vertical,
        round(coalesce(u.unpaid_eur, 0), 2) as unpaid_invoice_balance_eur,
        round(case when tr.q3_sessions is null or tr.q3_sessions = 0 then null
                   else 100.0 * (tr.q4_sessions - tr.q3_sessions) / tr.q3_sessions end, 1) as traffic_change_pct_q4_vs_q3,
        round(case when r.q3_fee is null or r.q3_fee = 0 then null
                   else 100.0 * (r.q4_fee - r.q3_fee) / r.q3_fee end, 1) as fee_revenue_change_pct_q4_vs_q3,
        (DATE '2026-01-05' - CAST(lo.last_placed_at AS DATE)) as days_since_last_order,
        t.status as tenant_status
      from tenants t
      left join unpaid u on u.tenant_id = t.id
      left join traffic tr on tr.tenant_id = t.id
      left join revenue r on r.tenant_id = t.id
      left join last_order lo on lo.tenant_id = t.id
      where t.deleted_at is null
      order by unpaid_invoice_balance_eur desc nulls last, traffic_change_pct_q4_vs_q3 asc nulls last, merchant_name
      limit 50
    • The warehouse answers
      Submitted to tonight's batch. Your work on this request is done.
    • OmarSubmitted. Summary of what I delivered and two deviations worth flagging to Hannah: 1. **Vertical**: used `tenants.industry` since there's no dedicated "vertical" column. 2. **Currency conversion**: unpaid invoice balances use one snapshot EUR rate per currency (latest rate on/before 2025-12-31), exactly as specified. Payout fee revenue, however, is converted per-payout using that payout's own `paid_at` date rate — more correct for a Q3-vs-Q4 trend than a single snapshot rate. 3. **Total at-risk count**: the request also asked for a total count of merchants with any non-zero unpaid balance or traffic/revenue decline. That's a different grain (one number, not one row per merchant) and would require scanning beyond the printed top-50, so I left it out of this report and flagged it as a candidate for a quick separate one-row report.