Onchain data standard
The DACP v1 message schema, identifiers, ordering, validation, and reconstruction rules.
This page defines the public data contract between Detalk, independent indexers, and compatible clients. A hosted database may accelerate reads, but it is never the source of truth.
Protocol invariant
Two independent implementations that read the same registered contracts from the same startBlock must derive the same identifiers, valid messages, references, edits, payments, and final channel state.
Version discovery
Each deployment is registered once with its protocol version, module addresses, earliest event block, and schema hash:
event ProtocolDeploymentRegistered(
uint16 indexed version,
address indexed messageRegistry,
address indexed channelManager,
address paymentMembership,
address sessionKeyRegistry,
address relayCredits,
uint256 startBlock,
bytes32 schemaHash
);Registrations cannot be overwritten. Deprecation appends a new record and never changes the meaning of historical data.
Stable identifiers
All identifiers use abi.encode, fixed integer widths, and the execution chain's actual chain ID. abi.encodePacked is not permitted.
channelId = keccak256(abi.encode(
keccak256(bytes("DACP_CHANNEL_ID")), uint256(chainId), target
));
publicSubchannelId = keccak256(abi.encode(
keccak256(bytes("DACP_PUBLIC_SUBCHANNEL_ID")), channelId
));target must be a non-zero 20-byte EVM address. Base Mainnet uses chain ID 8453; Base Sepolia uses 84532. Names are presentation aliases and never replace the target address in identifier derivation.
A messageId commits to the domain, protocol version, chain ID, Message Registry, channel and subchannel, author, author nonce, message kind, reference, support amount, and keccak256(content). The first author nonce is 0; uint64.max is the exhausted state and cannot publish.
Canonical message record
Every successful message transaction emits one canonical record:
event MessagePublished(
bytes32 indexed channelId,
bytes32 indexed subchannelId,
bytes32 indexed messageId,
address target,
address author,
uint8 kind,
uint64 authorNonce,
bytes32 referenceId,
uint128 supportAmount,
bytes content
);The three indexed fields are the primary query keys. supportAmount is denominated in wei and is 0 for an ordinary message. content is the complete UTF-8 message body, limited to 1,000 bytes. Invalid UTF-8, zero identifiers, an unknown kind, or an invalid empty/non-empty field shape is rejected.
| Value | Kind | Reference | Content |
|---|---|---|---|
0 | POST | zero | non-empty |
1 | REPLY | earlier message version in the same subchannel | non-empty |
2 | QUOTE | earlier message in the same registry | non-empty |
3 | REPOST | earlier message in the same registry | empty |
4 | EDIT | the author's original message | non-empty |
5 | TOMBSTONE | the author's original message | empty |
The official MVP creates POST, REPLY, and EDIT messages. Compatible clients still preserve and interpret all six DACP v1 values.
Ordering and references
Logs are processed in ascending (blockNumber, transactionIndex, logIndex) order. API arrival time, block timestamp, WebSocket order, and database IDs are not canonical ordering signals.
- A reference is evaluated once against valid objects that appeared earlier in the same Message Registry.
- A missing, forward, cross-registry, or otherwise malformed reference remains invalid even if a target appears later.
- A reply stays in the same subchannel. QUOTE and REPOST may cross channels but not registries.
- The last valid EDIT controls the current display. The original record remains available, and an existing reply keeps the version it referenced when published.
- The first valid TOMBSTONE ends default display updates without removing historical data.
Semantically invalid logs may be retained for audit, but they do not enter canonical conversation state or counts.
Direct and sponsored submission
For a direct wallet transaction, the transaction sender and MessagePublished.author are the same address. For a valid Session Key submission, the Relay may be the transaction sender, but the author remains the wallet that granted the authorization. A following SessionMessagePublished record identifies the session key, submitter, and authorization nonce; it never replaces the canonical author.
Reorganizations and duplicates
Raw logs are uniquely identified by (chainId, contractAddress, blockHash, transactionHash, logIndex). Exact duplicates are processed once. Removed logs are rolled back together with every derived state change after their block, then the remaining canonical history is replayed from the common ancestor.
An indexer must be able to delete its database, replay every registered contract from startBlock, and produce the same final state. Search, pagination, previews, notifications, unread counts, and personal display settings remain replaceable offchain views.