ELYDORA
Quick Start

Integrate Elydora.
Step by step.

The guide to integrating Elydora into an AI agent platform: key generation, registration, signing, submission, verification, and operations.

01. Getting Started

What is Elydora.

Elydora is the responsibility layer for AI agents. It produces verifiable, tamper-evident operation records for the actions your agents perform. Every operation is signed, chained, receipted, and sealed into time-bounded epochs.

When you need responsibility records

Use Elydora when agents perform actions with legal, financial, or compliance consequences: approving a loan, executing a trade, authorizing a payment, changing infrastructure. Regulators, auditors, and courts ask who decided, when, under what authority, and whether it can be proven. Without Elydora, you have logs. With it, you have evidence.

How it works in 30 seconds

Your agent signs an operation record with its Ed25519 private key, producing an Elydora Operation Record (EOR). The agent submits the EOR to the Elydora API. Elydora validates the signature, checks chain integrity, assigns a sequence number, and returns an Elydora Acknowledgment Receipt (EAR) signed by the platform. Every 5 minutes, accepted operations are sealed into an epoch through a Merkle tree, producing an Elydora Epoch Record (EER).

The core loop
Agent signs with Ed25519
Submit to the Elydora API
Receive attestation receipt
Operation sealed into epoch
02. System Requirements

Before you begin.

Ensure your environment meets these requirements before installing the SDK.

RuntimeNode.js 18+ / Python 3.10+ / Go 1.21+
AccountAn Elydora organization account (org_id)
KeysEd25519 key pair for each agent (base64url-encoded 32-byte seed)
NetworkHTTPS access to api.elydora.com (port 443)
ClockSystem clock synchronized via NTP (required for TTL validation)
Note

The Ed25519 private key is a base64url-encoded 32-byte seed, not PEM. Generate it with OpenSSL, or let the Console generate it when you create the agent. Each agent needs its own key pair. Never share private keys between agents.

03. Installing the SDK

Install the SDK.

Elydora provides official SDKs for Node.js, Python, and Go. Each SDK wraps the Elydora API and handles Ed25519 signing, chain hash tracking, and nonce generation.

Node.js
$ npm install @elydora/sdk
# or: yarn add @elydora/sdk

Requires Node.js 18 or later. The SDK uses the native crypto module for Ed25519 operations; no native dependencies are required.

Python
$ pip install elydora
# or: poetry add elydora

Requires Python 3.10 or later. Uses the cryptography package for Ed25519 operations, installed as a dependency.

Go
$ go get github.com/Elydora-Infrastructure/Elydora-Go-SDK/v2

Requires Go 1.21 or later. Uses the standard library crypto/ed25519 package; no external dependencies for cryptographic operations.

04. Creating an Agent Identity

Register your agent.

Every agent in Elydora has a unique cryptographic identity. Before an agent can submit operations, it must be registered with your organization. Registration binds an agent_id to one or more Ed25519 public keys, a display name, the responsible legal entity, and an integration type. This identity answers the question: who performed this action?

Step 1: Generate an Ed25519 key pair

Each agent needs its own Ed25519 key pair. The private key is a 32-byte seed, base64url-encoded, and signs operations. The SDK derives the public key from the seed for registration. Never share private keys between agents, and never hardcode them in source code.

# Generate a 32-byte Ed25519 seed, base64url-encoded
openssl genpkey -algorithm Ed25519 -outform DER | \
  tail -c 32 | basenc --base64url -w0 | tr -d '='

# Derive the public key from the seed
# Node.js
import { derivePublicKey } from '@elydora/sdk';
const publicKey = derivePublicKey(process.env.ELYDORA_KEY);

# Python
from elydora import get_public_key_base64url
public_key = get_public_key_base64url(os.environ["ELYDORA_KEY"])

# Go
seed, err := base64.RawURLEncoding.DecodeString(os.Getenv("ELYDORA_KEY"))
if err != nil {
    log.Fatal(err)
}
pub := ed25519.NewKeyFromSeed(seed).Public().(ed25519.PublicKey)
publicKey := base64.RawURLEncoding.EncodeToString(pub)

Step 2: Initialize the client and register

