Contents

The bridging algorithm behind Community Notes

Posted on April 27, 2026 by Daniel Vitek.
Tagged: , .
There’s a fascinating article from last year in Asterisk magazine about how the Community Notes feature on Twitter/X came to be. The article focuses more on the iterative design and rollout process behind Community Notes—formerly known by the wonderful name Birdwatch—than on the “bridging” algorithm that Twitter/X uses to evaluate notes. Thankfully, an October 2022 paper published by the feature’s designers (and linked in the article) gives more details.
The current Community Notes evaluation algorithm, described here with code here, contains substantially more bells and whistles.

The bridging algorithm is quite clever, and I found both certain choices that the designers made and the nature of their results to be non-intuitive. I’ll go through the details of the algorithm, then talk a little bit about what the results mean. I’ll also include some details about matrix factorization that aren’t relevant to the bridging algorithm implementation, but were useful to me in thinking about why the authors chose the minimization algorithm they did.

The data

The only data the bridging algorithm uses are ratings assigned by users to prospective community notes. Per the paper, users have three options for ratings: “helpful”, “somewhat helpful”, or “not helpful”. A key perspective behind the bridging algorithm is to view this data as a matrix R=(ru,n)R = (r_{u,n}) indexed by user uu and note nn; this is a standard approach in the recommender-systems literature. For the bridging algorithm, the entry ru,nr_{u,n} takes the value 00 for a “not helpful” rating, 11 for a “helpful” rating, and is null if the user has not rated the note.
It’s not clear from the paper how “somewhat helpful” ratings are handled.

The matrix RR has a couple features worth noting:

Matrix factorization

The key idea behind the bridging algorithm is to try and approximate the rating matrix R=(ru,n)R = (r_{u,n}) as r^u,nμ+iu+jn+fugn\hat{r}_{u,n} \approx \mu + i_u + j_n + f_u g_n where μ\mu is a scalar, ii and ff are vectors indexed by user, and jj and gg are vectors indexed by note. This is simply a rank-one approximate matrix factorization of RR together with certain intercept terms. There are 2(U+N)+12(U+N)+1 free parameters.

Interpreting the parameters

Let’s go through each of the five parameters (μ,f,g,i,j\mu, f, g, i, j) and try to understand how various features of the data are going to affect each of them.

Note that μ,i,j\mu, i, j all have natural interpretations along the “helpfulness” axis, whereas ff and gg don’t in and of themselves. This is because we can multiply every fuf_u by some global scalar cc and divide each gng_n by cc without affecting the quality of our approximation.
The choice of sign on cc might affect whether people suspect us of secretly being dog lovers or cat lovers, though.

Now, in the recommender-system setting, we want to find items nn such that jnj_n is large and the item content gng_n aligns with the user affinity fuf_u.
In the high-rank setting, this is a perfect use case for vector-retrieval databases.
But if instead we want to evaluate a note’s usefulness in the Community Notes setting, the scalar value jnj_n is the only thing we should focus on. If we want to show all users, no matter their position on the petisan axis, notes that they’ll find helpful, we should just show notes with both dog and cat pictures: that is, notes with positive jnj_n values. And this is exactly what Community Notes does: in order to be displayed to all users, notes must have a note intercept value of at least 0.40.4 in the system described in the paper. More generally, notes with intercept values below 0.08-0.08 are categorized as “unhelpful”, notes with intercepts above 0.40.4 are categorized as “helpful”, and notes with intercepts between 0.08-0.08 and 0.40.4 are categorized as “needs more ratings”.

Why rank one?

It is possible to make the scalars fuf_u and gng_n defined above vectors instead, provided we replace multiplication by taking the dot product. This makes ff a u×ku×k matrix and gg a k×nk×n matrix (the vector case above being k=1k=1), and takes the total number of parameters from 2(U+N)+12(U+N)+1 to (k+1)(U+N)+1(k+1)(U+N)+1. This is the traditional approach in the recommender-systems literature, where we can imagine distinct axes such as “is this a horror movie?” or “does this movie pass the Bechdel test?”.

We should expect that adding these parameters (called factor dimensions) improves the fit, and the authors note that taking k=2k=2 modestly improved RMSE on held-out samples from 0.076 to 0.073. Unfortunately, they also note that the additional factor dimension “reduced interpretability and replicability”, and hence they used only one factor dimension in the system evaluated in the paper. This isn’t terribly surprising, since in the paper’s example we are quite close to the overparametrized regime: taking even k=6k = 6 would yield more free parameters than data points. (Another point in favor of taking k=1k=1 is that higher kk make the diamond plots below more complicated.)

The choice of norm

We also need to decide how we’re going to evaluate the quality of a proposed matrix factorization. The authors use the objective function 1Pru,n(ru,nr^u,n)2+λi(μ2+1Ui2+1Nj2)+λf(1Uf2+1Ng2)\frac{1}{P}\sum_{r_{u,n}} \left(r_{u,n} - \hat{r}_{u,n}\right)^2 +\lambda_i\left(\mu^2 + \frac{1}{U}\left\|i\right\|^2 + \frac{1}{N}\left\|j\right\|^2\right) + \lambda_f\left(\frac{1}{U}\left\|f\right\|^2 + \frac{1}{N}\left\|g\right\|^2\right)

