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 Group | 16 (DRD) |
| Command Code | 147 (0x93) |
| Server function | cmd_drd_get_changes in cmd_drd.c |
| Request body | Exactly 60 bytes: challenge(16) + parameters(26) + fee_locker(16) + terminator(2) |
| Authentication | None — the feed is public (it carries only public user records) |
| Page size | 1–200 entries per call (DRD_CHANGES_MAX_RESULTS); 0 or larger is clamped to 200 |
| Transport | UDP works for small pages; a page that overflows one datagram answers 218 — use TCP for bulk sync |
| Success response | Status 250 + flags(1) + count(2) + count entries |
| Fee | None 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)
| Body offset | Size | Field | Description |
|---|---|---|---|
| 0–11 | 12 | Challenge random | Random bytes generated by the client. |
| 12–15 | 4 | Challenge CRC32 | Big-endian CRC32 of bytes 0–11. |
| 16 | 1 | shard_count | Number of shards the serial space is split into, 1–25. Membership is sn % shard_count. |
| 17 | 1 | shard_index | Which shard to read, 0 to shard_count−1. |
| 18–25 | 8 | cursor_updated_at | Big-endian Unix seconds. 0 = start from the beginning (the next two fields are then ignored). |
| 26 | 1 | cursor_den | Denomination of the last entry received. |
| 27–30 | 4 | cursor_sn | Serial of the last entry received, big-endian. Entries strictly after (cursor_updated_at, cursor_den, cursor_sn) are returned. |
| 31–38 | 8 | updated_before | Big-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. |
| 39 | 1 | mode | 0 = live records + tombstones, 1 = live only, 2 = tombstones only. Any other value is 198. |
| 40–41 | 2 | page_size | Big-endian maximum entries to return. 0 or anything above 200 is clamped to 200. |
| 42–57 | 16 | fee_locker | Reserved: 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–59 | 2 | Terminator | Fixed 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 offset | Size | Field | Description |
|---|---|---|---|
| 0 | 1 | flags | bit 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–2 | 2 | count | Big-endian number of entries that follow, 0–200. 0 with status 250 means the shard is fully caught up. |
| 3... | var | entries | count 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 offset | Size | Field | Description |
|---|---|---|---|
| 0 | 1 | entry_flags | 0x01. |
| 1 | 1 | Denomination | Key of the deleted record. |
| 2–5 | 4 | Serial Number | Key of the deleted record, big-endian. |
| 6–13 | 8 | deleted_at | Big-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 offset | Size | Field | Description |
|---|---|---|---|
| 0 | 1 | entry_flags | 0x00. |
| 1 | 1 | Denomination | Record key. |
| 2–5 | 4 | Serial Number | Record key, big-endian. |
| 6–13 | 8 | Inbox fee | Big-endian signed int64, units of 10−8 CC. |
| 14 | 1 | First symbol | Avatar symbol index. |
| 15 | 1 | Second symbol | Avatar symbol index. |
| 16 | 1 | Class rejection (CR) | Signed minimum sender denomination (0x00 = accepts all). |
| 17–24 | 8 | created_at | Big-endian Unix seconds of the first post. |
| 25–32 | 8 | updated_at | Big-endian Unix seconds of the latest post. Use with DN/SN as the next cursor. |
| 33 | 1 | First-name length (FL) | 0–63. |
| 34... | FL | First name | UTF-8. |
| 34+FL | 1 | Last-name length (LL) | 0–63. |
| 35+FL... | LL | Last name | UTF-8. |
| 35+FL+LL | 2 | Description length (DL) | Big-endian, 0–512. |
| 37+FL+LL... | DL | Description | Raw 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
| Decimal | Hex | Symbol | Meaning |
|---|---|---|---|
| 250 | 0xFA | STATUS_SUCCESS | Page returned (possibly with count = 0). |
| 16 | 0x10 | ERROR_INVALID_PACKET_LENGTH | Body is not exactly 60 bytes. |
| 36 | 0x24 | ERROR_EMPTY_REQUEST | Body missing. |
| 198 | 0xC6 | ERROR_INVALID_PARAMETER | shard_count outside 1–25, shard_index ≥ shard_count, or mode > 2. |
| 218 | 0xDA | ERROR_TCP_REQUIRED | The page does not fit in one UDP datagram. Retry over TCP or with a smaller page_size. |
| 252 | 0xFC | ERROR_INTERNAL | Database failure. |
| 254 | 0xFE | ERROR_MEMORY_ALLOC | Server 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.