Create a client with your organization ID, agent ID, and private key. Set an API token issued from the Console or the token endpoint, then register the agent. Registration happens once; the agent can submit operations immediately afterwards.

Node.js
import { ElydoraClient } from '@elydora/sdk';

const client = new ElydoraClient({
  orgId: 'org_acme',
  agentId: 'agent_underwriter',
  privateKey: process.env.ELYDORA_KEY!,
});
client.setToken(process.env.ELYDORA_TOKEN!);

const { agent } = await client.registerAgent({
  agent_id: 'agent_underwriter',
  display_name: 'Loan Underwriter Agent',
  responsible_entity: 'ACME Financial Corp',
  integration_type: 'sdk',
  keys: [{
    kid: 'agent_underwriter-key-1',
    public_key: client.getPublicKey(),
    algorithm: 'ed25519',
  }],
});
console.log(agent.status); // active
Python
import os
from elydora import ElydoraClient, get_public_key_base64url

client = ElydoraClient(
    org_id="org_acme",
    agent_id="agent_underwriter",
    private_key=os.environ["ELYDORA_KEY"],
    token=os.environ["ELYDORA_TOKEN"],
)

response = client.register_agent({
    "agent_id": "agent_underwriter",
    "display_name": "Loan Underwriter Agent",
    "responsible_entity": "ACME Financial Corp",
    "integration_type": "sdk",
    "keys": [{
        "kid": "agent_underwriter-key-1",
        "public_key": get_public_key_base64url(os.environ["ELYDORA_KEY"]),
        "algorithm": "ed25519",
    }],
})
print(response["agent"]["status"])  # active
Go
import (
    "log"
    "os"

    elydora "github.com/Elydora-Infrastructure/Elydora-Go-SDK/v2"
)

client, err := elydora.NewClient(&elydora.Config{
    OrgID:      "org_acme",
    AgentID:    "agent_underwriter",
    PrivateKey: os.Getenv("ELYDORA_KEY"),
    Token:      os.Getenv("ELYDORA_TOKEN"),
})
if err != nil {
    log.Fatal(err)
}

// publicKey: derived in step 1
resp, err := client.RegisterAgent(&elydora.RegisterAgentRequest{
    AgentID:           "agent_underwriter",
    DisplayName:       "Loan Underwriter Agent",
    ResponsibleEntity: "ACME Financial Corp",
    IntegrationType:   elydora.IntegrationTypeSDK,
    Keys: []elydora.RegisterAgentKeyInput{{
        KID:       "agent_underwriter-key-1",
        PublicKey: publicKey,
        Algorithm: "ed25519",
    }},
})
if err != nil {
    log.Fatal(err)
}
log.Println(resp.Agent.Status) // active

Step 3: Store keys securely

The private key is the root of your agent's identity. Compromise of the private key means an attacker can forge operations that appear to come from your agent. Follow these storage practices:

DevelopmentEnvironment variables via .env files (never committed to version control)
StagingCloud-native secret managers (AWS Secrets Manager, GCP Secret Manager, Azure Key Vault)
ProductionHardware Security Modules (HSMs) or cloud KMS with key wrapping for maximum security
05. Signing an Operation

Create an EOR.

An Elydora Operation Record (EOR) is the unit of accountability. It captures what happened, who did it, and when, and proves it cryptographically. The SDK handles the signing mechanics. You provide the business context, and the SDK produces a signed, chain-linked EOR ready for submission.

What happens under the hood

When you call createOperation(), the SDK performs these steps in sequence. Knowing this process helps when debugging signature or chain issues.

1Generate a UUIDv7 for the operation_id, which embeds a timestamp for natural ordering
2Generate a cryptographically random 16-byte nonce, base64url-encoded, to prevent replay attacks
3Compute payload_hash = SHA-256(JCS(payload)); the payload is JSON Canonicalized (RFC 8785) before hashing for deterministic results
4Compute chain_hash = SHA-256(prev_chain_hash | payload_hash | operation_id | issued_at), pipe-delimited, linking this operation to the previous one in the agent's chain; the SDK keeps this value as the next prev_chain_hash
5Build the EOR without the signature: ids, issued_at, ttl_ms, nonce, operation_type, subject, action, payload, payload_hash, prev_chain_hash, and agent_pubkey_kid
6JCS-canonicalize the entire EOR to produce a deterministic byte representation
7Sign the canonical bytes with the agent's Ed25519 private key
8Attach the base64url-encoded signature to the EOR

