{
  "name": "tailwind.reviews market benchmark",
  "version": 1,
  "summary": "Give-to-get benchmark for home-services businesses in Poland. The owner's browser turns last month's numbers into rounded ratios; only those are sent. Only verified businesses (one answer per business per month) count, and market quartiles are shown once at least 10 of them answered.",
  "privacy": {
    "rawNumbersLeaveBrowser": false,
    "companyIdentitySent": false,
    "cookiesSent": false,
    "verification": "Signed-in owner proves through Google Business that one of their locations is the company's Google Maps place. That earns one ticket per business per month (POST /v1.0/benchmark/ticket). The server derives the category and city from that listing's Google primary category and address and returns them with the ticket; the category and city in the signal must be those (a business outside the 10 categories gets 403 category_not_supported). The business is recorded as having taken part (keyed HMAC of its Google place id), but not what it answered.",
    "ticket": "Random 256-bit value. The server stores only its SHA-256 and the month, deletes it when the signal arrives, and stores the signal without the ticket, without a participant id and without a timestamp.",
    "notVerifiableFromOutside": "That the server keeps no log tying the moment a ticket was issued to the moment its signal arrived, and does not log the IP address of the signal request. The stored data has no such link, but a server operator could in principle correlate request times; see timing.",
    "minCompaniesPerCell": 10,
    "published": "Public: the median month-on-month change in orders per cell, and how many businesses each metric rests on (counts below 10 are published as null). Participants (a verified business that sent a signal for that month or the previous one): median, p25 and p75 of all seven metrics. Never single values, minimum or maximum. New signals appear within about 24 hours.",
    "excluded": "Prices, order values and anything price-like are not collected (competition-law reasons).",
    "timing": "The ticket request carries the session cookie; the signal does not. The web form waits a random 5-25 s (crypto.getRandomValues) between the two, and accepted signals enter the published statistics in a daily release in shuffled order, not instantly. This makes linking a signal to the account that took the ticket by timing harder, not impossible."
  },
  "endpoint": {
    "method": "POST",
    "url": "https://api.tailwind.reviews/v1.0/public/benchmark/signals",
    "credentials": "omit"
  },
  "signal": {
    "v": "1",
    "ticket": "one-time ticket from the ticket endpoint",
    "period": "YYYY-MM, a finished month",
    "category": "sprzatanie | przeprowadzki | mycie-okien | pranie-tapicerki | zlota-raczka | hydraulik | elektryk | remonty | dezynsekcja | ogrod (as returned with the ticket)",
    "city": "warszawa | krakow | wroclaw | lodz | poznan | trojmiasto | slask | szczecin | bydgoszcz | lublin | bialystok | rzeszow | inne (as returned with the ticket)",
    "metrics": {
      "growthMoM": "integer %, orders this month vs previous month, or null",
      "growthYoY": "integer %, orders this month vs same month last year, or null",
      "conversion": "integer % (0-100), orders from inquiries / inquiries, or null",
      "repeat90": "integer % (0-100), customers from 3 months ago who ordered again, or null",
      "cancellation": "integer % (0-100), cancelled / booked, or null",
      "cac": "integer PLN rounded to 5, marketing spend / new customers, or null",
      "hourlyPay": "integer PLN, gross hourly pay to the contractor (not the client price), or null"
    },
    "accepted": "growthMoM must not be null and at least 3 metrics in total must be non-null; otherwise the server answers 400."
  },
  "files": {
    "calculator": {
      "url": "/benchmark/calculator.v1.js",
      "role": "Runs in the browser; the only code that computes and sends the signal. Loaded with Subresource Integrity.",
      "sha256": "392a6fff21ba8bca2d1c35ad41b9e88f4cd2a104775d9d4139d8e6a6ead013c1",
      "sri": "sha384-tuycRIuDZZl+xyQ1WmSTZ/BbzUX8C6UV4d29Ub1PjfUmZB0Z9/uG8goPnoIgHs2c"
    },
    "aggregate": {
      "url": "/benchmark/aggregate.v1.js",
      "role": "Reference copy of the server-side aggregation rules.",
      "sha256": "1261044ce9d1e6483fd9d9057ff570255c09e802bb214dda7854835f178ff647"
    }
  },
  "howToVerify": [
    "Download /benchmark/calculator.v1.js and check its SHA-256 equals files.calculator.sha256.",
    "Read it: computeMetrics() returns only rounded ratios; buildSignal() adds only the ticket, period, category and city; sendSignal() posts that object with credentials 'omit'.",
    "Open the form page and inspect its <script> tag: src is /benchmark/calculator.v1.js and integrity equals files.calculator.sri, so the browser will not run a modified file.",
    "Submit the form with the network tab open: one POST to /v1.0/benchmark/ticket (signed in, body is only the month), then, after a random 5-25 s, one POST to /v1.0/public/benchmark/signals without cookies whose body matches the preview shown next to the form, with the category and city returned by the ticket endpoint.",
    "Read /benchmark/aggregate.v1.js for the k=10 threshold, the fallback order and the rule that withholds a market a wider one could be differenced against."
  ],
  "ticketEndpoint": {
    "method": "POST",
    "url": "https://api.tailwind.reviews/v1.0/benchmark/ticket",
    "credentials": "include (signed-in owner)",
    "body": {
      "period": "YYYY-MM"
    },
    "response": {
      "ticket": "one-time ticket",
      "period": "YYYY-MM",
      "category": "derived from the Google Business primary category",
      "city": "derived from the Google Business address"
    },
    "refusals": {
      "403 no_verified_business": "no Google-verified business on the account",
      "403 category_not_supported": "the Google primary category is not one of the 10 home-services categories",
      "409 already_participated": "the business already sent a signal for this month"
    }
  },
  "desktopAgent": {
    "source": "https://github.com/ivanshyla/tailwind-benchmark-agent",
    "releases": "https://github.com/ivanshyla/tailwind-benchmark-agent/releases",
    "role": "Reads invoices or payments on the owner's computer, runs a byte-for-byte copy of calculator.v1.js (sha256 pinned and checked against files.calculator.sha256), and sends the same signal. Network access is limited to the chosen data source and tailwind.reviews and is logged in the app.",
    "pairing": {
      "start": "POST https://api.tailwind.reviews/v1.0/public/benchmark/pairings",
      "approve": "https://tailwind.reviews/pl/benchmark/polacz (signed-in owner types the code the app shows; the page shows when the app asked and approves after the same Google Business check). The link never carries the code.",
      "poll": "GET https://api.tailwind.reviews/v1.0/public/benchmark/pairings/{id} with x-pairing-secret"
    }
  }
}
