Part 4 of 5

Three Hundred Files

Two Scores, One for Making and One for Being Seen

The art community in this series scored its members, which every forum of the era did. It scored them twice, on two numbers that never mix, and both scoring systems are cron jobs of under two hundred lines with the weights written as bare integers.

Participation Counts What Was Made

The first score counts output. It runs nightly, over every member, and adds up:

$participation += $countComments * 1;
$participation += $countTopics * 10;
$participation += $countImages * 20;
$participation += $countForumImages * 20;

A comment is worth one. Starting a thread is worth ten. Posting an image is worth twenty, and an image attached to a forum post is worth the same twenty as one posted to a gallery.

Those four numbers are the entire editorial policy of the site, and they say something specific. A comment is cheap, so it scores cheap. Starting a discussion takes more than replying to one, at ten times the value. Making something and showing it is the point of the site, at twice the value of starting a discussion about it.

The equality of the last two is the interesting one. An image posted in a forum thread counts exactly as much as one submitted to a portfolio, which means the score does not push people toward the gallery. Work in progress dumped into a thread was treated as the same contribution as a finished piece put on a profile, and for a site whose value was people showing each other unfinished work, that is the correct call.

Popularity Counts How Others Responded

The second score never looks at what a member made. It looks at what happened to it:

$loveWeight = 3;
$friendWeight = 5;
$viewWeight = 1;

A view is worth one, a "love" on an image is worth three, and somebody subscribing to a portfolio is worth five. The ratios are a rough model of intent. A view might be an accident. A love is a deliberate act costing one click. A subscription is a standing commitment to see more, and is priced highest.

Why They Are Separate

Combining these into a single reputation number is what most forum software did, and it produces a score that cannot be read. A high number means someone posts a great deal, or is well liked, and there is no way to tell which from the outside.

Kept apart, they answer different questions and support different features. Participation is what a moderator wants when deciding whether an account is a contributor. Popularity is what a member wants when deciding whose portfolio to follow. A prolific poster nobody responds to and a rare poster everyone watches are legible as different people, and a single number would flatten both into the middle.

There is a second reason, which is that they fail differently. Participation is gameable by volume, and the defence against that is moderation. Popularity is gameable by collusion, and the defence against that is different. Separate numbers mean each can be defended on its own terms without breaking the other.

Both Are Tracked Daily

Neither score is only a current value. Both crons write a row into a tracking table on every run:

$sql = "INSERT INTO popularityTracking SET trackingDate = NOW(), points = '$total', userId = '$ouserId'";

Participation stores today's total alongside dayPoints, the difference from yesterday. Popularity looks back seven days and computes the change over the week.

Storing the series rather than the number is what turns a score into something that can be built on. It makes "rising this week" answerable, which is the query a front page actually wants, and it means a scoring change can be evaluated against history rather than argued about. The current value is derivable from the series and the series is not derivable from the value, so storing the harder one is the right way round.

The cost is a row per member per day forever, in a schema with no expiry policy anywhere in it.

The Spam Heuristic Is One Query

Registration spam is the tax on running a public forum, and the defence here is a cron job of about ten meaningful lines:

UPDATE users
SET RankId = '7'
WHERE currentLogin = '0000-00-00 00:00:00'
AND lastVisit = '0000-00-00 00:00:00'
AND joinDate < DATE_SUB(NOW(), INTERVAL 24 HOUR)

Registered more than a day ago, never signed in, never visited. Rank 7.

The observation underneath it is that a real person who goes to the trouble of registering comes back, and usually within a day. A script that registers to plant a profile link never does, because the registration was the whole point. That distinction takes no CAPTCHA, no email verification round trip and no heuristics on the content, and it costs one indexed query a day.

It is also unfalsifiable from the account's side, which is what makes it good. There is nothing a spammer can do about it except sign in, and signing in is the behavior being asked for.

Below that in the same file is a loop over a hardcoded list of blocked IP addresses, applying the same rank. That part has aged worse, since a static list of addresses is a permanent record of last year's spammers, but it costs nothing to keep.

Rank 7 Is a Ban, and Everything Depends on It

There is no banned column. A banned account is one whose rank is 7, and the enforcement is that rankId != '7' appears twenty times across the codebase, in every query that lists or counts members.

That is a real design in the sense that it works, and a fragile one in the sense that it is a convention. Nothing prevents a new query from omitting the clause, and the failure is silent: a banned member appears in a list somewhere, and nobody notices until they do. The scoring crons both carry it, which means banned accounts stop accruing points, which is the wanted behavior and is also the reason the omission would be hard to spot.

A column with a default and a view that filters on it moves this from something every author must remember to something the schema does once.

What Transfers

Measure contribution and reception separately. They answer different questions, they are gamed in different ways, and a combined number cannot be read by the person looking at it. This is the decision in here I would carry into anything with a reputation system.

Store the series, not the score. A daily row makes trend queries possible and makes a change to the weights evaluable after the fact. The current value falls out of the series for free.

A behavioral anti-spam check is cheap and hard to defeat. Registered and never returned is one query with no false-positive cost worth worrying about, because the remedy for being wrongly caught is to sign in.

A convention enforced in twenty places is not enforced. Rank 7 works because every author remembered. That holds until somebody doesn't, and the schema is where that belongs.

The last post in the series is about the rebrand, the thirteen files that were left behind by it, and the advertising platform that was paying for the hosting.