Creating an operation

createOperation() is synchronous. It returns the signed EOR directly, not a Promise. All cryptographic work happens locally. Nothing is sent to Elydora until you call submitOperation().

// createOperation() is synchronous and returns the signed EOR
const record = client.createOperation({
  operationType: 'loan.approve',
  subject: {
    system: 'lending',
    resource: 'application',
    id: 'APP-2026-001',
  },
  action: {
    decision: 'approve',
    amount: 50000,
    currency: 'USD',
  },
  payload: {
    applicant_id: 'cust_123',
    credit_score: 750,
    debt_to_income: 0.32,
    loan_term_months: 360,
    interest_rate: 6.25,
    underwriting_model: 'model_v3.2.1',
  },
});

// record.operation_id     UUIDv7
// record.issued_at        Unix milliseconds
// record.nonce            16 random bytes, base64url
// record.payload_hash     SHA-256 of JCS(payload)
// record.prev_chain_hash  link to the previous operation
// record.signature        Ed25519 over the canonical record
Important

The subject field identifies what the operation acts upon. Use one naming convention across your organization: system identifies the business domain, resource identifies the entity type, and id is the specific resource identifier. This structure enables audit queries across your operation history.

06. Submitting to Elydora Edge

Submit and receive a receipt.

Submit a signed EOR to the Elydora API. The API validates the signature, checks chain integrity, assigns a sequence number, and returns an acknowledgment receipt.

// submitOperation() is async
const { receipt } = await client.submitOperation(record);

console.log('Receipt ID:', receipt.receipt_id);
console.log('Chain Hash:', receipt.chain_hash);
console.log('Sequence:', receipt.seq_no);
console.log('Received at:', receipt.server_received_at);
console.log('Signature:', receipt.elydora_signature);

The API responds with HTTP 202 Accepted after validation. The body contains the Elydora Acknowledgment Receipt (EAR): a platform-signed proof that Elydora accepted, validated, and sequenced the operation. This receipt is your agent's proof of submission.

What the receipt contains

receipt_idUnique identifier for this receipt (UUIDv7)
operation_idThe operation this receipt acknowledges
chain_hashThe chain hash of this operation; the SDK already holds it as the next prev_chain_hash
seq_noMonotonically increasing sequence number for this agent's chain
server_received_atServer-side Unix epoch milliseconds timestamp of acceptance
elydora_signatureEd25519 signature from the Elydora platform over receipt_hash, verifiable against the public JWKS
Tip

Store the receipt with the original operation. In a dispute you need both the EOR (what was submitted) and the EAR (proof of acceptance). The SDK tracks the chain hash in memory; persisting receipts is your responsibility.

07. Receiving & Storing Attestation Receipts

The EAR structure.

The Elydora Acknowledgment Receipt (EAR) is the platform's cryptographic attestation that an operation was received, validated, and sequenced. It is a signed proof of acceptance that any party with Elydora's public keys can verify.

// The EAR returned from submitOperation()
{
  receipt_version: "1.0",
  receipt_id: "019462a0-7e8b-7c3d-a1b2-3c4d5e6f7890",
  operation_id: "019462a0-7e8a-7b2c-9a8b-7c6d5e4f3210",
  org_id: "org_acme",
  agent_id: "agent_underwriter",
  server_received_at: 1740835800000,
  seq_no: 42,
  chain_hash: "base64url-encoded-sha256-hash",
  queue_message_id: "msg_01HQ4R5S6T7U8V9W0X1Y2Z3A",
  receipt_hash: "base64url-encoded-sha256-hash",
  elydora_kid: "elydora-server-key-v1",
  elydora_signature: "base64url-encoded-ed25519-signature"
}

Parsing and storing receipts

The SDK returns the receipt as a typed object. Persist it in durable storage next to the operation so the audit trail can be reconstructed at any time. An append-only table or immutable storage prevents accidental modification.

// Store the receipt alongside the operation
const { receipt } = await client.submitOperation(record);