with λi=0.15λ_i = 0.15 and λf=0.03λ_f = 0.03. This is a pretty straightforward choice: it’s basically the first thing you’d write down, and it’s got some nice properties. Let’s go through the three terms in order.

It’s not completely clear to me what the intuition behind the precise values of λi\lambda_i and λf\lambda_f is, or if there’s any intuition there at all. The authors give a good explanation for why λi>λf\lambda_i > \lambda_f: namely, that the preference for good precision (few false positives for good Community Notes) is much stronger than the preference for good recall (few false negatives). Part of the logic here is that there are other ways to address poor recall, such as deliberately seeking out additional ratings on prospective Community Notes with jn<0.4j_n < 0.4.

Learning the factorization

So now that we know what we’re looking for, and why we’re looking for it, we only need address how we’re going to find good values for our parameters μ,f,g,i,j\mu, f, g, i, j. If it were just the Frobenius term and RR were dense, we could use the rank-1 singular value decomposition. Unfortunately, the regularization terms make a linear-algebraic solution rather more challenging. So, following a tradition dating back (at least) to Simon Funk’s work on the Netflix Prize dataset, we just do gradient descent. After all, gradient descent on the Frobenius norm will actually find arbitrary-rank SVD approximations of a matrix, so we should expect it to do fine here. But there are a few fun insights in store for us about how to actually do the gradient descent!

Doing it ourselves

Twitter/X makes daily snapshots of Community Notes data available for download,
I believe that logging in is required for downloading the data, and that snapshot data is delayed approximately 48 hours to prevent interference with notes that are currently being rated.
so we can try and replicate these results. We’re going to deliberately skip a few tweaks that we discuss below, and we’re not going to spend much time optimizing gradient-descent hyperparameters, so we shouldn’t necessarily expect to get results anywhere near those of the paper. On the other hand, we have a couple orders of magnitude more data: recent snapshots contain 150M+ ratings by 1.5M+ users on 1.5M+ notes.

I slapped together some code that gets ratings files, drops all the data we don’t need to do matrix factorization, filters out notes/raters with too few ratings, and then runs the matrix factorization algorithm. You can find it

One battle after another

I hit pitfall after pitfall when setting this up. I started off by not reading the code and doing a pretty vanilla ML setup: Adam, learning rate around 3×1043 \times 10^{-4}, and batching. Given the form of the loss function above, we should expect initial loss on the order of 0.25-0.5, simply because guessing 0.5 for μ\mu and something around zero for everything else is a pretty decent starting point. Indeed, the pre-epoch-one losses were in this neighborhood. But the post-epoch-one losses were enormous: often greater than 10. So what went wrong?

This is a good puzzle, so feel free to stop and think about it for a bit. If you want, here’s a hint: it’s not momentum-related, as the same phenomenon occurs if we switch from Adam to SGD.

The answer turns out to be the batching. If we step our optimizer every 100,000 ratings or so, it’s very likely that a number of notes or raters only appear once in that ratings batch. So even with the regularization terms, the optimizer will
A plot of  values for all notes in a recent Community Notes daily snapshot. The peak at the origin is notes that are excluded from the matrix-factorization dataset due to not having enough ratings.

Diamond plots

Further tweaks

Two further tweaks are worth mentioning: diamond regularization and filtering for rater helpfulness.

Diamond regularization

While not mentioned in the paper, diamond regularization is another tool to help ensure that only Community Notes with cross-factor consensus have good intercept. The idea is to add the term λd1Nnjngn\lambda_d \frac{1}{N} \sum_n \left|j_ng_n\right| to our loss functional; a suggested value is λd=0.75\lambda_d = 0.75. This term penalizes notes that have both a large note content coefficient (in either direction) and a large note intercept, which makes sense: we should be hesitant to assign a large note-intercept value to a note whose ratings are also explained via a large note-content coefficient, since that suggests that much of the appeal of the note is due to partisanship rather than consensus. The name “diamond regularization” then suggests itself: we are penalizing notes that lie outside of the diamond shape on plots like the one above.

Rater helpfulness

Unsurprisingly, humans being humans, many users will give good prospective Community Notes bad ratings: for example, if the note is a consensus correction to a partisan tweet and the rater agrees with the perspective of the tweet. Because our threshold for unhelpfulness is so low, a few such early ratings can doom a good consensus-perspective note before it gets off the ground.

One way to combat this is to perform two rounds of our matrix-factorization algorithm. In the first round, the authors use all rating data to assign provisional ratings (using the same “helpful”/“needs more ratings”/“unhelpful” trichotomy as above) to all notes. Then they select for raters whose ratings generally agree
The threshold in the paper is 66% agreement.
with the provisional ratings, counting only ratings made before the provisional rating on a note became public. After a further filtering-out of raters who have authored low-quality Community Notes, all ratings by filtered-out raters are eliminated. In the paper’s example, roughly half of the raters and ratings remained after this filtering. Then a second round of matrix-factorization ratings are computed and used as the final ratings.