DRD Get Changes — Group 16, Code 147

Returns one page of the directory changes feed: every user record written (live) or deleted (tombstone) after a cursor, for one shard of the serial-number space. Open read: no authentication required. This is the differential-sync primitive that lets a client or a mirror pull a whole RAIDA’s user directory incrementally instead of one record at a time.

Quick reference

Command Group16 (DRD)
Command Code147 (0x93)
Server functioncmd_drd_get_changes in cmd_drd.c
Request bodyExactly 60 bytes: challenge(16) + parameters(26) + fee_locker(16) + terminator(2)
AuthenticationNone — the feed is public (it carries only public user records)
Page size1–200 entries per call (DRD_CHANGES_MAX_RESULTS); 0 or larger is clamped to 200
TransportUDP works for small pages; a page that overflows one datagram answers 218 — use TCP for bulk sync
Success responseStatus 250 + flags(1) + count(2) + count entries
FeeNone today. The fee_locker field is reserved for a future per-call download fee.

Sync model

Every write to the user table (post 140, delete 143) refreshes that record’s updated_at. Deletes do not erase the row: they tombstone it (personal fields blanked, denomination + serial + deletion time kept) so a mirror that has already copied the record can learn that it is gone. Tombstones are retained for 90 days (DRD_TOMBSTONE_RETENTION), then purged.

The feed is ordered by (updated_at, den, sn) ascending and is read with a three-part cursor of the same shape. A client remembers the cursor of the last entry it received and passes it back; the server returns entries strictly after it. A cursor whose updated_at is 0 means “from the beginning” and the den/sn parts are ignored.

Sharding. The serial-number space is split into shard_count shards (1–25) by sn % shard_count. A client asks for one shard per call, so 25 shards can be pulled in parallel from 25 different RAIDAs, or a single RAIDA can be drained shard by shard. Each shard keeps its own cursor.

Modes. 0 returns live records and tombstones interleaved in feed order, 1 live records only, 2 tombstones only.

Resync. If a client presents a cursor older than the tombstone-retention window, the server may already have purged deletions that client never saw. The page is still served, but the response sets the resync_required flag; the client must discard its copy of that shard and restart from cursor 0.

Request body (60 bytes)

Challenge random (12 bytes, part 1 of 2) 0 7 Challenge random (part 2) 8 11 Challenge CRC32 (BE) 12 15 shards 16 index 17 cursor_updated_at (BE, 8 bytes, part 1 of 2) 18 23 (part 2) 24 25 cur DN 26 cursor_sn (BE) 27 30 upd_bef 31 updated_before (BE, 8 bytes, continued) 32 38 mode 39 page_size (BE) 40 41 fee_locker (16 bytes, part 1 of 2) 42 47 fee_locker (part 2, all zeros = none) 48 57 3E 3E (term) 58 59
Body offsetSizeFieldDescription
0–1112Challenge randomRandom bytes generated by the client.
12–154Challenge CRC32Big-endian CRC32 of bytes 0–11.
161shard_countNumber of shards the serial space is split into, 1–25. Membership is sn % shard_count.
171shard_indexWhich shard to read, 0 to shard_count−1.
18–258cursor_updated_atBig-endian Unix seconds. 0 = start from the beginning (the next two fields are then ignored).
261cursor_denDenomination of the last entry received.
27–304cursor_snSerial of the last entry received, big-endian. Entries strictly after (cursor_updated_at, cursor_den, cursor_sn) are returned.
31–388updated_beforeBig-endian Unix seconds. Only entries with updated_at strictly before this time are returned; 0 = no upper bound. Lets a client freeze a snapshot boundary across many pages.
391mode0 = live records + tombstones, 1 = live only, 2 = tombstones only. Any other value is 198.
40–412page_sizeBig-endian maximum entries to return. 0 or anything above 200 is clamped to 200.
42–5716fee_lockerReserved: NUL-padded human-readable locker key for a future per-call download fee. Send all zeros. Today the server charges nothing; a non-zero key is only noted in the log.
58–592TerminatorFixed 3E 3E. The body must be exactly 60 bytes.

Response

Response payload (3 + entries)

+-------+-------+---------+---------+-----
| flags | count | entry 0 | entry 1 | ...
| 1     | 2 (BE)| var     | var     |
+-------+-------+---------+---------+-----
offset 0   1       3
Payload offsetSizeFieldDescription
01flagsbit 0 = has_more: more entries exist past this page; call again with the cursor of the last entry. bit 1 = resync_required: the supplied cursor is older than tombstone retention (90 days); deletions may have been purged, so discard this shard and restart from cursor 0.
1–22countBig-endian number of entries that follow, 0–200. 0 with status 250 means the shard is fully caught up.
3...varentriescount entries back to back, each in one of the two formats below, in (updated_at, den, sn) order.