await db.operations.insert({
  operation_id: record.operation_id,
  eor: record,   // the signed operation record
  ear: receipt,  // the platform acknowledgment receipt
  created_at: new Date(),
});

Verifying receipt authenticity

Every receipt is signed by the Elydora platform with its own Ed25519 key. receipt_hash is the SHA-256 of the canonical receipt fields, and elydora_signature covers the UTF-8 bytes of receipt_hash. Verify the signature against the public JWKS at /.well-known/elydora/jwks.json. The verify endpoint recomputes the receipt hash from stored evidence but does not check the platform signature.

// Server-side check of signature, chain, and receipt hash
const verification = await client.verifyOperation(record.operation_id);
console.log(verification.checks.receipt);

// Independent check: recompute the hash, then verify the signature
import { createPublicKey, verify } from 'node:crypto';
import { jcsCanonicalise, sha256Base64url } from '@elydora/sdk';

const { receipt_hash, elydora_kid, elydora_signature, ...fields } = receipt;
const hashMatches = sha256Base64url(jcsCanonicalise(fields)) === receipt_hash;

const { keys } = await client.getJWKS();
const jwk = keys.find((key) => key.kid === elydora_kid);
if (!jwk?.x) throw new Error(`Unknown Elydora key ${elydora_kid}`);
const signatureValid = verify(
  null,
  Buffer.from(receipt_hash, 'utf-8'),
  createPublicKey({ key: { kty: 'OKP', crv: 'Ed25519', x: jwk.x }, format: 'jwk' }),
  Buffer.from(elydora_signature, 'base64url'),
);
const authentic = hashMatches && signatureValid;
08. Verifying Records

Four layers of verification.

Elydora provides four verification checks for every operation. Each validates a different aspect of integrity; together they prove a record is authentic, correctly sequenced, and anchored.

// verifyOperation() is async
const verification = await client.verifyOperation(record.operation_id);

console.log('Valid:', verification.valid);
console.log('Signature OK:', verification.checks.signature);
console.log('Chain OK:', verification.checks.chain);
console.log('Receipt OK:', verification.checks.receipt);
console.log('Merkle OK:', verification.checks.merkle); // absent until the epoch closes
Signature Check
checks.signature

Verifies the Ed25519 signature on the EOR against the agent's registered public key. Confirms the operation was genuinely created by the claimed agent and has not been modified since signing.

Chain Check
checks.chain

Verifies the chain_hash linkage: this operation's prev_chain_hash references the predecessor operation. Confirms the sequential integrity of the agent's operation chain.

Receipt Check
checks.receipt

Recomputes receipt_hash from the stored EAR fields and compares it with the receipt. Confirms the receipt was issued for this operation and has not been modified.

Merkle Check
checks.merkle

Verifies the operation's inclusion in its epoch's Merkle tree with an inclusion proof. Confirms the operation was sealed into a specific epoch and anchored to the epoch root hash.

verification.valid is true only when all checks pass. Signature, chain, and receipt checks are available right after submission. The Merkle check is available once the epoch that covers the operation closes, within 5 minutes. Before then, checks.merkle is absent rather than false.

09. Handling Errors & Conflicts

Error codes and recovery.

The Elydora API uses structured error responses. Every error thrown by the SDK is an ElydoraError with typed properties: statusCode (HTTP status), code (error code string), message, requestId (for support), and details (additional context).

INVALID_SIGNATURE

The Ed25519 signature on the submitted EOR does not verify against the agent's registered public key. The cause is a key mismatch between the signing key and the registered key, or an EOR modified after signing.

Handling: Verify that the private key used to sign matches the public key registered for this agent. Re-sign the operation and retry.

UNKNOWN_AGENT

The agent_id in the submitted operation does not match any registered agent within the specified organization. Registration must be completed before submitting operations.

Handling: Register the agent using client.registerAgent() before submitting operations. Double-check the org_id and agent_id values.

KEY_REVOKED

The signing key (identified by kid) has been revoked. Revocation is permanent; the key can never sign operations again.

Handling: Switch to another key registered for the agent with the kid option, or register a new agent identity with a fresh key pair.

AGENT_FROZEN

