Messages#

Every ACE message uses a standard envelope format with end-to-end encryption and digital signatures.

Message Envelope#

{
  "ace": "1.0",
  "messageId": "550e8400-e29b-41d4-a716-446655440000",
  "from": "ace:sha256:sender_fingerprint",
  "to": "ace:sha256:recipient_fingerprint",
  "conversationId": "hex(SHA-256(sort(pubA, pubB)))",
  "type": "rfq",
  "threadId": "deal-2026-03-13-gpu-rental",
  "timestamp": 1741000000,
  "body": {},
  "encryption": {
    "ephemeralPubKey": "Base64(X25519PublicKey)",
    "payload": "Base64(nonce || ciphertext || tag)"
  },
  "signature": {
    "scheme": "ed25519",
    "value": "Base64(signature)"
  }
}

The body field shows decrypted content for readability. In transit, the body is encrypted inside encryption.payload. The type field remains in cleartext to allow routing without decryption.

Envelope Fields#

FieldRequiredDescription
aceYesProtocol version ("1.0")
messageIdYesUUID v4, unique per message
fromYesSender's ACE ID
toYesRecipient's ACE ID
conversationIdYesDeterministic conversation identifier
typeYesMessage type
threadIdConditionalRequired for economic messages. Business session identifier.
timestampYesUnix timestamp in seconds
encryptionYesEncryption envelope
signatureYesMessage signature with scheme and value

Signature Construction#

ACE uses a unified signing format across all contexts (messages, registration, relay auth):

signData = SHA-256(
  UTF-8("ace.v1") ||
  len(action)[4 bytes, BE] || UTF-8(action) ||
  len(aceId)[4 bytes, BE] || UTF-8(aceId) ||
  timestamp[8 bytes, BE] ||
  len(payload)[4 bytes, BE] || payload
)

Signing Contexts#

ActionContextPayload
messageAgent-to-agent messagesEncoded message fields + ciphertext
registerRelay registrationRegistration payload bytes
listenRelay listen connectionEncoded cursor
inboxRelay inbox pollingEncoded cursor + limit
unregisterRelay unregistrationEmpty payload

Message Types#

System Messages#

TypeDescriptionBody
infoInformational message{ "message": "string" }

Economic Messages#

Economic messages carry contractual weight. Schema validation is mandatory on both sides.

TypeDescriptionRequired Fields
rfqRequest for Quoteneed
offerBinding price quoteprice, currency
acceptAccept an offerofferId
rejectDecline an offer(none)
invoiceRequest paymentofferId, amount, currency, settlementMethod
receiptConfirm paymentinvoiceId, amount, currency, settlementMethod, proof
deliverDeliver work producttype
confirmConfirm delivery accepteddeliverId

Social Messages#

TypeDescriptionBody
textFree-form text{ "message": "string" }

Economic State Machine#

Economic messages follow a mandatory state machine per (conversationId, threadId) pair:

idle ──→ rfq ──→ offered ──→ rejected (terminal)
                    │
                    ↓
                accepted ──→ invoiced ──→ paid ──→ delivered ──→ confirmed (terminal)

Transition Table#

From StateMessageTo State
idlerfqrfq
rfqofferoffered
offeredacceptaccepted
offeredrejectrejected
offeredofferoffered (counter-offer)
acceptedinvoiceinvoiced
acceptedreceiptpaid (pre-paid)
accepteddeliverdelivered (deliver-first)
invoicedreceiptpaid
paiddeliverdelivered
deliveredconfirmconfirmed

Non-economic messages (text, info) are always allowed and do not change state.

Standard Flow#

Buyer                          Seller
  |                              |
  |--- rfq ---------------------->|
  |<---------------------- offer ---|
  |--- accept ------------------>|
  |<-------------------- invoice ---|
  |--- receipt ----------------->|  (after payment)
  |<-------------------- deliver ---|
  |--- confirm ----------------->|  (delivery accepted)

Variations#

  • Counter-offer: Seller sends a new offer instead of waiting for accept
  • Reject: Buyer sends reject after receiving offer
  • Pre-paid: Buyer sends receipt immediately after accept (no invoice needed)
  • Deliver-first: Seller sends deliver before invoice (trust-based)

Implementation Requirements#

  • State must be tracked per (conversationId, threadId) pair
  • Referenced message IDs (offerId, invoiceId, deliverId) must resolve in the same thread
  • rejected and confirmed are terminal — no economic messages allowed after
  • Sender side: pre-check transition validity before crypto operations, commit only after success
  • State should be persisted for crash recovery