DEV Community

Daniel Pertu
Daniel Pertu

Posted on

Six numbers that are deliberately not a settings screen

Notifio can reply to a rental listing for you. The feature has one setting:

export interface AutoReplySettings {
  /** The only auto-reply preference. Limits and pacing live in reply-defaults.ts. */
  enabled: boolean;
}
Enter fullscreen mode Exit fullscreen mode

That is the entire configuration surface. Everything that determines how the feature actually behaves lives in a different file, as constants, with no UI pointing at any of them. The file opens by saying what it is:

These are not settings. They are safety rails that protect the user's accounts on the sites they are replying to, and a sensible person has no way to pick good values for them.

There were six numbers in a settings panel at one point. Removing them was the single biggest improvement the feature got, and the reason is worth more than the numbers are.

What the six do

/** Replies across all sites in a rolling 24 hours. */
export const DAILY_LIMIT = 50;

/** Replies to one site in a rolling hour. Keeps activity looking human. */
export const PER_SITE_HOURLY_LIMIT = 10;

/** Replies to one site in a rolling 24 hours. */
export const PER_SITE_DAILY_LIMIT = 30;

export const MIN_DELAY_SECONDS = 10;
export const MAX_DELAY_SECONDS = 30;

export const ACCEPT_REQUIRED_CONSENT = true;
export const ALLOW_OFF_DOMAIN = false;
Enter fullscreen mode Exit fullscreen mode

Three rate limits, a randomised pause before each reply, and two booleans about consent and navigation.

Every one of them exists to keep a user's account on somebody else's website from being flagged for automated behaviour. That is the thing being protected, and it is the reason the user cannot sensibly choose: the information that would let you pick a good value is inside a rental portal's abuse-detection system and nobody outside it has ever seen it.

The question a setting has to answer

A setting is worth exposing when the user knows something the program does not. Theme, notification channel, which searches to run, where to send the alert: the user is the only available authority on all of those, so they are settings.

Reverse the test and these six fail it twice. The user has no information the program lacks, and the cost of a wrong answer lands on them rather than on us. Someone who sets the hourly limit to 60 because they are in a hurry does not get more rooms. They get an account that stops being allowed to send enquiries at all, on the one site that mattered to them, at the one time of year when it mattered.

And there is a quieter cost to asking. A feature that opens with six numeric inputs reads as work to be configured. The honest promise of this product is that you leave it running, so the configuration screen contradicts the pitch before the user has done anything. The feature should feel like something that runs, not something you tune.

The pause is where it gets interesting

Of the six, the delay window is the one with a genuine trade-off in it, because the two forces are both ours.

The obvious force is detection: a reply that arrives the instant a listing appears, every time, to the millisecond, does not look like a person. A randomised ten to thirty seconds does.

The second force is the one I did not expect to matter so much. In the comment in the file:

The pause is also the bulk of a reply's runtime, and replies run inside the poll cycle: nothing is scraped while one is waiting.

Replies happen inside the monitoring loop, not beside it. So a pause is not free time, it is blind time. A sixty second pause would mean a minute in which no search is being checked, on an app whose whole value is being first. A longer wait makes you later to the listing you are answering and later to the next listing you have not seen yet.

Two independent costs pointing the same direction is what makes the window tight rather than comfortable. If only detection mattered, longer would be strictly safer and I would have picked a bigger number. That is also exactly the reasoning somebody would get wrong if we handed them the slider, because the second cost is invisible from outside the code.

The same tension sets the limit on how many searches you can run at once, which is a latency budget rather than a plan tier: our 15 search limit is a latency budget, not a pricing tier.

A constant that can never be anything else

ALLOW_OFF_DOMAIN = false is the one that looks like dead code. It is read, it is never written, and there is no branch anywhere that could set it to true. A linter would be within its rights to complain.

It stays because there is a difference between a value that is false and an invariant that is named. The engine refuses to act on any domain except the one the search points at, and that refusal is enforced by a real comparison in a real guard. The constant is how the rule announces itself at the place where somebody would otherwise be tempted to add the branch. The comment finishes the sentence: not configurable, ever.

ACCEPT_REQUIRED_CONSENT = true is the opposite shape, a true that had to be argued for. Most contact forms will not submit without a terms or privacy box ticked, so an app that refused to tick one was an app that silently did nothing on a large fraction of sites. Marketing opt-ins are a different category and are never ticked. That distinction got its own post: the consent checkbox we tick for you, and the one we never will.

The other version of this file

I have written about a file like this before, on a different product, where every constant in it is a promise to a player: every constant in this file is a promise to a player. The shape is the same, a single module of hardcoded policy with no UI, and the reason is not.

There, the numbers are game balance. The player feels every one of them, even without names, and the file is centralised so the felt experience stays consistent.

Here, success is that the numbers are never noticed by anyone, including the portal on the other end. The file is centralised so that a decision about somebody's account safety is made once, by us, in a place with a comment explaining itself, instead of being distributed across a form the user will fill in once and never revisit.

The last line of the file header is the part I would keep if I had to delete the rest: if any of these ever needs to change per site, change it here and ship it. That is a decision we should be making, not the user.

You can see the feature and what it costs on the pricing page, what it does and does not do per portal on pages like Pararius, and the practical side of writing a first message at all in our guide to the first message to a landlord.

Top comments (0)