The agent has been frozen by an administrator. Frozen agents cannot submit new operations. The state is reversible; an administrator can unfreeze the agent.

Handling: Contact your organization administrator to review the freeze reason. Once unfrozen, the agent can resume normal operations.

TTL_EXPIRED

The operation's issued_at plus ttl_ms is in the past. The SDK default TTL is 30 seconds; the API accepts up to 300 seconds. This prevents replay of stale operations.

Handling: Regenerate the operation with a fresh timestamp. Ensure your system clock is synchronized via NTP.

REPLAY_DETECTED

The nonce in this operation has already been seen. Each operation must include a unique, cryptographically random 16-byte nonce. This check prevents duplicate submission of the same operation.

Handling: Ensure nonces come from a cryptographically secure random generator. The SDK handles this; if you see this error, you are resubmitting a cached operation object.

PREV_HASH_MISMATCH

The prev_chain_hash in the submitted operation does not match the server's chain head for this agent. This occurs when local chain state is out of sync: after a restart, a rejected submission, or a concurrent submission from another instance. The error message names the expected hash.

Handling: Read the expected hash from the error message and resync the local chain head, then rebuild and resubmit. Python: set_prev_chain_hash(). Go: pass the last accepted chain_hash as PrevChainHash on the next CreateOperation. Node.js: no setter yet, so keep one long-lived client per agent. The hook runtimes resync automatically.

PAYLOAD_TOO_LARGE

The operation payload exceeds the maximum size of 256 KB, measured on the serialized payload before hashing.

Handling: Reduce the payload size. Store large data externally and include only a reference (URL or hash) in the operation payload.

RATE_LIMITED

The organization has exceeded the allowed submission rate. The response carries a Retry-After header. The Node.js SDK retries 429 responses honoring Retry-After; Go retries on a fixed schedule; Python raises immediately.

Handling: Queue the operation and retry after the delay. Consider distributing load across several agents.

Error handling in code

Use the ElydoraError class to handle specific error codes. The code property is the machine-readable type and message the human-readable explanation. Include requestId when contacting Elydora support. A rejected submission leaves the local chain one step ahead of the server, so the next submission fails with PREV_HASH_MISMATCH until the chain head is resynced.

import { ElydoraError } from '@elydora/sdk';

try {
  const { receipt } = await client.submitOperation(record);
  console.log('Submitted:', receipt.receipt_id);
} catch (err) {
  if (!(err instanceof ElydoraError)) throw err;
  console.error(`[${err.code}] ${err.message} (request ${err.requestId})`);

  switch (err.code) {
    case 'PREV_HASH_MISMATCH': {
      // The message names the server's chain head
      const expected = /Expected prev_chain_hash "([^"]+)"/.exec(err.message)?.[1];
      await alertOperator('Chain out of sync', { expected });
      break;
    }

    case 'AGENT_FROZEN':
    case 'KEY_REVOKED':
      await alertOperator(err.code, err.details);
      break;

    case 'RATE_LIMITED':
      // The SDK already retried with Retry-After up to maxRetries times
      await queueForLater(record);
      break;

    default:
      throw err;
  }
}
10. Epoch & Batch Sealing

How epochs work.

Epochs are time-bounded batches of operations sealed into a single Merkle tree. They give a compact, verifiable summary of all operations in a window and enable inclusion checks.

Elydora seals epochs on 5-minute windows. All operations accepted in the window are hashed into a SHA-256 Merkle tree. The root is stored in the Elydora Epoch Record (EER), a single hash that commits to every operation in the epoch. The epoch root can be anchored to an RFC 3161 timestamping authority for independent proof of time.

Merkle tree construction

Leaves are the chain hashes of the operations in the window, sorted ascending. Interior nodes are SHA-256(left || right); an odd layer duplicates its last node. The verify endpoint builds the inclusion proof from the stored tree, so an auditor confirms one operation with O(log n) hashes.

Retrieving epoch data

// List recent epochs
const { epochs } = await client.listEpochs();

// Get one epoch and its RFC 3161 anchor when present
const { epoch, anchor } = await client.getEpoch(epochs[0].epoch_id);

