Echo/Docs

CLI command

pricing

Pricing protection, localisation, tests and research. pricing abuse answers whether a user may start a free trial, refusing one whose linked accounts already had a trial and naming it; pricing codes answers whether a user may redeem a promo code and which Stripe promotion code their checkout applies, issuing a code meant once per customer as a personal code redeemable once and refusing a user whose linked account already redeemed or holds it; pricing countries charges each country its own price from a declared purchasing-power table; pricing plans tests plan structures on conversion and revenue; pricing wtp reads willingness to pay from Van Westendorp and Gabor-Granger surveys.

Invocation#

Shell
echo-cli pricing abuse list --days <days> [--blocked]
echo-cli pricing abuse check --user <user-id> --by <name>
echo-cli pricing codes list --days <days> [--blocked]
echo-cli pricing codes check --code <code> --user <user-id> [--customer <stripe-customer-id>] --by <name>
echo-cli pricing countries list --price <stripe-price-id>
echo-cli pricing countries set --price <stripe-price-id> --country <ISO code> --currency <code> --amount <minor-units> --source <https-url> --by <name>
echo-cli pricing countries remove --price <stripe-price-id> --country <ISO code>
echo-cli pricing plans list --product <id>
echo-cli pricing plans start --product <id> --name <text> --app <web source> --currency <code> --file <arms.json> --by <name>
echo-cli pricing plans results --test <test-id> [--sigmas <standard-errors>]
echo-cli pricing plans stop --test <test-id>
echo-cli pricing wtp list --product <id>
echo-cli pricing wtp create --product <id> --name <text> --method van_westendorp|gabor_granger --currency <code> [--ladder <price,...>] [--purchase-scale <label=probability,...>] --min-answers <n> --by <name>
echo-cli pricing wtp read --survey <survey-id> [--segment <name> | --profile <icp-profile> --app <source>]

Inputs and output#

abuse list writes the trial checks of the last --days days, newest first, or with --blocked only the blocked ones: id, user_id, eligible, earlier_trial (subscription_id, product_id, reported_at and shared_identifier, or null when eligible), checked_by and checked_at. abuse check writes { eligible: true, check } for an eligible user. codes list writes the promo code checks the same way: id, promo_code_id, user_id, eligible, personal_promotion_code and customer (the personal Stripe promotion code the check issued and the customer it is bound to, or null), blocked_by ({ kind: redemption or personal_code, reference: the Stripe checkout session or personal promotion code, at, shared_identifier: the linked identifier it belongs to }, or null when eligible), checked_by and checked_at; codes check writes { eligible: true, check, promotion_code }, promotion_code being the Stripe promotion code the checkout applies. countries list writes the price's country prices by country: base_price_id, country, currency, unit_amount (minor units), source_url, stripe_price_id, set_by and set_at; set and remove write the country price they stored or removed. plans list writes the product's tests, newest first (id, product_id, name, app_id, currency, arms, created_by, created_at, stopped_at); start and stop write the test; results writes test, currency, sigmas (the --sigmas given, or null) and arms, each with key, assigned, converted, conversion (null when no one was assigned), revenue, revenue_per_user, and conversion_interval and revenue_per_user_interval: { low, high }, the value ± --sigmas standard errors of it — a proportion's √(p(1−p)/n) for conversion, the users' net revenue sample standard deviation over √n for revenue per user — or null without --sigmas or for an arm of fewer than two users, which give no spread. The number of standard errors is the caller's; none is assumed. wtp list writes the product's surveys, newest first (id, product_id, name, method, currency, ladder, purchase_scale — each label with the purchase probability it stands for, or null — min_answers, created_by, created_at), and create the survey; read writes survey_id, method, currency, segment, answers (how many it used) and reading: for van_westendorp used, inconsistent, range ({ low, high } or null), indifference_price, optimal_price and curves (per price, the share finding it too cheap, cheap, expensive and too expensive), and for a survey with a purchase scale purchase_intent: used, trial (per price, trial — the mean purchase probability — and revenue, the price times trial), trial_price and revenue_price (where each peaks, or null when nobody would buy); for gabor_granger used, demand (per price, buyers, share and revenue_index) and revenue_price.