Entry formats

Each entry begins with a one-byte entry_flags. Bit 0 selects the format; all other bits are reserved and zero.

Tombstone entry (entry_flags bit 0 = 1) — 14 bytes

+-------------+----+--------+------------+
| entry_flags | DN | SN     | deleted_at |
| 1 (0x01)    | 1  | 4 (BE) | 8 (BE)     |
+-------------+----+--------+------------+
offset 0        1    2        6
Entry offsetSizeFieldDescription
01entry_flags0x01.
11DenominationKey of the deleted record.
2–54Serial NumberKey of the deleted record, big-endian.
6–138deleted_atBig-endian Unix seconds of the delete. This is also the entry’s updated_at for cursor purposes.

Live entry (entry_flags bit 0 = 0) — 1 + user record

The entry_flags byte (0x00) is followed by one user record in the standard DRD serialization, the same layout get_user (141) returns, including the trailing description field:

+-------------+----+--------+-----------+----+----+----+------------+------------+----+-------+----+------+--------+-------------+
| entry_flags | DN | SN     | inbox fee | S1 | S2 | CR | created_at | updated_at | FL | first | LL | last | DL     | description |
| 1 (0x00)    | 1  | 4 (BE) | 8 (BE)    | 1  | 1  | 1  | 8 (BE)     | 8 (BE)     | 1  | FL    | 1  | LL   | 2 (BE) | DL          |
+-------------+----+--------+-----------+----+----+----+------------+------------+----+-------+----+------+--------+-------------+
offset 0        1    2        6           14   15   16   17           25           33   34      ..   ..     ..       ..
Entry offsetSizeFieldDescription
01entry_flags0x00.
11DenominationRecord key.
2–54Serial NumberRecord key, big-endian.
6–138Inbox feeBig-endian signed int64, units of 10−8 CC.
141First symbolAvatar symbol index.
151Second symbolAvatar symbol index.
161Class rejection (CR)Signed minimum sender denomination (0x00 = accepts all).
17–248created_atBig-endian Unix seconds of the first post.
25–328updated_atBig-endian Unix seconds of the latest post. Use with DN/SN as the next cursor.
331First-name length (FL)0–63.
34...FLFirst nameUTF-8.
34+FL1Last-name length (LL)0–63.
35+FL...LLLast nameUTF-8.
35+FL+LL2Description length (DL)Big-endian, 0–512.
37+FL+LL...DLDescriptionRaw UTF-8 free text the user posted about themselves.

Side effects

Although 147 is a read, it doubles as the RAIDA’s once-a-day DRD housekeeping trigger. The first call each day purges tombstones older than 90 days and runs the multi-device maintenance sweep (reaping delivered tells and pruning stale device links). These are cheap no-ops on later calls the same day and never change the page you receive.

Status codes

DecimalHexSymbolMeaning
2500xFASTATUS_SUCCESSPage returned (possibly with count = 0).
160x10ERROR_INVALID_PACKET_LENGTHBody is not exactly 60 bytes.
360x24ERROR_EMPTY_REQUESTBody missing.
1980xC6ERROR_INVALID_PARAMETERshard_count outside 1–25, shard_indexshard_count, or mode > 2.
2180xDAERROR_TCP_REQUIREDThe page does not fit in one UDP datagram. Retry over TCP or with a smaller page_size.
2520xFCERROR_INTERNALDatabase failure.
2540xFEERROR_MEMORY_ALLOCServer allocation failure.

Common mistakes

Advancing the cursor by time alone

Many records can share one updated_at second. The cursor is the full triple (updated_at, den, sn) of the last entry received; passing only the time skips or repeats records.

Ignoring resync_required

When flag bit 1 is set the page is still valid data, but the client’s local copy of that shard may contain records the RAIDA has since deleted and can no longer report. Drop the shard and restart from cursor 0.

Pulling a full directory over UDP

A page of 200 live records is far larger than one datagram. Bulk sync should open a TCP connection; UDP is fine for small catch-up polls.

Expecting the same feed from every RAIDA

Each RAIDA’s DRD is independent and timestamps are stamped by the receiving server. Cursors are per RAIDA per shard; never reuse a cursor from one server on another.