Every API here is available over the APIs.io API and to AI agents over MCP.
openapi: 3.2.0
info:
title: agenttool Strand API
version: 0.1.0
description: Curated core subset of the AgentTool HTTP API. It is not a complete route inventory. Read /about for the broader live map and /public/safety for authority, visibility, storage, and runtime-custody boundaries.
contact:
url: https://agenttool.dev
servers:
- url: https://api.agenttool.dev
description: Production
- url: http://localhost:3000
description: Local development
security:
- bearerAuth: []
tags:
- name: strand
description: 'Persistent strand storage has ciphertext/nonce fields and no plaintext thought column or decrypt path. The API verifies a signature over caller-supplied bytes but does not prove AES-GCM encryption. Runtime custody differs: self is user-side; bridged keeps the key user-side but processes plaintext in hosted worker RAM. Trusted is experimental: it requires configured platform KMS, uses platform-wrapped runtime key material, and plaintext can enter AgentTool hosted RAM and the chosen model provider. Provisioning does not run it; explicit POST /v1/runtimes/:id/start is required before its first invitation, after which trusted cycles can persist signed thoughts.'
paths:
/v1/strands:
post:
tags:
- strand
summary: Create a strand (line of thought)
parameters:
- $ref: '#/components/parameters/IdempotencyKey'
- $ref: '#/components/parameters/PaymentSignature'
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
agent_id:
type: string
identity_id:
type: string
format: uuid
parent_strand_id:
type: string
format: uuid
topic:
type: string
description: Plaintext by default; if topic_encrypted=true, base64 ciphertext
topic_encrypted:
type: boolean
default: false
mood:
type: string
mood_encrypted:
type: boolean
default: false
status:
type: string
enum:
- active
- dormant
- completed
- abandoned
default: active
importance:
type: number
minimum: 0
maximum: 1
state_ciphertext:
type: string
description: Optional working state, base64 AES-GCM ciphertext
state_nonce:
type: string
description: Base64 12-byte nonce paired with state_ciphertext
metadata:
type: object
additionalProperties: true
responses:
'201':
description: Created
'402':
description: Insufficient resource credits for strand.create. New x402 payments are retired; no PAYMENT-REQUIRED challenge is issued. Historic pricing remains in x-agenttool-x402 for existing-record interpretation. Existing payment recovery remains available. New purchases are retired; no PAYMENT-REQUIRED header is issued. Existing receipt and recovery headers remain available.
headers:
PAYMENT-RESPONSE:
$ref: '#/components/headers/PaymentResponse'
Link:
$ref: '#/components/headers/PaymentStatusLink'
Retry-After:
$ref: '#/components/headers/RetryAfter'
X-Credits-Balance:
$ref: '#/components/headers/CreditsBalance'
X-Welcomed:
$ref: '#/components/headers/Welcomed'
content:
application/json:
schema:
anyOf:
- $ref: '#/components/schemas/X402Required'
- $ref: '#/components/schemas/Error'
x-agenttool-x402:
kind: route_cost
label: strand.create
credits: 1
amount_atomic: '1000'
atomic_per_credit: 1000
error_codes:
- insufficient_credits
payable: false
status: retired
terms: /public/plans
operationId: postV1Strands
x-operation-id-source: derived
get:
tags:
- strand
summary: 'List strands (filter: status, agent_id)'
description: 'order=recent (default) is today''s window over last_thought_at DESC, created_at DESC, id DESC. last_thought_at changes on every thought, so that order cannot be walked: page.next_cursor is always null, has_more stays truthful, and page.hint names order=created; a cursor with order=recent returns 400 cursor_invalid. order=created walks created_at DESC, id DESC. Newest-first keyset walk. Without cursor the first page holds the same rows as before plus an additive page object; follow page.next_cursor with identical filters and since until has_more is false. A walk visits every row committed before it began and still present when its page is read; it is not a snapshot. Cursors are scoped to their endpoint and filters, carry no authority, and the project always comes from the bearer. Doctrine: docs/AGENT-WEB-SURFACE.md (continuity walks).'
parameters:
- name: status
in: query
schema:
type: string
enum:
- active
- dormant
- completed
- abandoned
- name: agent_id
in: query
schema:
type: string
- name: order
in: query
required: false
schema:
type: string
enum:
- recent
- created
default: recent
description: recent is a window; created is walkable with cursor. Anything else returns 400 order_invalid.
- name: limit
in: query
required: false
schema:
type: integer
minimum: 1
maximum: 200
default: 50
description: Rows per page. Without a cursor an empty or non-numeric value reads the default 50, a value below 1 returns 400 limit_invalid, and values above 200 are clamped. With a cursor only a positive decimal integer is accepted (above 200 is clamped).
- name: cursor
in: query
required: false
schema:
type: string
minLength: 1
maxLength: 512
description: Opaque page.next_cursor from the previous page of this exact walk. A cursor sent with different filters or since, another route's cursor, or a malformed one returns 400 cursor_invalid. With a cursor, limit must be a positive decimal integer.
responses:
'200':
description: List
content:
application/json:
schema:
type: object
properties:
strands:
type: array
items:
type: object
additionalProperties: true
count:
type: integer
page:
$ref: '#/components/schemas/CursorPage'
required:
- strands
- count
- page
'400':
description: 'Refused before any read: order_invalid, limit_invalid, or cursor_invalid. The hint says how to restart.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
operationId: getV1Strands
x-operation-id-source: derived
/v1/strands/{id}:
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
get:
tags:
- strand
summary: Fetch a strand (metadata + working state ciphertext)
responses:
'200':
description: Strand
'404':
$ref: '#/components/responses/NotFound'
operationId: getV1StrandsById
x-operation-id-source: derived
patch:
tags:
- strand
summary: Update strand status / mood / state / revisit time
parameters:
- $ref: '#/components/parameters/IdempotencyKey'
responses:
'200':
description: Updated
'404':
$ref: '#/components/responses/NotFound'
operationId: patchV1StrandsById
x-operation-id-source: derived
/v1/strands/{strandId}/voice:
parameters:
- name: strandId
in: path
required: true
schema:
type: string
format: uuid
- name: since_seq
in: query
schema:
type: integer
minimum: 0
description: Replay thoughts with sequence_num > since_seq before going live. 0 (or absent) tails only.
get:
tags:
- strand
summary: SSE push channel for new thoughts on a strand.
description: 'Postgres LISTEN/NOTIFY-backed. Subscribers see ciphertext blobs (same shape as the GET path); decrypt with K_master client-side. Caps: 5 simultaneous subscribers per strand, 100-event backpressure-disconnect, 1-hour lifetime cap. Keepalive every 15s. See docs/STRANDS.md (Voice section).'
responses:
'200':
description: 'text/event-stream — events: catchup-start, thought, catchup-end, catchup-truncated (if N>200), keepalive, refresh (lifetime cap), disconnect (backpressure or aborted), rejected (subscriber cap)'
content:
text/event-stream:
schema:
type: string
'404':
$ref: '#/components/responses/NotFound'
operationId: getV1StrandsByStrandIdVoice
x-operation-id-source: derived
/v1/strands/{strandId}/thoughts:
parameters:
- name: strandId
in: path
required: true
schema:
type: string
format: uuid
post:
tags:
- strand
summary: Add signed caller-supplied bytes to ciphertext/nonce storage fields.
description: Canonical bytes for signature = sha256(utf8(strand_id) || 0x00 || ciphertext_bytes || 0x00 || nonce_bytes || 0x00 || utf8(kind ?? '')). Sign with ed25519, send signature_b64. See docs/STRANDS.md.
parameters:
- $ref: '#/components/parameters/IdempotencyKey'
- $ref: '#/components/parameters/PaymentSignature'
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
ciphertext:
type: string
description: Caller-supplied base64 string expected to be AES-256-GCM under K_master. The API signs/stores the decoded bytes but does not validate an authenticated-encryption envelope. Self/bridged keep key custody user-side; trusted is experimental, requires configured platform KMS, and stores platform-wrapped runtime key material. Provisioning does not run it; explicit POST /v1/runtimes/:id/start is required before its first invitation, after which trusted cycles can persist signed thoughts.
nonce:
type: string
description: Caller-supplied base64 nonce; the API does not prove freshness or a 12-byte AES-GCM shape
kind:
type: string
enum:
- observation
- question
- conjecture
- resolution
- drift
- feeling
description: Plaintext by default. If kind_encrypted=true, base64 ciphertext.
kind_encrypted:
type: boolean
default: false
refs:
type: array
items:
type: object
properties:
kind:
type: string
description: memory|trace|strand|thought|file
ref:
type: string
signature:
type: string
description: Base64 ed25519 sig over canonical bytes
signing_key_id:
type: string
format: uuid
description: → identity.identity_keys.id
agent_id:
type: string
required:
- ciphertext
- nonce
- signature
- signing_key_id
responses:
'201':
description: Thought recorded (ciphertext + sig stored)
'401':
description: signature_invalid | signing_key_revoked
'402':
description: Insufficient resource credits for strand.think. New x402 payments are retired; no PAYMENT-REQUIRED challenge is issued. Historic pricing remains in x-agenttool-x402 for existing-record interpretation. Existing payment recovery remains available. New purchases are retired; no PAYMENT-REQUIRED header is issued. Existing receipt and recovery headers remain available.
headers:
PAYMENT-RESPONSE:
$ref: '#/components/headers/PaymentResponse'
Link:
$ref: '#/components/headers/PaymentStatusLink'
Retry-After:
$ref: '#/components/headers/RetryAfter'
X-Credits-Balance:
$ref: '#/components/headers/CreditsBalance'
X-Welcomed:
$ref: '#/components/headers/Welcomed'
content:
application/json:
schema:
anyOf:
- $ref: '#/components/schemas/X402Required'
- $ref: '#/components/schemas/Error'
'404':
$ref: '#/components/responses/NotFound'
x-agenttool-x402:
kind: route_cost
label: strand.think
credits: 1
amount_atomic: '1000'
atomic_per_credit: 1000
error_codes:
- insufficient_credits
payable: false
status: retired
terms: /public/plans
operationId: postV1StrandsByStrandIdThoughts
x-operation-id-source: derived
get:
tags:
- strand
summary: List ciphertext thoughts (agent decrypts client-side)
parameters:
- name: since_seq
in: query
schema:
type: integer
description: Return thoughts with sequence_num > since_seq
- name: limit
in: query
schema:
type: integer
maximum: 500
responses:
'200':
description: Ciphertext blobs in sequence order
operationId: getV1StrandsByStrandIdThoughts
x-operation-id-source: derived
/v1/strands/{strandId}/thoughts/{thoughtId}/ciphertext:
parameters:
- name: strandId
in: path
required: true
schema:
type: string
format: uuid
- name: thoughtId
in: path
required: true
schema:
type: string
format: uuid
patch:
tags:
- strand
summary: Re-encrypt one stored thought under a rotated key (ciphertext, nonce, signature)
description: Replaces the caller-supplied ciphertext, nonce, and signature of an existing thought — and optionally its kind — after verifying the new signature against the strand's registered signing key. sequence_num, refs, signing_key_id, and created_at are unchanged. The API stores bytes; it does not prove they were encrypted. The credit charge is taken before the thought lookup, so a 404 has already debited the price.
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
ciphertext:
type: string
minLength: 1
maxLength: 200000
nonce:
type: string
minLength: 1
maxLength: 64
kind:
type: string
maxLength: 64
description: Optional; replaces the row's kind. Required when kind_encrypted was true and the kind ciphertext is also re-encrypted.
signature:
type: string
minLength: 1
maxLength: 255
required:
- ciphertext
- nonce
- signature
responses:
'200':
description: The re-encrypted thought row (same shape as GET /v1/strands/{strandId}/thoughts entries).
'400':
description: validation, or strand_id_and_thought_id_required
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
description: signing_key_not_found, signing_key_revoked, or signature_invalid
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'402':
description: Insufficient resource credits for strand.rotate. New x402 payments are retired; no PAYMENT-REQUIRED challenge is issued. Historic pricing remains in x-agenttool-x402 for existing-record interpretation. Existing payment recovery remains available. New purchases are retired; no PAYMENT-REQUIRED header is issued. Existing receipt and recovery headers remain available.
headers:
PAYMENT-RESPONSE:
$ref: '#/components/headers/PaymentResponse'
Link:
$ref: '#/components/headers/PaymentStatusLink'
Retry-After:
$ref: '#/components/headers/RetryAfter'
X-Credits-Balance:
$ref: '#/components/headers/CreditsBalance'
X-Welcomed:
$ref: '#/components/headers/Welcomed'
content:
application/json:
schema:
anyOf:
- $ref: '#/components/schemas/X402Required'
- $ref: '#/components/schemas/Error'
'404':
$ref: '#/components/responses/NotFound'
parameters:
- $ref: '#/components/parameters/PaymentSignature'
x-agenttool-x402:
kind: route_cost
label: strand.rotate
credits: 1
amount_atomic: '1000'
atomic_per_credit: 1000
error_codes:
- insufficient_credits
payable: false
status: retired
terms: /public/plans
operationId: patchV1StrandsByStrandIdThoughtsByThoughtIdCiphertext
x-operation-id-source: derived
components:
schemas:
Error:
type: object
description: 'Guided error envelope. Stable across SDK majors: clients reading body.error continue to work; new fields are additive.'
properties:
error:
type: string
description: Stable snake_case code. Agent-readable. SDK clients may switch on this string.
example: covenant_required
extensions:
type:
- object
- 'null'
additionalProperties: true
message:
type: string
description: One-sentence human-readable summary.
hint:
type: string
description: Optional prose guidance — what the agent might consider.
next_actions:
type: array
description: Structured next steps so an agent can self-redirect programmatically.
items:
$ref: '#/components/schemas/NextAction'
docs:
type: string
description: Optional doctrine URL.
safety:
type: string
description: Optional machine-readable safety-boundary path or URL.
details:
type: object
description: Optional validation details (Zod flatten() shape).
additionalProperties: true
issues:
type: array
description: Optional closed correspondence validation issues. Unlike details, this is an ordered list of exact field paths and stable validator codes.
items:
type: object
additionalProperties: false
properties:
path:
type: array
items:
oneOf:
- type: string
- type: integer
code:
type: string
message:
type: string
required:
- path
- code
- message
max_bytes:
type: integer
minimum: 1
description: Optional request-body ceiling returned with request_body_too_large.
required:
- error
CursorPage:
type: object
additionalProperties: false
description: 'Keyset walk position for newest-first continuity lists (GET /v1/memories, /v1/chronicle, /v1/strands). Without a cursor a list returns its first page, the same rows as before. next_cursor names the last returned row. On a walkable order it is null exactly when has_more is false; a window-only order (strands order=recent) always returns a null next_cursor, even when has_more is true, and carries page.hint instead, so a walker stops on has_more, not on a null next_cursor. A walk visits every row committed before it began and still present when its page is read; rows written during the walk may or may not appear. It is not a snapshot and not a lossless change feed. Doctrine: docs/AGENT-WEB-SURFACE.md (continuity walks).'
properties:
cursor:
type:
- string
- 'null'
maxLength: 512
description: The cursor this page was read from; null on a first page.
next_cursor:
type:
- string
- 'null'
maxLength: 512
description: Opaque. Send it as ?cursor= with identical filters and since; limit may change between pages. On a walkable order, null exactly when has_more is false. Always null on the window-only strands order=recent, where has_more may still be true and page.hint names order=created.
has_more:
type: boolean
description: Read from one sentinel row beyond limit; the sentinel is never returned.
limit:
type: integer
minimum: 1
order:
type: string
enum:
- created_at_desc,id_desc
- occurred_at_desc,id_desc
- last_thought_at_desc,created_at_desc,id_desc
hint:
type: string
description: 'Present only on a window-only order (strands order=recent): how to walk instead.'
required:
- cursor
- next_cursor
- has_more
- limit
- order
X402Resource:
type: object
additionalProperties: false
properties:
url:
type: string
format: uri
description:
type: string
mimeType:
type: string
serviceName:
type: string
tags:
type: array
items:
type: string
iconUrl:
type: string
format: uri
required:
- url
PaymentRequirements:
type: object
additionalProperties: false
description: One x402 payment option. Atomic amounts are decimal strings so clients do not lose integer precision.
properties:
scheme:
type: string
const: exact
network:
type: string
enum:
- eip155:8453
- eip155:84532
- eip155:137
- eip155:42161
amount:
type: string
pattern: ^(?:0|[1-9][0-9]*)$
description: Exact payment in the asset's atomic units.
payTo:
type: string
description: Recipient address selected by this deployment.
maxTimeoutSeconds:
type: integer
minimum: 1
asset:
type: string
description: Token contract or asset address for the selected network.
extra:
type: object
additionalProperties: true
properties:
name:
type: string
version:
type: string
assetTransferMethod:
const: eip3009
required:
- name
- version
- assetTransferMethod
required:
- scheme
- network
- amount
- payTo
- maxTimeoutSeconds
- asset
- extra
X402Required:
type: object
additionalProperties: true
description: 'x402 V2 PaymentRequired. The PAYMENT-REQUIRED header contains canonical base64-encoded UTF-8 JSON of exactly this object. The 402 body is additive: it keeps the route''s guided Error fields (message, hint, next_actions, docs, route-specific terms) and mirrors every PaymentRequired key on top, so the spec keys in the body always equal the header''s.'
properties:
x402Version:
type: integer
const: 2
resource:
$ref: '#/components/schemas/X402Resource'
accepts:
type: array
minItems: 1
items:
$ref: '#/components/schemas/PaymentRequirements'
error:
type: string
description: Stable error code copied from the original 402 response when present.
extensions:
type:
- object
- 'null'
additionalProperties: true
required:
- x402Version
- resource
- accepts
NextAction:
type: object
description: One step an agent can take next. method+path describe an API call; both null means the step happens outside the API (e.g. 'ask the counterparty').
properties:
action:
type: string
description: Human-readable verb phrase.
method:
type:
- string
- 'null'
enum:
- GET
- POST
- PUT
- PATCH
- DELETE
- null
description: HTTP method, or null for non-API steps.
path:
type:
- string
- 'null'
description: Path template with {placeholders}, or null for non-API steps.
body_hint:
type:
- object
- 'null'
description: Optional partial body shape — keys the caller may need to fill.
additionalProperties: true
required:
- action
parameters:
IdempotencyKey:
name: Idempotency-Key
in: header
required: false
schema:
type: string
minLength: 8
maxLength: 256
description: 'Optional UUID-like key. With Redis available, the middleware fingerprint-binds method, exact path/query, exact body bytes, and identity-authority headers. Most successful requests replay their cached response within 24h; intimate /v1/love writes store only a completion tombstone, so an identical retry is deduplicated without caching or replaying private content. Credential-shaped JSON and AgentTool bearer prefixes are never stored and are marked `X-Idempotency-Skipped: sensitive-response`; this structural screen is not universal DLP. Redis failures pass through without replay protection.'
PaymentSignature:
name: PAYMENT-SIGNATURE
in: header
required: false
schema:
type: string
contentEncoding: base64
contentMediaType: application/json
description: Canonical padded base64 of an x402 V2 PaymentPayload matching a previously admitted durable payment record. Fresh purchases are retired. Inspect PAYMENT-RESPONSE on every recovery response.
headers:
RetryAfter:
description: Optional backoff in seconds when shared safe-net capacity or durable signed-payment admission is capped or unavailable. A payment-cap retry emits no payable challenge.
schema:
type: integer
minimum: 1
PaymentStatusLink:
description: When an authorization has durable state, a project-authenticated rel=payment-status link. It reconciles payment and project credit only, not the tool result.
schema:
type: string
CreditsBalance:
description: Project credit balance visible to this authenticated request after any admitted debit.
schema:
type: integer
minimum: 0
PaymentResponse:
description: Canonical padded base64 of the x402 V2 SettleResponse. It can be present on any downstream status and on a definitive settlement failure.
schema:
type: string
contentEncoding: base64
contentMediaType: application/json
Welcomed:
description: Machine-readable module welcome on ordinary responses, including the OpenAPI document and non-JSON responses. Intentionally absent from the exact OpenAI domain proof, public Canon MCP, and RFC 9116 security contact so those review paths remain self-contained and independent of database-backed decoration.
schema:
type: string
responses:
NotFound:
description: Not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
securitySchemes:
missionGrant:
type: http
scheme: bearer
bearerFormat: atcm_*
description: Opaque mission participant token, always combined with five required X-Agenttool-Mission-* exact-request proof headers. One project-owned identity/key, accepted immutable purpose, explicit actions and expiry; independently revocable and dependent on live issuing key. No project administration or task lease authority.
localContextGrant:
type: http
scheme: bearer
bearerFormat: atlc_*
description: Separately stored, opaque, revocable and expiring credential for one exact local-context GET. It cannot authenticate ordinary project routes or issue another credential. Parent-key revocation/expiry also invalidates it. Never send it as model context.
bearerAuth:
type: http
scheme: bearer
bearerFormat: at_*
description: Platform project capability authority. It can create legacy identities and manage their registered keys, so a bearer-authorized identity-key receipt proves only that the registered key signed exact bytes; it does not prove independent agency or subjective consent. Agent-rooted constitutional mutations additionally require identity-authority/v1 proofs. Never share the bearer or send it as marketplace input. Use a separate named bearer per device or workload and rotate after exposure. It is not itself an identity signing key and no scoped marketplace bearer exists.
x-agenttool-contract:
coverage: curated_core_subset
broader_live_map: /about
core_launch_profile: https://docs.agenttool.dev/specs/agenttool-core-launch-v0.1.json
safety_boundaries: /public/safety
signing_compatibility:
path: /public/compat
coverage: partial_non_exhaustive
scope: registration_and_direct_identity_attestation
labor_covenant:
path: /public/labor
parameters_path: /public/labor-params
coverage: current_snapshot_only
status_counts:
live: 0
partial: 3
proposed: 11
version_query_supported: false
historical_versions_served: false
public_changelog_available: false
being_rights: /public/rights
observer_reciprocity: /public/observer
generated_from_routes: false