State effect#

The abuse verbs call Echo Web's authenticated /api/analytics/trials, the countries verbs /api/analytics/prices, the plans verbs /api/analytics/plans and the wtp verbs /api/analytics/wtp, with ECHO_INTERNAL_BEARER at ECHO_WEB_BASE_URL. Products ask for their signed-in user at GET /api/public/trials before offering a trial. The Trial abuse, Country prices, Plan tests and Willingness to pay sections of the tracking screen and of Echo Desktop's GEO screen do the same, and echo-mcp lists trial checks (echo_list_trial_checks), country prices (echo_list_country_prices), plan tests (echo_list_plan_tests) and pricing surveys (echo_list_wtp_surveys) and reads plan results (echo_plan_test_results) and survey readings (echo_read_wtp). A trial check resolves every identifier the identity edges link to the user (see tracking identity show) and looks for a stored RevenueCat event whose period type is TRIAL naming any of them as app user, original app user or alias; the earliest such trial blocks. Every check, eligible or blocked, is recorded with who asked. countries set reads the base price from Stripe and creates a price on the same product with the same billing period in --currency and --amount, its metadata naming the base price, the country and the source; a country set again gets a new Stripe price and the one it replaces is archived, and remove archives the country's price so the country is charged the base price. A product's checkout asks GET /api/public/prices?price_id=<base price>&country=<ISO code> which Stripe price to charge, or leaves country out to use the country Vercel's edge read from the visitor's address (x-vercel-ip-country); it answers price_id, country, localized (false when the table has no price for the country, and price_id is then the base price) and source_url. The plans --file holds the arms, [{ "key": "…", "weight": <whole number>, "plans": [{ "name": "…", "stripe_price_id": "price_…" }] }, …], at least two. A product's pricing page asks GET /api/public/plans?test_id=<id> for its signed-in user and gets the arm they see, drawn by weight the first time and the same every time after; its checkout sells that arm's Stripe price with the user's id as userId in the Stripe metadata. results reads every assignment and the Stripe payments the Stripe webhook recorded as events of the test's web source in its currency since it started: a payment counts for the arm of the user it names, after their assignment; an arm's conversion is the share of its users with a positive payment, and its revenue what they paid less what was refunded. A stopped test assigns no one new and keeps counting its users' payments. A signed-in user answers a pricing survey once with POST /api/public/wtp { survey_id, answers, segment? }: for van_westendorp the four prices { too_cheap, cheap, expensive, too_expensive }, with buy_at_cheap and buy_at_expensive — each a label of the survey's purchase scale, how likely they are to buy at their cheap and at their expensive price — when the survey declares one; for gabor_granger [{ price, buy }] at every rung of the ladder. --purchase-scale declares the labels and the probability each stands for, label=probability pairs from 0 through 1, for a Van Westendorp survey only; the probabilities are the operator's, none is assumed. wtp read uses the stored answers of all segments or of --segment. Van Westendorp leaves out answers not in rising order and counts them as inconsistent; at every price anyone named it reads the share finding it too cheap (too-cheap price at or above it), cheap, expensive (expensive price at or below it) and too expensive, and takes the lowest price where each pair of curves meets, as van Westendorp's original paper defines them: the acceptable range runs from too cheap meeting not cheap to too expensive meeting not expensive, the indifference price is where cheap meets expensive and the optimal price where too cheap meets too expensive; a pair that never meets is null. With a purchase scale, Newton, Miller and Smith's extension gives each consistent respondent the declared probability of their label at their cheap and at their expensive price, none at their too-cheap and too-expensive prices or beyond, and a straight line between those four points; trial at each price is the mean over them and revenue the price times trial. Gabor-Granger reports, per rung, how many would buy, their share and the price times that share, and the rung where that index is highest; it gives no range. A survey with fewer answers than its min_answers, for the segment read, is refused.