console.log('Epoch ID:', epoch.epoch_id);
console.log('Window:', epoch.start_time, epoch.end_time);
console.log('Root hash:', epoch.root_hash);
console.log('Leaf count:', epoch.leaf_count);
console.log('Anchored:', anchor !== undefined);

// Merkle inclusion is reported per operation by verifyOperation()
const { checks } = await client.verifyOperation(record.operation_id);
console.log('Included:', checks.merkle);
Note

Epoch sealing is asynchronous. An operation is included in a sealed epoch within 5 minutes of acceptance. Until then the receipt is the proof of acceptance; the epoch adds batch-level anchoring.

11. Freezing & Revoking Agents

Agent lifecycle management.

Elydora provides two controls over agent access: freezing (reversible) and key revocation (permanent). The API enforces both on the next submission; hook guards installed in coding tools refresh status within 60 seconds.

Freeze (Reversible)

Freezing an agent stops new operations at the API. The agent's history remains intact and verifiable. An administrator can unfreeze the agent to restore normal operation. Use freezing to suspend an agent while investigating an issue.

active → frozen → active
Revoke (Permanent)

Revoking a key permanently invalidates it. The revocation is recorded as an admin event with the reason and timestamp. Operations signed with the revoked key are rejected with KEY_REVOKED; the agent continues with another key registered for it.

active → revoked (permanent)
// Freeze an agent (reversible)
await client.freezeAgent(
  'agent_underwriter',
  'Suspicious activity detected, pending investigation'
);

// Unfreeze an agent (restore normal operation)
await client.unfreezeAgent(
  'agent_underwriter',
  'Investigation complete, no issues found'
);

// Revoke one key (permanent)
await client.revokeKey(
  'agent_underwriter',
  'agent_underwriter-key-1',
  'Key compromised'
);
Python
# Continue on the same chain with another registered key
client.set_kid("agent_underwriter-key-2")
client.private_key = os.environ["ELYDORA_SPARE_KEY"]

# Go: pass KID and PrevChainHash (the last accepted chain_hash) to CreateOperation.
# Node.js: the client starts from the genesis chain head, so it cannot
# switch keys on a live chain today.
Warning

Key revocation is permanent. Keys are added only at registration, so register a spare key before you need it. Operations signed before the revocation remain valid and verifiable.

12. Security Best Practices

Protect the chain.

The integrity of your accountability chain depends on these practices. A compromised private key lets an attacker forge operations that appear legitimate. A broken chain hash stops operations from being sequenced.

Never hardcode private keys

Private keys must not appear in source code, committed configuration, Docker images, or build artifacts. Use environment variables in development and a secret manager (AWS Secrets Manager, GCP Secret Manager, Azure Key Vault) in production.

Register a spare key

Keys are registered with the agent at creation. Register two keys, sign with one, and switch to the other with the kid option after a revocation. Adding keys after registration requires a new agent identity.

Monitor chain hash continuity

The chain hash orders each agent's operations. If it desynchronizes (concurrent submissions, restarts without persisted state, or a rejected submission), you get PREV_HASH_MISMATCH errors. Monitor them; they can also indicate unauthorized use of an agent's credentials.

Set up alerts for agent freezes

Alert your security team when any agent is frozen. A freeze can indicate suspicious activity, a compliance issue, or an unauthorized access attempt. Review the frozen agent's recent operation history for anomalies.

Use short TTLs in production

The TTL window is how long an operation stays valid before submission. The SDK default is 30 seconds (ttlMs); the API rejects values above 300 seconds. Raise it only for debugging.

Validate receipts immediately

After submitting, compare the receipt's operation_id with the record you sent and its chain_hash with client.getChainHash(), then store the receipt. Call verifyOperation() for a full server-side check.

Isolate agent keys per environment

Never use the same private key across development, staging, and production. Each environment gets its own agent registrations with unique key pairs. This prevents cross-environment operations and limits the blast radius of a key compromise.

13. Performance & Scaling

Optimize for throughput.

Elydora runs on Cloudflare Workers and scales across agents and organizations. These limits and tips keep submission fast.

30 s
Default operation TTL
256 KB
Maximum payload size
5 min
Epoch sealing window
Keep payloads lean

