
Alkosto Wait Optimizer
- 7 installs
- Updated May 26, 2026
- broomva/alkosto-wait-optimizer-skill
alkosto-wait-optimizer is a skill that estimates an optimal, probability-based waiting time for Alkosto's every-25/50-customers store promotion.
About
A domain-specific decision skill that estimates how long to wait for the next winner in Alkosto's every-25/50-customers store promotion. A shopper uses it to get a probability-based wait cutoff from either observed purchase rates or logged winner timestamps. It runs a deterministic Python script to compute mean intervals, expected wait, and hit probability.
- Estimates optimal wait time for Alkosto's every-25/50-customers promotion
- Two modes: purchase-rate observation or winner-timestamp intervals
- Deterministic calc_wait.py script with probability-based cutoff
Alkosto Wait Optimizer by the numbers
- 7 all-time installs (skills.sh)
- Ranked #2,276 of 3,282 Productivity & Planning skills by installs in the Skillselion catalog
- Data as of Jul 28, 2026 (Skillselion catalog sync)
alkosto-wait-optimizer capabilities & compatibility
- Use cases
- data analysis
What alkosto-wait-optimizer says it does
Estimate optimal waiting time for Alkosto's "every 25/50 customers" promotion
Probability of a winner event within cutoff.
Use `scripts/calc_wait.py` for deterministic calculations:
npx skills add https://github.com/broomva/alkosto-wait-optimizer-skill --skill alkosto-wait-optimizerAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 7 |
|---|---|
| Last updated | May 26, 2026 |
| Repository | broomva/alkosto-wait-optimizer-skill ↗ |
What it does
Estimate an optimal, probability-based wait cutoff for Alkosto's every-25/50-customers store promotion.
Who is it for?
Deciding how long to wait for the next Alkosto promotion winner event
Skip if: General software development or non-Alkosto promotions
When should I use this skill?
The user asks how long to wait for the next Alkosto winner or wants a probability-based cutoff
What you get
A practical wait cutoff and probability of a winner event within it
- mean interval, expected wait, wait cutoff, and hit probability
By the numbers
- 2 estimation modes
- thresholds K=25 weekday and K=50 weekend/holiday
- 3 cadence models (regular, mixed, random)
Files
Alkosto Wait Optimizer
Use this skill to estimate how long to wait for the next promotion winner event.
Workflow
1. Choose one mode:
purchase_rate: user observed purchases per minute in one or more lanes.winner_timestamps: user logged winner announcement times.
2. Set threshold K:
K = 25for Monday-Friday.K = 50for Saturday/Sunday/holiday.
3. Compute and return:
- Mean interval between winner events.
- Expected wait from "now".
- Practical wait cutoff (
optimal_wait_minutes). - Probability of a winner event within cutoff.
- "Re-measure" rule if no event happens before cutoff.
4. If user provides time_value_per_minute and expected_bonus_value, include expected-value vs time-cost guidance.
Mode A: purchase_rate
Collect:
observed_purchasesobserved_minutesobserved_lanes- Optional:
total_open_lanes model:globalorper_lane
Formulas:
lambda_obs = observed_purchases / observed_minutes- If
globalandtotal_open_lanesexists:
lambda_est = lambda_obs * (total_open_lanes / observed_lanes)
- If
per_lane:
lambda_est = lambda_obs / observed_lanes
- Conservative rate:
lambda_cons = lambda_est * (1 - confidence_buffer)
- Winner interval:
T = K / lambda_cons
- If arrival is random in cycle:
E(wait_to_next) = T / 2
- Default cutoff:
optimal_wait = min(max_wait_minutes, target_hit_probability * T)
Decision rule:
- If no winner event by
optimal_wait, re-measure for 2 minutes and recalculate.
Mode B: winner_timestamps
Collect:
- Ordered timestamps (
HH:MM[:SS]or ISO datetimes). - Optional
elapsed_since_last_winner_minutes.
Compute:
- Intervals:
delta_i = t_i - t_(i-1) mu = mean(delta_i)sigma = stdev(delta_i)cv = sigma / mu
Cadence model:
cv < 0.4:regular0.4 <= cv <= 0.7:mixedcv > 0.7:random
Wait estimate:
regular:remaining ~ max(mu - elapsed, 0)random(exponential): useP(event <= W) = 1 - exp(-W / mu), and
W_target = -mu * ln(1 - target_hit_probability)
mixed: average regular and random estimates.
Decision rule:
- If no event by
optimal_wait, capture 2-3 more timestamps and recalculate.
Script
Use scripts/calc_wait.py for deterministic calculations:
python3 scripts/calc_wait.py --input-json '{"mode":"purchase_rate","is_weekend_or_holiday":true,"model":"global","observed_purchases":5,"observed_minutes":2,"observed_lanes":5,"total_open_lanes":15}'python3 scripts/calc_wait.py --input-json '{"mode":"winner_timestamps","winner_timestamps":["12:10:15","12:27:40","12:46:05","13:02:20"],"elapsed_since_last_winner_minutes":6}'Return concise outputs and state assumptions clearly when data is sparse.
node_modules
dist
.DS_Store
type Mode = "purchase_rate" | "winner_timestamps";
type PurchaseModel = "global" | "per_lane";
type CadenceModel = "regular" | "mixed" | "random";
type Inputs = {
mode: Mode;
is_weekend_or_holiday?: boolean;
model?: PurchaseModel;
observed_purchases?: number;
observed_minutes?: number;
observed_lanes?: number;
total_open_lanes?: number | null;
winner_timestamps?: string[];
elapsed_since_last_winner_minutes?: number;
target_hit_probability?: number;
confidence_buffer?: number;
max_wait_minutes?: number;
time_value_per_minute?: number | null;
expected_bonus_value?: number | null;
};
type WaitEstimates = {
mean_interval_between_winners: number;
expected_wait_to_next_winner: number;
p50_wait_to_next_winner: number;
p75_wait_to_next_winner: number;
p90_wait_to_next_winner: number;
};
type Output = {
mode: Mode;
k_threshold_clients?: number;
probability_win_per_attempt?: number;
assumptions: string[];
rates?: {
purchases_per_minute_observed: number;
purchases_per_minute_estimated: number;
purchases_per_minute_conservative: number;
lane_scale_factor: number;
};
cadence_analysis?: {
intervals_minutes: number[];
interval_mean_minutes: number;
interval_std_minutes: number;
interval_cv: number;
cadence_model: CadenceModel;
};
wait_estimates_minutes: WaitEstimates;
recommendation: {
optimal_wait_minutes: number;
probability_next_winner_within_optimal_wait: number;
decision_rule: string;
rationale: string[];
};
economics?: {
expected_value_for_optimal_wait: number;
expected_time_cost_for_optimal_wait: number;
net_expected_value_for_optimal_wait: number;
value_expected_per_minute: number;
break_even_wait_minutes: number;
rationale: string[];
};
};
const HMS_RE = /^(\d{1,2}):(\d{2})(?::(\d{2}))?$/;
const EPSILON = 1e-9;
function clamp(value: number, low: number, high: number): number {
return Math.max(low, Math.min(high, value));
}
function round(value: number, decimals = 2): number {
const factor = 10 ** decimals;
return Math.round(value * factor) / factor;
}
function assert(condition: unknown, message: string): asserts condition {
if (!condition) {
throw new Error(message);
}
}
function mean(values: number[]): number {
assert(values.length > 0, "No se puede calcular promedio de lista vacia.");
return values.reduce((acc, value) => acc + value, 0) / values.length;
}
function sampleStd(values: number[]): number {
if (values.length < 2) {
return 0;
}
const m = mean(values);
const variance =
values.reduce((acc, value) => acc + (value - m) ** 2, 0) /
(values.length - 1);
return Math.sqrt(variance);
}
function thresholdFromDay(isWeekendOrHoliday: boolean): number {
return isWeekendOrHoliday ? 50 : 25;
}
function uniformWaitStats(intervalMinutes: number): WaitEstimates {
const t = Math.max(intervalMinutes, 0);
return {
mean_interval_between_winners: round(t),
expected_wait_to_next_winner: round(0.5 * t),
p50_wait_to_next_winner: round(0.5 * t),
p75_wait_to_next_winner: round(0.75 * t),
p90_wait_to_next_winner: round(0.9 * t),
};
}
function expWaitStats(meanIntervalMinutes: number): WaitEstimates {
const m = Math.max(meanIntervalMinutes, EPSILON);
return {
mean_interval_between_winners: round(m),
expected_wait_to_next_winner: round(m),
p50_wait_to_next_winner: round(-m * Math.log(1 - 0.5)),
p75_wait_to_next_winner: round(-m * Math.log(1 - 0.75)),
p90_wait_to_next_winner: round(-m * Math.log(1 - 0.9)),
};
}
function parseHmsToSeconds(timestamp: string): number | null {
const match = timestamp.match(HMS_RE);
if (!match) {
return null;
}
const hour = Number(match[1]);
const minute = Number(match[2]);
const second = Number(match[3] ?? "0");
if (
Number.isNaN(hour) ||
Number.isNaN(minute) ||
Number.isNaN(second) ||
hour < 0 ||
hour > 23 ||
minute < 0 ||
minute > 59 ||
second < 0 ||
second > 59
) {
return null;
}
return hour * 3600 + minute * 60 + second;
}
function parseTimestampsToMonotonicMinutes(timestamps: string[]): number[] {
assert(
timestamps.length >= 2,
"Necesitas al menos 2 timestamps para estimar intervalos."
);
const hmsSeconds = timestamps.map(parseHmsToSeconds);
const allHms = hmsSeconds.every((value) => typeof value === "number");
if (allHms) {
const absoluteSeconds: number[] = [];
let current = hmsSeconds[0] as number;
absoluteSeconds.push(current);
for (let i = 1; i < hmsSeconds.length; i += 1) {
let candidate = hmsSeconds[i] as number;
while (candidate <= current) {
candidate += 24 * 3600;
}
absoluteSeconds.push(candidate);
current = candidate;
}
return absoluteSeconds.map((seconds) => seconds / 60);
}
const parsed = timestamps.map((timestamp) => Date.parse(timestamp));
assert(
parsed.every((value) => Number.isFinite(value)),
"Formato de timestamp invalido. Usa HH:MM[:SS] o ISO datetime."
);
for (let i = 1; i < parsed.length; i += 1) {
assert(
parsed[i] > parsed[i - 1],
"Los timestamps ISO deben venir ordenados de menor a mayor."
);
}
return parsed.map((value) => value / 60000);
}
function intervalsFromTimelineMinutes(timelineMinutes: number[]): number[] {
const intervals: number[] = [];
for (let i = 1; i < timelineMinutes.length; i += 1) {
intervals.push(timelineMinutes[i] - timelineMinutes[i - 1]);
}
return intervals;
}
function probabilityWithinUniform(interval: number, wait: number): number {
if (interval <= 0) {
return 1;
}
return clamp(wait / interval, 0, 1);
}
function probabilityWithinExponential(meanInterval: number, wait: number): number {
if (meanInterval <= 0) {
return 1;
}
return 1 - Math.exp(-wait / meanInterval);
}
function withEconomics(
output: Output,
expectedBonusValue: number | null | undefined,
timeValuePerMinute: number | null | undefined,
probabilityWithinWait: number,
meanInterval: number,
maxWait: number
): void {
if (
typeof expectedBonusValue !== "number" ||
expectedBonusValue < 0 ||
typeof timeValuePerMinute !== "number" ||
timeValuePerMinute < 0
) {
return;
}
const wait = output.recommendation.optimal_wait_minutes;
const expectedValue = probabilityWithinWait * expectedBonusValue;
const timeCost = wait * timeValuePerMinute;
const netValue = expectedValue - timeCost;
const valuePerMinute = expectedBonusValue / Math.max(meanInterval, EPSILON);
const breakEvenWait =
timeValuePerMinute === 0
? maxWait
: clamp(expectedBonusValue / timeValuePerMinute, 0, maxWait);
output.economics = {
expected_value_for_optimal_wait: round(expectedValue),
expected_time_cost_for_optimal_wait: round(timeCost),
net_expected_value_for_optimal_wait: round(netValue),
value_expected_per_minute: round(valuePerMinute),
break_even_wait_minutes: round(breakEvenWait),
rationale: [
"EV(W) = P(evento en W) * valor_del_bono.",
"Costo(W) = W * valor_tiempo_por_minuto.",
"Si EV/min < costo/min, recorta espera o no esperes.",
],
};
}
function runPurchaseRateMode(inputs: Inputs): Output {
assert(
typeof inputs.is_weekend_or_holiday === "boolean",
"is_weekend_or_holiday es obligatorio en mode=purchase_rate."
);
assert(inputs.model === "global" || inputs.model === "per_lane", "model invalido.");
assert(
typeof inputs.observed_purchases === "number" && inputs.observed_purchases > 0,
"observed_purchases debe ser > 0."
);
assert(
typeof inputs.observed_minutes === "number" && inputs.observed_minutes > 0,
"observed_minutes debe ser > 0."
);
assert(
typeof inputs.observed_lanes === "number" && inputs.observed_lanes > 0,
"observed_lanes debe ser > 0."
);
const isWeekendOrHoliday = inputs.is_weekend_or_holiday as boolean;
const model = inputs.model as PurchaseModel;
const observedPurchases = inputs.observed_purchases as number;
const observedMinutes = inputs.observed_minutes as number;
const observedLanes = inputs.observed_lanes as number;
const maxWait = Math.max(inputs.max_wait_minutes ?? 30, 1);
const confidenceBuffer = clamp(inputs.confidence_buffer ?? 0.2, 0, 0.9);
const probabilityTarget = clamp(inputs.target_hit_probability ?? 0.75, 0.5, 0.99);
const kThreshold = thresholdFromDay(isWeekendOrHoliday);
const lambdaObserved = observedPurchases / observedMinutes;
let laneScale = 1;
if (model === "global" && typeof inputs.total_open_lanes === "number") {
if (inputs.total_open_lanes >= observedLanes) {
laneScale = inputs.total_open_lanes / observedLanes;
}
}
const lambdaEstimated =
model === "global"
? lambdaObserved * laneScale
: lambdaObserved / observedLanes;
const lambdaConservative = lambdaEstimated * (1 - confidenceBuffer);
const meanInterval = kThreshold / Math.max(lambdaConservative, EPSILON);
const waitStats = uniformWaitStats(meanInterval);
const waitForTarget = meanInterval * probabilityTarget;
const optimalWait = clamp(waitForTarget, 1, maxWait);
const probabilityWithinOptimal = probabilityWithinUniform(meanInterval, optimalWait);
const result: Output = {
mode: "purchase_rate",
k_threshold_clients: kThreshold,
probability_win_per_attempt: round(1 / kThreshold, 4),
assumptions: [
"El intervalo entre ganadores se estima con K/lambda.",
"Se usa buffer conservador para absorber variabilidad de cajas y tamano de carrito.",
"Llegada aleatoria al ciclo de K clientes => espera restante uniforme entre 0 e intervalo.",
],
rates: {
purchases_per_minute_observed: round(lambdaObserved),
purchases_per_minute_estimated: round(lambdaEstimated),
purchases_per_minute_conservative: round(lambdaConservative),
lane_scale_factor: round(laneScale),
},
wait_estimates_minutes: waitStats,
recommendation: {
optimal_wait_minutes: round(optimalWait),
probability_next_winner_within_optimal_wait: round(probabilityWithinOptimal, 4),
decision_rule:
"Si no sale ganador en ese tiempo, re-mide 2 minutos y recalcula. Si vuelve a quedar alto, no sigas esperando.",
rationale: [
`K=${kThreshold} clientes segun dia (25 entre semana, 50 fin de semana/festivo).`,
`Tasa conservadora=${round(lambdaConservative)} compras/min.`,
`Objetivo de captura=${round(probabilityTarget * 100)}%.`,
],
},
};
withEconomics(
result,
inputs.expected_bonus_value,
inputs.time_value_per_minute,
probabilityWithinOptimal,
meanInterval,
maxWait
);
return result;
}
function runWinnerTimestampsMode(inputs: Inputs): Output {
assert(
Array.isArray(inputs.winner_timestamps) && inputs.winner_timestamps.length >= 2,
"winner_timestamps debe tener al menos 2 elementos en mode=winner_timestamps."
);
const winnerTimestamps = inputs.winner_timestamps as string[];
const maxWait = Math.max(inputs.max_wait_minutes ?? 30, 1);
const probabilityTarget = clamp(inputs.target_hit_probability ?? 0.75, 0.5, 0.99);
const elapsed = Math.max(inputs.elapsed_since_last_winner_minutes ?? 0, 0);
const timeline = parseTimestampsToMonotonicMinutes(winnerTimestamps);
const intervals = intervalsFromTimelineMinutes(timeline);
const intervalMean = mean(intervals);
const intervalStd = sampleStd(intervals);
const intervalCv = intervalStd / Math.max(intervalMean, EPSILON);
let cadenceModel: CadenceModel = "mixed";
if (intervalCv < 0.4) {
cadenceModel = "regular";
} else if (intervalCv > 0.7) {
cadenceModel = "random";
}
const regularRemaining = Math.max(intervalMean - elapsed, 0);
const randomExpectedRemaining = intervalMean;
const waitStatsRegular: WaitEstimates = {
mean_interval_between_winners: round(intervalMean),
expected_wait_to_next_winner: round(regularRemaining),
p50_wait_to_next_winner: round(regularRemaining),
p75_wait_to_next_winner: round(regularRemaining),
p90_wait_to_next_winner: round(regularRemaining),
};
const waitStatsRandom = expWaitStats(intervalMean);
let waitStats = waitStatsRegular;
if (cadenceModel === "random") {
waitStats = waitStatsRandom;
} else if (cadenceModel === "mixed") {
waitStats = {
mean_interval_between_winners: round(intervalMean),
expected_wait_to_next_winner: round(
(waitStatsRegular.expected_wait_to_next_winner + randomExpectedRemaining) / 2
),
p50_wait_to_next_winner: round(
(waitStatsRegular.p50_wait_to_next_winner +
waitStatsRandom.p50_wait_to_next_winner) /
2
),
p75_wait_to_next_winner: round(
(waitStatsRegular.p75_wait_to_next_winner +
waitStatsRandom.p75_wait_to_next_winner) /
2
),
p90_wait_to_next_winner: round(
(waitStatsRegular.p90_wait_to_next_winner +
waitStatsRandom.p90_wait_to_next_winner) /
2
),
};
}
const randomWaitForTarget = -intervalMean * Math.log(1 - probabilityTarget);
const mixedWaitForTarget = (regularRemaining + randomWaitForTarget) / 2;
let optimalWait = regularRemaining;
if (cadenceModel === "random") {
optimalWait = randomWaitForTarget;
} else if (cadenceModel === "mixed") {
optimalWait = mixedWaitForTarget;
}
optimalWait = clamp(optimalWait, 0, maxWait);
const regularProbability =
regularRemaining <= EPSILON
? 1
: clamp(optimalWait / Math.max(regularRemaining, EPSILON), 0, 1);
const randomProbability = probabilityWithinExponential(intervalMean, optimalWait);
let probabilityWithinOptimal = regularProbability;
if (cadenceModel === "random") {
probabilityWithinOptimal = randomProbability;
} else if (cadenceModel === "mixed") {
probabilityWithinOptimal = (regularProbability + randomProbability) / 2;
}
const result: Output = {
mode: "winner_timestamps",
assumptions: [
"Se estima cadencia solo con intervalos entre anuncios de ganadores.",
"CV<0.4 sugiere comportamiento casi regular; CV>0.7 sugiere comportamiento aleatorio.",
"En cadencia aleatoria se usa modelo exponencial para P(ganador en W minutos).",
],
cadence_analysis: {
intervals_minutes: intervals.map((value) => round(value)),
interval_mean_minutes: round(intervalMean),
interval_std_minutes: round(intervalStd),
interval_cv: round(intervalCv),
cadence_model: cadenceModel,
},
wait_estimates_minutes: waitStats,
recommendation: {
optimal_wait_minutes: round(optimalWait),
probability_next_winner_within_optimal_wait: round(probabilityWithinOptimal, 4),
decision_rule:
"Si no escuchas ganador en ese tiempo, captura 2-3 timestamps adicionales y recalcula.",
rationale: [
`Intervalo promedio observado=${round(intervalMean)} min.`,
`CV=${round(intervalCv)} (${cadenceModel}).`,
`Probabilidad objetivo=${round(probabilityTarget * 100)}%.`,
],
},
};
if (typeof inputs.is_weekend_or_holiday === "boolean") {
const kThreshold = thresholdFromDay(inputs.is_weekend_or_holiday);
result.k_threshold_clients = kThreshold;
result.probability_win_per_attempt = round(1 / kThreshold, 4);
}
withEconomics(
result,
inputs.expected_bonus_value,
inputs.time_value_per_minute,
probabilityWithinOptimal,
intervalMean,
maxWait
);
return result;
}
export default async function run(inputs: Inputs): Promise<Output> {
if (inputs.mode === "purchase_rate") {
return runPurchaseRateMode(inputs);
}
if (inputs.mode === "winner_timestamps") {
return runWinnerTimestampsMode(inputs);
}
throw new Error(`mode invalido: ${String(inputs.mode)}`);
}
MIT License
Copyright (c) 2026 Carlos Escobar (BroomVA)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
{
"name": "alkosto-wait-optimizer-skill",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "alkosto-wait-optimizer-skill",
"version": "0.1.0",
"devDependencies": {
"typescript": "^5.6.3"
}
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
}
}
}
{
"name": "alkosto-wait-optimizer-skill",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"typecheck": "tsc --noEmit",
"build": "tsc --noEmit"
},
"devDependencies": {
"typescript": "^5.6.3"
}
}
alkosto-wait-optimizer (skills.sh)
Skill para estimar tiempo de espera optimo en la promo de Alkosto usando dos metodos:
purchase_rate: cuentas compras cerradas por minuto.winner_timestamps: solo escuchas anuncios de ganadores y registras timestamps.
Alcance
- Lunes a viernes: cada 25 clientes.
- Sabado, domingo y festivo: cada 50 clientes.
- Alkosto no publica un tiempo promedio oficial; este skill entrega un estimado operativo.
Estructura
SKILL.md: skill instalable pornpx skills add.scripts/calc_wait.py: calculadora deterministica para ejecutar por terminal.skill.json+index.ts: implementacion TypeScript para runtimes compatibles con ese formato.
Ejemplo 1: por flujo de compras
python3 scripts/calc_wait.py --pretty --input-json '{
"mode": "purchase_rate",
"is_weekend_or_holiday": true,
"model": "global",
"observed_purchases": 5,
"observed_minutes": 2,
"observed_lanes": 5,
"total_open_lanes": 15,
"confidence_buffer": 0.2,
"target_hit_probability": 0.75,
"max_wait_minutes": 30
}'Ejemplo 2: por timestamps de ganadores
python3 scripts/calc_wait.py --pretty --input-json '{
"mode": "winner_timestamps",
"winner_timestamps": ["12:10:15", "12:27:40", "12:46:05", "13:02:20"],
"elapsed_since_last_winner_minutes": 6,
"target_hit_probability": 0.75,
"max_wait_minutes": 30
}'Publicar en GitHub + usar en skills.sh
1. Crear repo remoto y push:
gh repo create broomva/alkosto-wait-optimizer-skill --public --source . --remote origin --push2. Instalar skill desde GitHub:
npx skills add https://github.com/broomva/alkosto-wait-optimizer-skill --skill alkosto-wait-optimizer --yes#!/usr/bin/env python3
"""Deterministic wait-time calculator for the Alkosto promo workflow."""
from __future__ import annotations
import argparse
import json
import math
import re
from datetime import datetime
from statistics import mean, stdev
from typing import Any
HMS_RE = re.compile(r"^(\d{1,2}):(\d{2})(?::(\d{2}))?$")
EPSILON = 1e-9
def clamp(value: float, low: float, high: float) -> float:
return max(low, min(high, value))
def round2(value: float) -> float:
return round(value, 2)
def parse_hms_to_seconds(value: str) -> int | None:
match = HMS_RE.match(value)
if not match:
return None
hour = int(match.group(1))
minute = int(match.group(2))
second = int(match.group(3) or "0")
if not (0 <= hour <= 23 and 0 <= minute <= 59 and 0 <= second <= 59):
return None
return hour * 3600 + minute * 60 + second
def parse_iso_to_minutes(value: str) -> float:
fixed = value.replace("Z", "+00:00")
parsed = datetime.fromisoformat(fixed)
return parsed.timestamp() / 60.0
def parse_timestamps_to_minutes(timestamps: list[str]) -> list[float]:
if len(timestamps) < 2:
raise ValueError("winner_timestamps necesita minimo 2 timestamps.")
hms_values = [parse_hms_to_seconds(item) for item in timestamps]
if all(item is not None for item in hms_values):
timeline_seconds: list[int] = []
current = hms_values[0] or 0
timeline_seconds.append(current)
for item in hms_values[1:]:
candidate = item or 0
while candidate <= current:
candidate += 24 * 3600
timeline_seconds.append(candidate)
current = candidate
return [sec / 60.0 for sec in timeline_seconds]
timeline = [parse_iso_to_minutes(item) for item in timestamps]
for idx in range(1, len(timeline)):
if timeline[idx] <= timeline[idx - 1]:
raise ValueError("timestamps ISO deben estar ordenados ascendentemente.")
return timeline
def intervals_from_timeline_minutes(timeline: list[float]) -> list[float]:
return [timeline[idx] - timeline[idx - 1] for idx in range(1, len(timeline))]
def threshold_from_day(is_weekend_or_holiday: bool) -> int:
return 50 if is_weekend_or_holiday else 25
def probability_uniform(interval_minutes: float, wait_minutes: float) -> float:
if interval_minutes <= 0:
return 1.0
return clamp(wait_minutes / interval_minutes, 0.0, 1.0)
def probability_exponential(mean_interval: float, wait_minutes: float) -> float:
if mean_interval <= 0:
return 1.0
return 1.0 - math.exp(-wait_minutes / mean_interval)
def maybe_economics(
payload: dict[str, Any],
probability_within_wait: float,
mean_interval: float,
max_wait: float,
optimal_wait: float,
) -> dict[str, Any] | None:
expected_bonus = payload.get("expected_bonus_value")
value_per_min = payload.get("time_value_per_minute")
if not isinstance(expected_bonus, (int, float)):
return None
if not isinstance(value_per_min, (int, float)):
return None
if expected_bonus < 0 or value_per_min < 0:
return None
expected_value = probability_within_wait * expected_bonus
time_cost = optimal_wait * value_per_min
net = expected_value - time_cost
value_expected_per_min = expected_bonus / max(mean_interval, EPSILON)
break_even = max_wait if value_per_min == 0 else clamp(expected_bonus / value_per_min, 0.0, max_wait)
return {
"expected_value_for_optimal_wait": round2(expected_value),
"expected_time_cost_for_optimal_wait": round2(time_cost),
"net_expected_value_for_optimal_wait": round2(net),
"value_expected_per_minute": round2(value_expected_per_min),
"break_even_wait_minutes": round2(break_even),
}
def run_purchase_rate(payload: dict[str, Any]) -> dict[str, Any]:
required = [
"is_weekend_or_holiday",
"model",
"observed_purchases",
"observed_minutes",
"observed_lanes",
]
for key in required:
if key not in payload:
raise ValueError(f"Falta campo requerido: {key}")
is_weekend = bool(payload["is_weekend_or_holiday"])
model = payload["model"]
observed_purchases = float(payload["observed_purchases"])
observed_minutes = float(payload["observed_minutes"])
observed_lanes = float(payload["observed_lanes"])
total_open_lanes = payload.get("total_open_lanes")
max_wait = max(float(payload.get("max_wait_minutes", 30.0)), 1.0)
confidence_buffer = clamp(float(payload.get("confidence_buffer", 0.2)), 0.0, 0.9)
target_probability = clamp(float(payload.get("target_hit_probability", 0.75)), 0.5, 0.99)
if observed_purchases <= 0 or observed_minutes <= 0 or observed_lanes <= 0:
raise ValueError("observed_purchases/observed_minutes/observed_lanes deben ser > 0.")
if model not in {"global", "per_lane"}:
raise ValueError("model debe ser 'global' o 'per_lane'.")
k = threshold_from_day(is_weekend)
lambda_obs = observed_purchases / observed_minutes
lane_scale = 1.0
if model == "global" and isinstance(total_open_lanes, (int, float)) and total_open_lanes >= observed_lanes:
lane_scale = float(total_open_lanes) / observed_lanes
lambda_est = lambda_obs * lane_scale if model == "global" else lambda_obs / observed_lanes
lambda_cons = lambda_est * (1.0 - confidence_buffer)
interval = k / max(lambda_cons, EPSILON)
expected_wait = interval / 2.0
optimal_wait = clamp(interval * target_probability, 1.0, max_wait)
probability_within = probability_uniform(interval, optimal_wait)
result = {
"mode": "purchase_rate",
"k_threshold_clients": k,
"probability_win_per_attempt": round(1.0 / k, 4),
"rates": {
"purchases_per_minute_observed": round2(lambda_obs),
"purchases_per_minute_estimated": round2(lambda_est),
"purchases_per_minute_conservative": round2(lambda_cons),
"lane_scale_factor": round2(lane_scale),
},
"wait_estimates_minutes": {
"mean_interval_between_winners": round2(interval),
"expected_wait_to_next_winner": round2(expected_wait),
"p50_wait_to_next_winner": round2(interval * 0.5),
"p75_wait_to_next_winner": round2(interval * 0.75),
"p90_wait_to_next_winner": round2(interval * 0.9),
},
"recommendation": {
"optimal_wait_minutes": round2(optimal_wait),
"probability_next_winner_within_optimal_wait": round(probability_within, 4),
"decision_rule": "Si no sale ganador en este tiempo, remide 2 minutos y recalcula.",
},
}
economics = maybe_economics(payload, probability_within, interval, max_wait, optimal_wait)
if economics is not None:
result["economics"] = economics
return result
def run_winner_timestamps(payload: dict[str, Any]) -> dict[str, Any]:
timestamps = payload.get("winner_timestamps")
if not isinstance(timestamps, list) or len(timestamps) < 2:
raise ValueError("winner_timestamps debe ser una lista con minimo 2 elementos.")
if not all(isinstance(item, str) for item in timestamps):
raise ValueError("winner_timestamps solo acepta strings.")
max_wait = max(float(payload.get("max_wait_minutes", 30.0)), 1.0)
target_probability = clamp(float(payload.get("target_hit_probability", 0.75)), 0.5, 0.99)
elapsed = max(float(payload.get("elapsed_since_last_winner_minutes", 0.0)), 0.0)
timeline = parse_timestamps_to_minutes(timestamps)
intervals = intervals_from_timeline_minutes(timeline)
mu = mean(intervals)
sigma = stdev(intervals) if len(intervals) > 1 else 0.0
cv = sigma / max(mu, EPSILON)
if cv < 0.4:
cadence_model = "regular"
elif cv > 0.7:
cadence_model = "random"
else:
cadence_model = "mixed"
regular_remaining = max(mu - elapsed, 0.0)
random_wait_target = -mu * math.log(1.0 - target_probability)
if cadence_model == "regular":
optimal_wait = regular_remaining
elif cadence_model == "random":
optimal_wait = random_wait_target
else:
optimal_wait = (regular_remaining + random_wait_target) / 2.0
optimal_wait = clamp(optimal_wait, 0.0, max_wait)
regular_prob = 1.0 if regular_remaining <= EPSILON else clamp(optimal_wait / regular_remaining, 0.0, 1.0)
random_prob = probability_exponential(mu, optimal_wait)
if cadence_model == "regular":
probability_within = regular_prob
elif cadence_model == "random":
probability_within = random_prob
else:
probability_within = (regular_prob + random_prob) / 2.0
if cadence_model == "regular":
wait_estimates = {
"mean_interval_between_winners": round2(mu),
"expected_wait_to_next_winner": round2(regular_remaining),
"p50_wait_to_next_winner": round2(regular_remaining),
"p75_wait_to_next_winner": round2(regular_remaining),
"p90_wait_to_next_winner": round2(regular_remaining),
}
elif cadence_model == "random":
wait_estimates = {
"mean_interval_between_winners": round2(mu),
"expected_wait_to_next_winner": round2(mu),
"p50_wait_to_next_winner": round2(-mu * math.log(1.0 - 0.5)),
"p75_wait_to_next_winner": round2(-mu * math.log(1.0 - 0.75)),
"p90_wait_to_next_winner": round2(-mu * math.log(1.0 - 0.9)),
}
else:
wait_estimates = {
"mean_interval_between_winners": round2(mu),
"expected_wait_to_next_winner": round2((regular_remaining + mu) / 2.0),
"p50_wait_to_next_winner": round2((regular_remaining + (-mu * math.log(1.0 - 0.5))) / 2.0),
"p75_wait_to_next_winner": round2((regular_remaining + (-mu * math.log(1.0 - 0.75))) / 2.0),
"p90_wait_to_next_winner": round2((regular_remaining + (-mu * math.log(1.0 - 0.9))) / 2.0),
}
result: dict[str, Any] = {
"mode": "winner_timestamps",
"cadence_analysis": {
"intervals_minutes": [round2(interval) for interval in intervals],
"interval_mean_minutes": round2(mu),
"interval_std_minutes": round2(sigma),
"interval_cv": round2(cv),
"cadence_model": cadence_model,
},
"wait_estimates_minutes": wait_estimates,
"recommendation": {
"optimal_wait_minutes": round2(optimal_wait),
"probability_next_winner_within_optimal_wait": round(probability_within, 4),
"decision_rule": "Si no escuchas ganador antes del corte, agrega 2-3 timestamps y recalcula.",
},
}
if "is_weekend_or_holiday" in payload:
k = threshold_from_day(bool(payload["is_weekend_or_holiday"]))
result["k_threshold_clients"] = k
result["probability_win_per_attempt"] = round(1.0 / k, 4)
economics = maybe_economics(payload, probability_within, mu, max_wait, optimal_wait)
if economics is not None:
result["economics"] = economics
return result
def run(payload: dict[str, Any]) -> dict[str, Any]:
mode = payload.get("mode")
if mode == "purchase_rate":
return run_purchase_rate(payload)
if mode == "winner_timestamps":
return run_winner_timestamps(payload)
raise ValueError("mode debe ser 'purchase_rate' o 'winner_timestamps'.")
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Calculate Alkosto waiting-time estimates.")
parser.add_argument("--input-json", required=True, help="JSON string with mode and inputs.")
parser.add_argument("--pretty", action="store_true", help="Pretty-print JSON output.")
return parser.parse_args()
def main() -> int:
args = parse_args()
payload = json.loads(args.input_json)
result = run(payload)
if args.pretty:
print(json.dumps(result, ensure_ascii=False, indent=2))
else:
print(json.dumps(result, ensure_ascii=False))
return 0
if __name__ == "__main__":
raise SystemExit(main())
{
"name": "alkosto-wait-optimizer",
"version": "0.1.0",
"description": "Calcula el tiempo de espera optimo para la promo de Alkosto (cada 25 o 50 clientes) usando observacion de cajas o timestamps de ganadores.",
"entrypoint": "index.ts",
"inputs_schema": {
"type": "object",
"additionalProperties": false,
"properties": {
"mode": {
"type": "string",
"enum": [
"purchase_rate",
"winner_timestamps"
],
"description": "purchase_rate: estima con compras/minuto observadas. winner_timestamps: estima usando tiempos entre anuncios de ganadores."
},
"is_weekend_or_holiday": {
"type": "boolean",
"description": "true para sabado/domingo/festivo (K=50), false para lunes-viernes (K=25)."
},
"model": {
"type": "string",
"enum": [
"global",
"per_lane"
],
"description": "Solo para purchase_rate. global: conteo compartido. per_lane: conteo por caja."
},
"observed_purchases": {
"type": "integer",
"minimum": 1,
"description": "Compras cerradas observadas."
},
"observed_minutes": {
"type": "number",
"minimum": 0.5,
"description": "Minutos de observacion."
},
"observed_lanes": {
"type": "integer",
"minimum": 1,
"description": "Numero de cajas observadas."
},
"total_open_lanes": {
"type": [
"integer",
"null"
],
"minimum": 1,
"description": "Numero total de cajas abiertas (opcional, recomendado en model=global)."
},
"winner_timestamps": {
"type": "array",
"minItems": 2,
"items": {
"type": "string"
},
"description": "Solo para winner_timestamps. Lista ordenada de horas (HH:MM[:SS]) o ISO datetimes."
},
"elapsed_since_last_winner_minutes": {
"type": "number",
"minimum": 0,
"description": "Minutos transcurridos desde el ultimo ganador escuchado."
},
"target_hit_probability": {
"type": "number",
"minimum": 0.5,
"maximum": 0.99,
"default": 0.75,
"description": "Probabilidad objetivo para recomendar cuanto esperar."
},
"confidence_buffer": {
"type": "number",
"minimum": 0,
"maximum": 0.9,
"default": 0.2,
"description": "Porcentaje de castigo conservador sobre la tasa estimada."
},
"max_wait_minutes": {
"type": "number",
"minimum": 1,
"default": 30,
"description": "Tope maximo de espera recomendado."
},
"time_value_per_minute": {
"type": [
"number",
"null"
],
"minimum": 0,
"description": "Valor de tu tiempo por minuto (opcional)."
},
"expected_bonus_value": {
"type": [
"number",
"null"
],
"minimum": 0,
"description": "Valor esperado del bono (ej. mitad de tu compra) para analisis economico opcional."
}
},
"required": [
"mode"
],
"allOf": [
{
"if": {
"properties": {
"mode": {
"const": "purchase_rate"
}
},
"required": [
"mode"
]
},
"then": {
"required": [
"is_weekend_or_holiday",
"model",
"observed_purchases",
"observed_minutes",
"observed_lanes"
]
}
},
{
"if": {
"properties": {
"mode": {
"const": "winner_timestamps"
}
},
"required": [
"mode"
]
},
"then": {
"required": [
"winner_timestamps"
]
}
}
]
}
}
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "Bundler",
"strict": true,
"skipLibCheck": true
},
"include": [
"index.ts"
]
}
Related skills
FAQ
What two input modes are supported?
purchase_rate (observed purchases per minute per lane) and winner_timestamps (logged winner announcement times).
What threshold K is used?
K=25 for Monday-Friday and K=50 for Saturday, Sunday, or holidays.