{
  "name": "Pitch fee against the day: the sales that cover it, by footfall and capture rate",
  "page": "https://otherworldlabs.com/pitch-fee-worth-it/",
  "endpoint": "https://otherworldlabs.com/pitch-fee-worth-it/api.json",
  "question": "For one quoted event pitch, how many sales cover the day once the flat fee, the organiser's commission on takings, the deposit and extras, stock, crew, travel and card fees are counted, and what does the day leave at a given footfall and capture rate?",
  "answers": "one quoted event at a time; the page keeps no history and this endpoint stores nothing",
  "version": "2026-10-11",
  "licence": "The arithmetic is not ours to license. Reuse the formulas; the page is free and needs no attribution.",
  "method": "static description plus a worked example; the same formulas run in the page in the reader's browser, with nothing sent anywhere",
  "currency": "one of GBP, USD or EUR, chosen by the caller and printed back as a symbol. No conversion is done, no rate is attached and no figure is rescaled: the label changes the symbol and nothing else. Any other label, including a blank one, returns the same figures with no symbol at all. The page's notice for that case has two forms, and which one prints is whether the label is empty once trimmed: a blank or whitespace-only label prints that no currency was chosen, and a label this page prints no symbol for - ZAR, say - prints that the page prints a symbol for GBP, USD and EUR only and the label sent is not one of them, because a caller who sent ZAR did choose a currency. The label itself is never echoed back into the page's markup.",
  "rounding": {
    "counts": "whole. The claimed footfall is rounded to the nearest whole person and the endpoint says both numbers in a notice when a fraction was sent. The sales implied by a footfall and a capture rate are rounded to the NEAREST whole sale (round2's rule at zero decimals, so a half goes toward positive infinity). The count that covers the day is the fewest WHOLE sales whose contribution, taken to the cent, reaches the money owed before the first sale: a part of a sale does not cover anything. It is NOT ceil of the quotient, which differs from that count wherever the division returns a float a hair above a whole number, and would then put the headline count on one side of the contour and the grid's marked cells on the other.",
    "money": "two decimals by round2(x) = Math.round(x * 100) / 100 on the double, which puts a half toward positive infinity rather than away from zero: -0.125 comes out as -0.12 and not as -0.13, and negative money is reachable here wherever the three shares of takings come to 100 per cent or more, or wherever a day is short. round2 is applied at the point each figure is defined and again to every sum and difference built out of figures already rounded, so no line is worked from a longer number than the one shown. A figure the caller sends ABOVE ZERO that this fixing sends to nothing is said out loud in a notice naming both numbers, exactly as a fractional footfall is, for the fields a sentence would otherwise describe as absent: the flat fee, the deposit and extras, the commission, the average spend, travel, and each person's hours and rate. Fixing that merely shortens a figure - a typed 9.375 average spend fixed to 9.38 - gets no notice, because it changes no sentence. Two figures that are each rounded and then added by hand can therefore land a cent away from a third figure rounded on its own; the figure returned is the one computed by the line that defines it, which is written out in `formula` below.",
    "percentages": "a percentage the caller sends is used as given, at two decimals. The one percentage this endpoint DERIVES, breakEvenSharePct, is printed at the fewest decimals, minimum one, that make the percentage reach the count beside it at that count's own precision; the search runs from one decimal to six and the figure returned is that rounded percentage rather than the exact quotient, so a reader who recomputes salesToCover from it gets salesToCover. Where no precision up to six reaches the count, it is returned at six decimals."
  },
  "inputs": [
    {
      "name": "currency",
      "type": "string",
      "unit": "none",
      "required": false,
      "range": "GBP, USD or EUR",
      "note": "The currency the quote is in. The page's own control offers exactly these three and nothing else, so no other value is reachable from it; any other string, blank or whitespace included, is accepted here and returns the same figures with no symbol, which the page says out loud in a notice."
    },
    {
      "name": "flatFee",
      "type": "number",
      "unit": "money",
      "required": false,
      "range": "0 to 5000, step 1",
      "note": "The fixed figure on the organiser's quote for the pitch, whatever is sold. Called a vendor fee, a booth fee or a space fee in the United States. It reaches the arithmetic only through its sum with depositAndExtras, so a single all-in quote may be sent entirely in this field."
    },
    {
      "name": "commissionPct",
      "type": "number",
      "unit": "per cent",
      "required": false,
      "range": "0 to 40, step 0.5",
      "note": "The share of takings the organiser keeps, from the booking terms. This is NOT part of the money owed before the first sale: it comes off each sale instead, which is what makes the divisor move with the fee. Nothing here states what any organiser charges or whether such a term is fair, standard or negotiable."
    },
    {
      "name": "depositAndExtras",
      "type": "number",
      "unit": "money",
      "required": false,
      "range": "0 to 1000, step 1",
      "note": "The deposit on the booking plus the extras billed with the pitch: power, water, waste. Money owed before the first sale, like flatFee, and it reaches the arithmetic only through its sum with flatFee, so where a caller splits one combined quote across the two changes no figure returned."
    },
    {
      "name": "avgSpend",
      "type": "number",
      "unit": "money",
      "required": false,
      "range": "1 to 60, step 0.25",
      "note": "What one customer spends in one transaction, from the trader's own till history. One sale means one transaction, not one item. At zero or below ONCE FIXED at the cent, the entry is incomplete and every figure that depends on it is null: a typed 0.004 is fixed to 0.00 and is an incomplete entry, not a very small sale."
    },
    {
      "name": "stockSharePct",
      "type": "number",
      "unit": "per cent",
      "required": false,
      "range": "10 to 60, step 1",
      "note": "The trader's own food-cost percentage: what the ingredients in a sale cost as a share of what the sale rings up."
    },
    {
      "name": "cardSharePct",
      "type": "number",
      "unit": "per cent",
      "required": false,
      "range": "0 to 5, step 0.1",
      "note": "The card provider's share of takings, from their statement. No figure for any provider is asserted here."
    },
    {
      "name": "travel",
      "type": "number",
      "unit": "money",
      "required": false,
      "range": "0 to 500, step 1",
      "note": "Travel and fuel, there and back. Owed whatever is sold, so it joins the money owed before the first sale."
    },
    {
      "name": "crewCount",
      "type": "integer",
      "unit": "people",
      "required": false,
      "range": "1 to 3",
      "note": "How many people work the stall. The page's own control offers 1, 2 and 3. A value outside the range is clamped into it and a fraction is rounded first, and only the first crewCount entries of crew[] are read, so every sum over the crew is a sum over those people and not over every entry the request carries."
    },
    {
      "name": "crew[].hours",
      "type": "number",
      "unit": "hours",
      "required": false,
      "range": "0 to 60, step 0.5",
      "note": "The hours that person works on the day, set-up and break-down included. It reaches the arithmetic only as the product with that person's rate, so somebody paid a flat day rate may send the hours worked and that day rate divided by those hours."
    },
    {
      "name": "crew[].rate",
      "type": "number",
      "unit": "money",
      "required": false,
      "range": "0 to 40, step 0.25",
      "note": "What that person is paid an hour. A person on no wage is hours with a rate of zero, which costs the day nothing; hours and rate reach the arithmetic only as their product."
    },
    {
      "name": "crewHours",
      "type": "number",
      "unit": "hours",
      "required": false,
      "range": "0 to 60, step 0.5",
      "note": "One person, spelled flat: read ONLY when no crew array is given, and then as that one person's hours. It is the same state the page's own crew control reaches with the select on 1, so this advertises no state the controls cannot produce; it exists because a caller holding one pair of figures, and a row of a kit's events record, cannot be an array of objects."
    },
    {
      "name": "crewRate",
      "type": "number",
      "unit": "money",
      "required": false,
      "range": "0 to 40, step 0.25",
      "note": "The rate for that one person, read only when no crew array is given. With neither crewHours nor crewRate nor crew given, the day carries no wage at all."
    },
    {
      "name": "claimedFootfall",
      "type": "integer",
      "unit": "people",
      "required": false,
      "range": "100 to 200000, step 100",
      "note": "The figure the organiser claims for the event. It is treated as an upper limit on the people who could walk past the pitch and never as attendance served or as customers. At zero or below ONCE ROUNDED to the whole person, the entry is incomplete and every figure that depends on it is null: a typed 0.3 is fixed to 0 and is an incomplete entry, not an event with a third of a person at it. Nothing here says whether an organiser's figure is honest."
    },
    {
      "name": "ownCapturePct",
      "type": "number",
      "unit": "per cent",
      "required": false,
      "range": "0.05 to 10, step 0.05",
      "note": "The share of that footfall who buy from the trader. The figure a quote cannot supply and a record of past events can. No typical capture rate for the trade is stated or implied anywhere in this endpoint; the arithmetic is returned at whatever rate is sent."
    }
  ],
  "outputs": [
    {
      "name": "feeOwed",
      "unit": "money",
      "formula": "round2(flatFee + depositAndExtras); the organiser's fixed money, owed whatever is sold"
    },
    {
      "name": "staffCost",
      "unit": "money",
      "formula": "the running sum, rounded at each step, of round2(crew[i].hours x crew[i].rate) over the first crewCount people"
    },
    {
      "name": "fixedOutlay",
      "unit": "money",
      "formula": "round2(feeOwed + staffCost + travel); the money owed before the first sale"
    },
    {
      "name": "stockPerSale",
      "unit": "money",
      "formula": "round2(avgSpend x stockSharePct / 100)"
    },
    {
      "name": "organiserPerSale",
      "unit": "money",
      "formula": "round2(avgSpend x commissionPct / 100)"
    },
    {
      "name": "cardPerSale",
      "unit": "money",
      "formula": "round2(avgSpend x cardSharePct / 100)"
    },
    {
      "name": "sharesPct",
      "unit": "per cent",
      "formula": "round(stockSharePct + commissionPct + cardSharePct, 2); the three shares of takings added together. It may exceed 100, and when it does nothing is left from a sale"
    },
    {
      "name": "deductionsPerSale",
      "unit": "money",
      "formula": "round2(avgSpend x sharesPct / 100); the three shares taken out of one sale in ONE rounding, which is the figure the contribution is defined from. Adding stockPerSale, organiserPerSale and cardPerSale by hand can land a cent away from this, because each of those three is rounded on its own"
    },
    {
      "name": "contributionPerSale",
      "unit": "money",
      "formula": "round2(avgSpend - deductionsPerSale); what one sale leaves to put against the money owed before the first sale. Zero or below whenever the three shares take the whole of a sale or more, and returned as it falls out"
    },
    {
      "name": "salesExact",
      "unit": "sales",
      "formula": "round2(fixedOutlay / contributionPerSale); null when the entry is incomplete or when contributionPerSale is zero or below"
    },
    {
      "name": "salesToCover",
      "unit": "sales",
      "formula": "the smallest whole n for which round2(n x contributionPerSale) is at or above fixedOutlay, which is the same test the grid uses to mark a cell; null when the entry is incomplete or when contributionPerSale is zero or below, and never returned as a negative, an infinity or a zero standing in for one of those. It is zero where nothing at all is owed before the first sale, which a percentage-only pitch with no travel and no wage reaches"
    },
    {
      "name": "breakEvenSharePct",
      "unit": "per cent",
      "formula": "salesToCover / claimedFootfall x 100, printed at the fewest decimals that reach salesToCover; null when salesToCover is null, when claimedFootfall is zero, or when salesToCover exceeds claimedFootfall, which is a day needing more sales than the organiser claims people"
    },
    {
      "name": "ownCellSales",
      "unit": "sales",
      "formula": "round(claimedFootfall x ownCapturePct / 100, 0); the sales the claimed footfall comes to at the trader's own capture rate. null when the entry is incomplete"
    },
    {
      "name": "ownCellTakings",
      "unit": "money",
      "formula": "round2(ownCellSales x avgSpend); null when the entry is incomplete"
    },
    {
      "name": "ownCellCosts",
      "unit": "money",
      "formula": "round2(ownCellSales x deductionsPerSale); the stock, the organiser's share and the card fee on those sales. null when the entry is incomplete"
    },
    {
      "name": "ownCellLeft",
      "unit": "money",
      "formula": "round2(round2(ownCellSales x contributionPerSale) - fixedOutlay); what the day leaves at the claimed footfall and the trader's own rate. It is worked from what ONE sale leaves rather than from the takings less the costs so that this figure and salesToCover are one arithmetic: left at or above zero is exactly round2(sales x contributionPerSale) at or above fixedOutlay, which is the test salesToCover is found by. The two routes cannot in fact disagree here - a whole sales count times a two-decimal figure is exact to the cent, so round2(sales x avgSpend) - round2(sales x deductionsPerSale) is round2(sales x contributionPerSale), checked over every cell of 20,000 states with 0 cells apart. An earlier version of this line said the routes could land a cent apart; the route that can is adding the three per-sale share lines by hand instead of taking deductionsPerSale in one rounding. Negative on a day that does not cover itself, and returned negative. null when the entry is incomplete. HOW THE PAGE PRINTS IT, which this endpoint does not do and an agent reproducing the page needs: never with a minus sign. In all three places the page prints this quantity - the fourth figure at the top of the page, the grid cell it belongs to, and the working row in section D - it is printed as a magnitude with the direction in the word beside it, a figure left or a figure short, and a day that breaks even exactly prints nothing left rather than a third form of its own. S12 round 3, B1 and m1: the working row went through the money formatter until that round, which printed a minus in the one place the page's own glossary promised one never appears, and exactly nothing printed as the bare word \"square\", which was defined nowhere on the page. The sign of the number this endpoint returns is the direction that word carries"
    },
    {
      "name": "clearCells",
      "unit": "cells",
      "formula": "how many of the grid's 24 cells are MARKED, which is how many have a sales count at or above salesToCover. Where salesToCover is null - a sale leaves nothing, so no number of sales covers the day - it is 0, and not a count of the cells whose figure happens to be at or above zero: a day that owes nothing before the first sale and leaves nothing per sale has zero-sales cells at exactly zero, and marking those would draw a contour saying the fewer people walk past the better the day goes. Where salesToCover is a count, a marked cell is the same thing as a cell with what the day leaves at or above zero. null when the entry is incomplete and so there is no grid"
    },
    {
      "name": "gridSalesTotal",
      "unit": "sales",
      "formula": "the sum of the sales counts in all 24 cells of the grid. A checksum on the grid rather than a figure about a day: it is the only output that is a total across footfalls and capture rates, and no page sentence prints it. null when the entry is incomplete"
    }
  ],
  "formula": [
    "an entry is COMPLETE when avgSpend is above zero AND claimedFootfall is above zero AND at least one of flatFee, depositAndExtras and commissionPct is above zero - each of them AS FIXED by the line below, never as the caller typed it, because every other line here is worked from the fixed figure and a rule tested on the raw one would call an entry complete and then answer about a figure it had itself reduced to zero. A typed 0.004 average spend, a typed 0.3 footfall and a fee limb under half a cent are therefore all INCOMPLETE, and the page says which figure was fixed away in a notice. Until the entry is complete, salesExact, salesToCover, breakEvenSharePct, the four ownCell figures, clearCells and gridSalesTotal are all null, and the page prints no number in any of its four headline rows",
    "every input is first clamped at zero from below and fixed where it is defined - money at two decimals, a percentage at two, AN HOURS FIGURE AT TWO DECIMALS, the claimed footfall at a whole person - and FIRST means before everything, the completeness test in the line above included. The hours limb is named here because this enumeration reads as closed and left it out: round 7's reviewer wrote a reimplementation from this line, clamped the hours without fixing them, and came out 16 pence under the page's staffCost at 0.006 hours against a rate of 40 - a gap no family of the shared sweep can produce, because the generator snaps the hours to their declared step of 0.5. `rounding.money` above has always said it (it names \"each person's hours and rate\" among the fields whose fixing to nothing is said out loud in a notice, which only makes sense if the two-decimal fixing applies to them), so the rule was recoverable from the file and this line was the incomplete one. The declared state in gates.json labelled \"an hours figure typed finer than two decimals\" now puts the gap inside the sweep, so the next reimplementation finds it where the comparison runs rather than by hand",
    "feeOwed = round2(flatFee + depositAndExtras)",
    "for each of the first crewCount people: cost = round2(hours x rate); staffCost is those costs added in order, rounded again at each addition",
    "fixedOutlay = round2(feeOwed + staffCost + travel)",
    "sharesPct = round(stockSharePct + commissionPct + cardSharePct, 2)",
    "deductionsPerSale = round2(avgSpend x sharesPct / 100)",
    "contributionPerSale = round2(avgSpend - deductionsPerSale)",
    "stockPerSale, organiserPerSale and cardPerSale are each round2(avgSpend x that share / 100), reported for the reader and used in no other figure",
    "if contributionPerSale is at or below zero there is no sales count: salesExact and salesToCover are null and a sentence naming the three shares is returned in their place",
    "otherwise salesExact = round2(fixedOutlay / contributionPerSale) and salesToCover = the smallest whole n for which round2(n x contributionPerSale) is at or above fixedOutlay",
    "breakEvenSharePct = the fewest-decimals percentage p, at least one decimal, for which round(claimedFootfall x p / 100, 0) equals salesToCover; null when salesToCover exceeds claimedFootfall",
    "the grid: rows are claimedFootfall at 100, 75, 50 and 25 per cent of itself, each row's footfall being round(claimedFootfall x share / 100, 0); columns are the capture rates 0.5, 1, 2, 3 and 5 per cent plus ownCapturePct, sorted ascending, the trader's own rate always keeping a column of its own even when it equals one of the five, so the grid is always 4 rows by 6 columns and 24 cells",
    "for each cell: sales = round(rowFootfall x capturePct / 100, 0); takings = round2(sales x avgSpend); costs = round2(sales x deductionsPerSale); left = round2(round2(sales x contributionPerSale) - fixedOutlay). The cell is MARKED when salesToCover is not null and sales is at or above salesToCover, which is the contour and not a test on the cell's own figure: where salesToCover is null no cell is marked, however large left is. Where salesToCover is a count, marked and left-at-or-above-zero are the same set of cells. left is worked from contributionPerSale rather than from takings less costs because that is the route salesToCover is defined on, so the headline count and the marked cells are one answer by construction. The figure this endpoint returns is unchanged by how the page SHOWS the arithmetic for its one named cell: where what one sale leaves is a figure the page prints, section D's working shows the cell as that figure times the sales count less the money owed; where the page withholds it - the three shares taking the whole of a sale or more - the same cell is shown from the takings and the costs lines printed above it, because a page that refuses a figure in one section and prints it as a bare term two rows later has printed it (S12 round 3, m4). The two routes are the same to the cent, for the reason given two sentences up, and the line runs in the direction the printed magnitude reads",
    "ownCellSales, ownCellTakings, ownCellCosts and ownCellLeft are that cell of the top row, at 100 per cent of the claimed footfall, whose capture rate is the trader's own"
  ],
  "identities": [
    "fixedOutlay equals feeOwed + staffCost + travel, every term rounded as returned",
    "contributionPerSale equals avgSpend - deductionsPerSale, both as returned",
    "round2(salesToCover x contributionPerSale) is at or above fixedOutlay, and round2((salesToCover - 1) x contributionPerSale) is below it, wherever salesToCover is not null and above zero",
    "round(claimedFootfall x breakEvenSharePct / 100, 0) equals salesToCover, wherever breakEvenSharePct is not null",
    "a cell of the grid is marked exactly when salesToCover is not null and the cell's sales count is at or above salesToCover, with no cell on either side of that line, and clearCells is the count of those cells; wherever salesToCover is a count, a marked cell is also exactly a cell with left at or above zero, so the page's headline count and the marked cells of its grid are the same answer. Wherever salesToCover is null, clearCells is 0 and no cell is marked, including the cells of a day that owes nothing before the first sale, whose zero-sales cells have left exactly zero. The page harness asserts both limbs at every swept state",
    "sending one combined quote wholly as flatFee, or split in any way across flatFee and depositAndExtras, returns identical values for every output, provided each split is already a whole number of cents; a split that is not is fixed to the cent before it is added, the same as any other money field, and can then land a cent away from the figure the whole quote gives",
    "replacing one person's hours and rate with any other pair having the same product returns identical values for every output, provided each replacement pair is already at the precision its own field is fixed to (two decimals for hours, the cent for the rate); a pair that is not is fixed first, the same as any other hours or money field, and the product can then differ from the one the original pair gives",
    "sending one person as crewHours and crewRate, with no crew array and no crewCount, returns identical values to sending crewCount 1 and crew [{hours, rate}] with the same two figures"
  ],
  "example": {
    "note": "the quote the page ships filled in, so this endpoint and the page answer with the same figures",
    "inputs": {
      "currency": "GBP",
      "flatFee": 350,
      "commissionPct": 10,
      "depositAndExtras": 60,
      "avgSpend": 9.5,
      "stockSharePct": 32,
      "cardSharePct": 1.8,
      "travel": 45,
      "crewCount": 2,
      "crew": [
        {
          "hours": 9,
          "rate": 0
        },
        {
          "hours": 9,
          "rate": 11.5
        },
        {
          "hours": 0,
          "rate": 0
        }
      ],
      "claimedFootfall": 8000,
      "ownCapturePct": 1.5
    },
    "outputs": {
      "feeOwed": 410,
      "staffCost": 103.5,
      "fixedOutlay": 558.5,
      "stockPerSale": 3.04,
      "organiserPerSale": 0.95,
      "cardPerSale": 0.17,
      "sharesPct": 43.8,
      "deductionsPerSale": 4.16,
      "contributionPerSale": 5.34,
      "salesExact": 104.59,
      "salesToCover": 105,
      "breakEvenSharePct": 1.31,
      "ownCellSales": 120,
      "ownCellTakings": 1140,
      "ownCellCosts": 499.2,
      "ownCellLeft": 82.3,
      "clearCells": 9,
      "gridSalesTotal": 2600
    }
  },
  "not_covered": [
    "how many of an organiser's claimed footfall will actually buy, which is the one figure a quote cannot supply; the grid is the day at every rate the caller names, and none of those rates is picked out",
    "whether an organiser's footfall figure is honest, or measured, or a sales figure",
    "whether a deposit is refundable, whether a minimum guarantee applies, and anything else about the terms of a booking contract",
    "VAT, tax, and whether any of these costs is deductible",
    "street-trading licences, food-business registration, hygiene ratings, insurance, alcohol licences and gas safety",
    "the weather, a pitch's position on the site, a queue, a sell-out, and anything else that moves a real day",
    "any comparison between organisers, events, apps or pitches",
    "a second day: this is one day's arithmetic, and a two-day event with one set-up is not modelled here",
    "the PAGE withholds every figure it works out in one further state that this endpoint does not have: the page ships every field filled with the worked example below, and while some of its three input blocks still hold those shipped figures and others do not, the four headline rows print what they are waiting for instead of a figure, the working in each section prints its label and the reason in place of its figures, the grid prints no cell at all, and the page says which blocks are still the example - above those figures and again beside them. Until S12 round 5 the rule covered the four headline rows ALONE, and the page then printed three of the same quantities again under its own labels in sections B, C and D and printed all 24 grid cells, some of them marked as covering a day worked from the reader's fee and a stranger's costs. It is a rule about whose figures are on the page, not about the arithmetic: this endpoint computes every figure from whatever state it is sent, and a caller sending the example's own figures gets the example's own answers. Every output named above is computed in that state too and returned unchanged, which is why nothing in this file's outputs, rounding or identities moves with this rule, and why the page carries the decision on two fields of its own answer (heldByExample, exampleHold) rather than by emptying a figure"
  ],
  "no_advice": "This endpoint returns arithmetic on figures supplied to it. It recommends no event, no pitch, no price, no capture rate and no decision, and it returns no forecast of what a day will take."
}