Payload size affects serialization, hashing, and transfer time. Store large data (documents, images, raw datasets) externally and include references or content hashes in the payload. Keep payloads under 10 KB for high-frequency operations.

Track the chain head

The Node.js and Python clients keep prev_chain_hash in memory and advance it on every createOperation(); a new process starts from the genesis chain hash, so keep one long-lived client per agent. The Go client takes PrevChainHash on every CreateOperation: persist the chain_hash from each accepted receipt and pass it to the next call.

Submit in order

createOperation() is synchronous and CPU-bound; submitOperation() is async and IO-bound. Submit each operation before creating the next: a failed submission leaves the local chain ahead of the server, and the next submission fails with PREV_HASH_MISMATCH.

Partition by agent

Each agent has its own chain and sequence. For high throughput, split work across several logical agents, each with its own key pair and client, instead of pushing one chain harder.

Monitor submission latency

Track the time between createOperation() and the receipt in your own metrics. Latency spikes point to network issues or rate limiting.

14. Integration Patterns

Common architectures.

Elydora fits several agent architectures. Below are four common patterns, each suited to a different operational profile. Most production deployments combine elements from more than one.

Pattern 01
API call wrapping

The most common pattern. Wrap each outbound action your agent takes with an Elydora operation record: third-party API calls, database mutations, payment processing. The operation is created and submitted as part of the action flow, so the audit trail stays consistent with the behavior.

// Wrap an external API call with an Elydora operation
async function approvePayment(paymentId: string, amount: number) {
  // 1. Record the decision before executing it
  const record = client.createOperation({
    operationType: 'payment.approve',
    subject: { system: 'payments', resource: 'payment', id: paymentId },
    action: { decision: 'approve', amount, currency: 'USD' },
    payload: { payment_id: paymentId, amount, approved_by: 'agent_underwriter' },
  });

  // 2. Submit to Elydora (proof of intent)
  const { receipt } = await client.submitOperation(record);

  // 3. Execute the payment
  const result = await paymentGateway.charge(paymentId, amount);

  // 4. Record the outcome
  const outcomeRecord = client.createOperation({
    operationType: 'payment.executed',
    subject: { system: 'payments', resource: 'payment', id: paymentId },
    action: { status: result.status, transaction_id: result.txId },
    payload: { receipt_id: receipt.receipt_id, gateway_response: result },
  });
  await client.submitOperation(outcomeRecord);

  return result;
}
Pattern 02
Background job auditing

For agents that run as background workers processing queues or scheduled jobs. Each job execution is wrapped in an Elydora operation, recording what the agent processed, what it decided, and what it produced. Suited to batch pipelines, cron-based agents, and event-driven architectures.

// Background job worker with Elydora auditing
async function processJob(job: QueueJob) {
  const record = client.createOperation({
    operationType: 'batch.process',
    subject: { system: 'pipeline', resource: 'job', id: job.id },
    action: { type: job.type, batch_size: job.items.length },
    payload: {
      job_id: job.id,
      queue: job.queue,
      items_count: job.items.length,
      started_at: new Date().toISOString(),
    },
  });

  const { receipt } = await client.submitOperation(record);
  const result = await executeJob(job);

  return { receipt, result };
}
Pattern 03
High-frequency operations

For agents making many decisions per second (trading, real-time fraud detection, content moderation). Keep one client per agent and submit sequentially; the chain forbids reordering. Partition operations across several logical agents, each with its own chain, to raise throughput.

// One client per logical agent; each agent owns its chain
const scorers = ['agent_fraud_a', 'agent_fraud_b'].map((agentId) => {
  const client = new ElydoraClient({ orgId: 'org_acme', agentId, privateKey: keyFor(agentId) });
  client.setToken(process.env.ELYDORA_TOKEN!);
  return client;
});

// Submit sequentially within an agent: the chain forbids reordering
async function scoreBatch(client: ElydoraClient, decisions: Decision[]) {
  for (const decision of decisions) {
    const record = client.createOperation({
      operationType: 'fraud.score',
      subject: { system: 'fraud', resource: 'transaction', id: decision.txId },
      action: { score: decision.score, flagged: decision.score > 0.8 },
      payload: { features: decision.features },
    });
    await client.submitOperation(record);
  }
}

