Integrate Elydora.
Step by step.
The guide to integrating Elydora into an AI agent platform: key generation, registration, signing, submission, verification, and operations.
What this guide covers.
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).
Before you begin.
Ensure your environment meets these requirements before installing the SDK.
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.
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.
$ npm install @elydora/sdk# or: yarn add @elydora/sdkRequires Node.js 18 or later. The SDK uses the native crypto module for Ed25519 operations; no native dependencies are required.
$ pip install elydora# or: poetry add elydoraRequires Python 3.10 or later. Uses the cryptography package for Ed25519 operations, installed as a dependency.
$ go get github.com/Elydora-Infrastructure/Elydora-Go-SDK/v2Requires Go 1.21 or later. Uses the standard library crypto/ed25519 package; no external dependencies for cryptographic operations.
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.
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); // activeimport 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"]) # activeimport (
"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) // activeStep 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:
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.
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 recordThe 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.
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 acknowledgeschain_hashThe chain hash of this operation; the SDK already holds it as the next prev_chain_hashseq_noMonotonically increasing sequence number for this agent's chainserver_received_atServer-side Unix epoch milliseconds timestamp of acceptanceelydora_signatureEd25519 signature from the Elydora platform over receipt_hash, verifiable against the public JWKSStore 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.
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;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 closeschecks.signatureVerifies 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.
checks.chainVerifies the chain_hash linkage: this operation's prev_chain_hash references the predecessor operation. Confirms the sequential integrity of the agent's operation chain.
checks.receiptRecomputes 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.
checks.merkleVerifies 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.
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_SIGNATUREThe 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_AGENTThe 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_REVOKEDThe 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_FROZENThe 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_EXPIREDThe 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_DETECTEDThe 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_MISMATCHThe 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_LARGEThe 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_LIMITEDThe 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;
}
}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);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.
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.
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.
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.
// 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'
);
# 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.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.
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.
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.
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.
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.
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.
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.
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.
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.
Optimize for throughput.
Elydora runs on Cloudflare Workers and scales across agents and organizations. These limits and tips keep submission fast.
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.
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.
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.
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.
Track the time between createOperation() and the receipt in your own metrics. Latency spikes point to network issues or rate limiting.
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.
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;
}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 };
}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])));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);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.