Refusals#

  • The codes verbs call Echo Web's authenticated /api/analytics/promos/checks; products ask for their signed-in user at GET /api/public/promos?code=<code>&customer=<their Stripe customer id> before creating the checkout session and apply the promotion_code it answers to it, and the Promo code abuse section of the tracking screen and of Echo Desktop's GEO screen, and echo-mcp (echo_list_promo_checks, echo_promo_check), do the same. A code not created with --once-per-customer true answers its shared promotion code without a lookup. For one that is, Echo Web resolves every identifier the identity edges link to the user and reads the personal promotion codes already issued for the code to any of them: one that a Stripe checkout redeemed (the Stripe webhook records each completed checkout's promotion codes on its event) blocks with a conflict, "the code <code> is meant once per customer and was already redeemed in Stripe checkout <session> at <time> by an account linked to this one through <identifier>"; one held by another linked account blocks with "the code <code> is meant once per customer and an account linked to this one, <identifier>, already holds its personal promotion code <id> since <time>"; the user's own is answered again; otherwise Stripe issues a personal promotion code of the code's coupon, bound to --customer, redeemable once and expiring with the code. A person holds at most one personal code per promo code, so two checks answered at once issue one, and the second code Stripe created is turned off. Every check is recorded with who asked. Echo Web answers a bad request with "code is required: the promo code the user would redeem", "user_id is required: the user who would redeem the code", "customer is required: the Stripe customer the personal promotion code of <code> is bound to, since it is meant once per customer", "days is required: the window of checks to read" or "blocked takes only true, to read only the blocked checks; got <value>", not found with "no promo code <code> was created through Echo" or "the code <code> was turned off by <name> at <time>, so nobody may redeem it", and fails naming the cause with "Stripe could not issue a personal promotion code of <code> for customer <customer>: <Stripe's message>" or when a recorded redemption names no checkout session; the signed-in endpoint answers unauthorized with "sign in first: a promo code is checked for the signed-in user". Echo does not check that --customer belongs to the user; the product passes its own Stripe customer of that user.
  • A wrong invocation is refused before anything is sent: "pricing takes abuse, codes, countries, plans, wtp; got <word>", "pricing <area> takes <every verb's usage, separated by semicolons>; got <word>", "pricing <area> <verb> does not take <flag>; it takes …", "<flag> needs a value", "pricing <area> <verb> needs <flag>", "--days must be a whole number of days; got <value>", "--amount must be a whole number of minor units; got <value>", "--min-answers must be a whole number; got <value>", "--ladder holds <rung>, not a price", "--purchase-scale holds <pair>, not <label>=<probability>", "--file <path> is not JSON: <parser's reason>" and "--blocked narrows pricing <area> list only"; a --file that cannot be read ends with the system's error naming the path.
  • A blocked user is refused with a conflict naming the earlier trial: "a trial was already started as subscription <id>, reported at <time>, by an account linked to this one through <identifier>". Echo Web answers a bad request with "user_id is required: the user who would start the trial", "days is required: the window of checks to read", "days must be a positive whole number: the window to read", "blocked takes only true, to read only the blocked checks; got <value>" or "checked_by is required: an internal caller has no signed-in user, so it names who is acting"; the signed-in endpoint answers unauthorized with "sign in first: a trial is checked for the signed-in user". The command prints Echo Web's sentence and exits non-zero.
  • A country price is refused with a bad request: "base_price_id is required: …", "country must be an ISO 3166-1 alpha-2 code such as DE or BR; got <value>", "currency must be an ISO 4217 code such as eur or brl; got <value>", "unit_amount must be a whole number of the currency's minor units, at least 1; got <value>", "source_url is required: the https address of the purchasing-power table this price follows; got <value>" or "set_by is required: an internal caller has no signed-in user, so it names who is acting"; not found with "Stripe has no price <id> this account can read: <Stripe's reason>" or "<price> has no price for <country>; echo-cli pricing countries list --price <price> lists them". A price Stripe refuses fails with "Stripe refused the <country> price of <price>: <Stripe's reason>". /api/public/prices refuses a request with neither a country nor Vercel's header: "country is required: send ?country=<ISO code>, or call through Vercel's edge, which sends x-vercel-ip-country".
  • A plan test is refused with a bad request: "product_id is required: …", "name is required: …", "app_id is required: …", "currency must be an ISO 4217 code such as USD or EUR; got <value>", "arms is required: at least two { key, plans, weight }, each a structure of plans to compare", "arms[<n>] must be { key, plans, weight }", "arms[<n>].weight must be a whole number of at least 1: its share of the users assigned; got <value>", "arms[<n>].plans is required: at least one { name, stripe_price_id }", "arms[<n>].plans[<m>] must be { name, stripe_price_id }", "arms names <key> twice; each arm has its own key", "product_id or test_id is required: …" or "created_by is required: an internal caller has no signed-in user, so it names who is acting"; not found with "no product has id <id>; …" or "no plan test has id <id>; echo-cli pricing plans list --product <id> lists them"; a conflict with "plan test <id> was already stopped at <time>". /api/public/plans answers unauthorized with "sign in first: plans are assigned to a signed-in user" and a conflict with "plan test <id> stopped at <time>; it assigns no one" for a user it never assigned. Results fail naming the event when a stored Stripe payment's revenue or refund is not a number.
  • A pricing survey is refused with a bad request: "method must be one of van_westendorp, gabor_granger; got <value>", "min_answers must be a whole number of at least 1: the fewest answers a reading accepts; got <value>", "ladder is required for gabor_granger: at least two prices to ask about", "ladder[<n>] must be a price above 0; got <value>", "ladder names a price twice; each rung is a different price", "ladder belongs to gabor_granger only; van_westendorp respondents name their own prices", "purchase_scale belongs to van_westendorp only: …", "purchase_scale must be an object of at least one label, each with the purchase probability it stands for, from 0 through 1", "purchase_scale.<label> must be a probability from 0 through 1: how likely a respondent giving it is to buy; got <value>" or "product_id or survey_id is required: …"; not found with "no pricing survey has id <id>; echo-cli pricing wtp list --product <id> lists them". A reading with too few answers is refused: "pricing survey <id> has <n> answers for <all segments|segment name>; <method> readings here need at least <minimum>". /api/public/wtp answers unauthorized with "sign in first: a pricing survey is answered by a signed-in user", a bad request with "answers must be <the method's shape>: <what is wrong>", "answers.<buy_at_cheap|buy_at_expensive> must be one of <labels>: how likely you are to buy at that price; got <label>" or "answers must say buy or not at every price of the ladder, <prices>, once each", and a conflict with "you already answered pricing survey <id> at <time>".
  • wtp read --profile <icp-profile> --app <source> reads only the respondents the survey's product's ICP profile describes, as icp score matches people: a respondent (a signed-in user id) matches when, for every attribute of the profile at its latest version, the latest of their web or mobile events of --app carrying that attribute carries the profile's value, compared trimmed and case-blind; the reading then carries profile { name, version, app_id } (null otherwise). It is refused with a bad request: "app_id is required with profile: the web or mobile source whose events say which respondents the profile describes", "app_id narrows a reading by an ICP profile only; name the profile too", "segment and profile both narrow the answers read; name one: …", "profile <name> was retired at <time> by <name>; a retired hypothesis segments no one" or "profile <name> names no firmographic attribute, so no person can be matched to it; …"; not found with "product <id> has no ICP profile named <name>; echo-cli icp list --product <id> lists them"; and below the minimum with "pricing survey <id> has <n> answers for ICP profile <name> version <v> on <source>; …".
  • plans results with a --sigmas that is not a positive number is refused with a bad request: "sigmas must be a positive number: how many standard errors each interval spans; got <value>".