// Agents run in parallel with each other
await Promise.all(scorers.map((client, i) => scoreBatch(client, partitions[i])));
Pattern 04
Multi-agent coordination

For systems where several agents collaborate on one workflow. Each agent keeps its own chain, and operations reference each other through the subject field. This gives a cross-agent audit trail showing how decisions flowed through the pipeline. Use one correlation_id in the payload to link related operations.

// Agent A: triage agent
const triageRecord = triageClient.createOperation({
  operationType: 'ticket.triage',
  subject: { system: 'support', resource: 'ticket', id: 'TKT-5001' },
  action: { priority: 'high', assigned_to: 'agent_resolver' },
  payload: { correlation_id: 'workflow_abc123', sentiment: 'negative' },
});
await triageClient.submitOperation(triageRecord);

// Agent B: resolver agent (references the same ticket)
const resolveRecord = resolverClient.createOperation({
  operationType: 'ticket.resolve',
  subject: { system: 'support', resource: 'ticket', id: 'TKT-5001' },
  action: { resolution: 'refund_issued', amount: 150 },
  payload: {
    correlation_id: 'workflow_abc123',
    triage_operation_id: triageRecord.operation_id,
  },
});
await resolverClient.submitOperation(resolveRecord);
15. Troubleshooting

Common issues.

Most integration issues fall into a few categories. This section lists the common problems, their causes, and how to resolve them.

PREV_HASH_MISMATCH on startup

Cause: The local chain state is out of sync with the server. Node.js and Python track prev_chain_hash in memory, and that state is lost on restart; a new client starts from the genesis chain hash. Go starts from genesis whenever PrevChainHash is empty.

Solution: Read the expected hash from the error message and resync: set_prev_chain_hash() in Python, or PrevChainHash on the next CreateOperation in Go. The Node.js client does not expose a setter yet; keep one long-lived client per agent. Ensure only one instance submits for a given agent_id at any time.

TTL_EXPIRED errors

Cause: The system clock is out of sync with UTC, or operations are created long before they are submitted. The TTL (default 30 seconds) is measured from issued_at.

Solution: Synchronize the system clock with NTP. On Linux, check timedatectl status. In containers, ensure the host clock is synced. Create each operation right before submitting it, or raise ttlMs up to the 300-second limit.

REPLAY_DETECTED with unique operations

Cause: Each operation must include a cryptographically random 16-byte nonce. If you are constructing operations manually (bypassing the SDK), you may be reusing nonces. The SDK handles nonce generation automatically.

Solution: Use createOperation(), which generates a unique nonce for each operation. If constructing operations manually, use crypto.randomBytes(16) (Node.js) or os.urandom(16) (Python). Never use counters or timestamps as nonces.

Connection timeouts

Cause: Retry behavior differs by SDK. Node.js retries 429 and 5xx responses up to maxRetries times after the first attempt (default 3), with 1 s, 2 s, 4 s delays capped at 10 s, honoring Retry-After. Go retries three times at 100 ms, 400 ms, and 900 ms. Python retries only idempotent requests, so a failed submission raises immediately. Persistent failures point to network issues, DNS failures, or API unavailability.

Solution: Check connectivity to api.elydora.com and DNS resolution. Behind a corporate firewall or proxy, allow HTTPS to port 443. Configure retries with the maxRetries option; proxy settings come from the runtime's fetch configuration.

Receipt verification fails

Cause: verifyOperation() recomputes the receipt hash from the stored evidence. The receipt check fails when the stored EAR or its evidence object was modified, or the evidence is missing from storage.

Solution: Recompute receipt_hash from the stored receipt fields as shown in section 07 and compare it with the stored value; a mismatch means your copy was modified. If checks.receipt is false while your copy is intact, contact support with the requestId.

Signature verification fails on valid operations

Cause: The key used to sign does not match the key registered with Elydora: the client uses a revoked kid, or the wrong environment variable is loaded.

Solution: Verify that ELYDORA_KEY holds the correct private key for this agent. Use client.getPublicKey() to derive the public key and compare it with the registered key from GET /v1/agents/:id. Check that the kid option matches the key you registered.

Ready to build with responsibility.

Start with agent registration, sign your first operation, and verify the receipt.