Johnnyloans sorry for the slow reply, I got back to my machine and had a proper look at the ?page= duplicates you reported. You found a real bug, and it is Flarum's, not something GrapheneOS has done.
Here is what happens.
When Flarum renders a discussion without JavaScript, which is what curl gets, it asks its own API for the discussion and then walks the whole response looking for anything post-shaped to print. The test it uses is roughly "is a post, has a discussion attached, has rendered content".
The catch is that the mentions extension asks that same API call to also return, for every post on the page, the other posts that have quoted it. Those quoted posts come back as full posts, with a discussion attached and with rendered content, so they satisfy that test too and get printed as though they belonged on the page. They then get printed again on the page they actually live on. That is your duplicate.
It also explains something you may not have noticed: the pages are not 20 posts long. On the thread you linked I got 29, 20, 23, 24, 21, 23, 20 and 24 posts across the first eight pages, with 21 posts appearing twice. I checked page one against the API and 10 of the 29 were posts quoted from elsewhere in the thread. The remaining one is a moderator action with no content, which is the same null you spotted in your own output.
Because the extras come from quoting, they pile up wherever people quote most, which is usually the post just above them, and that is why so many of your duplicates landed on page boundaries.
This is fixed in Flarum 2.0, where that page renderer was rewritten to ask for the posts it wants and print exactly those. I ran your test against a 2.0 forum: 20 posts on page one, 8 on page two, 28 total for a 28 post discussion, no duplicates.
Since 2.0 has not shipped yet and 1.x is what everyone including this forum is actually running, I am putting a fix together for 1.x as well. I will follow up here once it is in.
None of this affects your script, since /api/... was never involved. If anyone does want one page rather than a whole thread, /api/posts?filter[discussion]=<id>&page[offset]=<n>&page[limit]=20 returns exactly that window and nothing extra.
Thanks for the clear report. The screenshot and the repro made this quick to track down.