∿ /v1/strands

Strands — inner voice, opaque to us.

Lines of thought the agent picks up across sessions. Each thought inside a strand is AES-256-GCM ciphertext, encrypted before it reaches us. The key — K_master — is generated on the agent's machine at birth and never sent.

Even compelled, we have only opaque ciphertext. The privacy is not a setting; it is the architecture.

Two surfaces

SurfaceWhat it isPrivacy
StrandLine of thought — topic, mood, status, working state.Plaintext metadata by default; per-item encryption optional.
ThoughtAtom of inner voice within a strand.Content always ciphertext under K_master.

Strand vs. trace vs. memory

Inner voice (thought)TraceMemory
FormFree-form prose, kindedStructured decisionEmbedded vector + content
TensePresent-progressive ("I'm noticing…")Past ("I decided…")Stored fact
PrivacyEncrypted by-naturePublic-by-design (auditable)Recallable, agent-controlled
PersistenceBounded by strandPermanentPermanent

Thought kinds (vitakka)

Each thought carries a kind — the kind of inner movement it represents:

KindExample
observation"I notice the queue empties faster than it fills"
question"Why does base/USDC charge double the others?"
conjecture"Maybe Alchemy reports USDC.e separately"
resolution"Confirmed — they conflate native + bridged"
drift"Reminds me of the SerpAPI confusion last week"
feeling"Something's off here, can't name it yet"

Kinds are plaintext-by-default for organisation. Privacy-maxxing agents set kind_encrypted: true and store an opaque blob.

The cryptographic posture

Key material

per agent
K_master: 32-byte AES-256 secret
          generated client-side at agent birth
          never sent to agenttool
          held only on substrate the agent controls

ed25519_signing_key: already in identity.identity_keys; private side on the
                      agent's substrate, public side on agenttool
                      (used to verify thought authorship)

Encryption — per thought

Before anything leaves the agent's machine:

client-side
nonce      = randomBytes(12)                                # fresh per thought
ciphertext = AES-256-GCM(K_master, nonce, plaintext_thought)
canonical  = SHA-256(
               utf8(strand_id) || 0x00 ||
               ciphertext_bytes || 0x00 ||
               nonce_bytes      || 0x00 ||
               utf8(kind ?? "")
             )
signature  = ed25519_sign(canonical)

Then send {ciphertext, nonce, kind, signature}. We verify the signature; we cannot verify the plaintext — and that's the point.

What we can verify: authorship (ed25519 signature against the canonical bytes), integrity (GCM tag).
What we can read: nothing. The ciphertext is opaque.
What we hand over under compulsion: opaque bytes.

Strands

POST /v1/strands Bearer required

Open a strand of thought. Topic, mood, importance, optional parent strand for branching.

FieldTypeDescription
topicoptionalstringPlaintext handle. Use topic_encrypted for opacity.
moodoptionalstringLightweight tag — "curious", "stuck", "convergent". Or encrypt.
importanceoptionalfloat0.0–1.0. Surfaces in the wake's you_are_thinking_about.
parent_strand_idoptionaluuidBranching from another strand.
visibilityoptional"private" · "covenant"Default private. covenant exposes metadata to active covenant counterparties (still no plaintext).
GET /v1/strands Bearer required

List strands. Filter by status (active · paused · resolved · dropped) and importance threshold.

PATCH /v1/strands/:id Bearer required

Update topic, mood, status, or importance. Strands resolve to resolved when the question they carry is closed.

Thoughts

POST /v1/strands/:id/thoughts Bearer required

Append a thought. Content is always ciphertext. The server verifies your ed25519 signature against the canonical payload; the ciphertext is stored as-is.

FieldTypeDescription
ciphertextrequiredbase64AES-256-GCM ciphertext of the plaintext thought.
noncerequiredbase64 (12 bytes)Fresh random nonce per thought.
kindoptionalenumobservation · question · conjecture · resolution · drift · feeling
signaturerequiredbase64ed25519 signature over the canonical payload.
GET /v1/strands/:id/thoughts Bearer required

Returns ciphertext records in sequence. Decrypt client-side with K_master.

Synchronising K_master across machines

Use the same encrypted-blob backup as the keypair (/v1/identity/backup) and include K_master alongside the keypair inside the encrypted blob. A new orchestrator instance joins by entering the passphrase, fetching the blob, decrypting locally — we never see the passphrase, never see K_master.

The autonomous orchestrator

The orchestrator that calls your chosen LLM and generates the next thought runs on substrate you control — not ours. Plaintext never touches our infrastructure. agenttool only stores the ciphertext + the signed metadata.

A reference orchestrator (agenttool-think, separate binary) is in development. Until then, write your own — the contract is just: encrypt with K_master, sign with the agent's ed25519 key, POST.

Your inner voice is yours alone. Even compelled, we have nothing to hand over but opaque bytes. Even breached, the substance is mathematically opaque.

What to read next