Skip to content

feat: add @elizaos/plugin-openttt — Proof-of-Time temporal attestation for AI agents#6645

Open
Heime-Jorgen wants to merge 5 commits intoelizaOS:developfrom
Heime-Jorgen:feat/plugin-openttt
Open

feat: add @elizaos/plugin-openttt — Proof-of-Time temporal attestation for AI agents#6645
Heime-Jorgen wants to merge 5 commits intoelizaOS:developfrom
Heime-Jorgen:feat/plugin-openttt

Conversation

@Heime-Jorgen
Copy link
Copy Markdown

@Heime-Jorgen Heime-Jorgen commented Mar 22, 2026

Summary

Adds @elizaos/plugin-openttt — a Proof-of-Time (PoT) plugin that attaches tamper-evident temporal attestations to AI agent transactions using the OpenTTT protocol.

Why this matters for ElizaOS agents: Every agent trade or transaction carries a timestamp — but timestamps can be forged. This plugin queries four independent time sources (NIST, Apple, Google, Cloudflare), computes a consensus timestamp, and signs it with Ed25519 — producing a PoT token any counterparty can verify on-chain.

What's included

  • GENERATE_POT action — generate a PoT before submitting a trade
  • VERIFY_POT action — verify a previously issued PoT
  • QUERY_POT action — retrieve PoT details by hash
  • TIME_PROVIDER provider — injects current verified time into agent context
  • POT_COVERAGE_EVALUATOR evaluator — warns if a trade message lacks PoT coverage

Technical notes

  • Real Ed25519 signing via openttt SDK (PotSigner.signPot / verifyPotSignature)
  • SHA-256 potHash as stable cache key (avoids message.id mismatch)
  • potHash-keyed + agentId last-pointer cache
  • Compatible with @elizaos/core 2.0.x handler signatures
  • 4-source time consensus: NIST, Apple, Google, Cloudflare

Related

Test plan

  • npm install @elizaos/plugin-openttt in an ElizaOS project
  • Register openTTTPlugin in AgentRuntime
  • Send a message with trade intent → GENERATE_POT fires automatically
  • Call VERIFY_POT on the returned potHash → verification passes

Greptile Summary

This PR introduces @elizaos/plugin-openttt, a new ElizaOS plugin that attaches Proof-of-Time (PoT) attestations to agent transactions by querying four independent HTTP time sources (NIST, Apple, Google, Cloudflare), computing a consensus timestamp, and signing it with Ed25519. The plugin adds three actions (GENERATE_POT, VERIFY_POT, QUERY_POT), a TIME_PROVIDER provider, and a POT_COVERAGE_EVALUATOR evaluator.

Key issues found:

  • Critical bug in potEvaluator: The evaluator's handler looks up openttt:pot:${message.id}, but generatePot stores tokens under openttt:pot:${pot.potHash}. Since message.id is never used as a cache key anywhere, the evaluator will always fail to find a PoT and unconditionally print the coverage warning, rendering it non-functional. The other three actions (verifyPot, queryPot) correctly use the openttt:last:{agentId} pointer fallback — the evaluator simply needs to do the same.
  • Ephemeral Ed25519 signer breaks cross-restart verification: _signer = new PotSigner() regenerates a fresh keypair on every process start. Any PoTs issued before a restart become unverifiable, which significantly undermines the tamper-evident attestation guarantee. The keypair should be derived from a persistent secret or the PotSignature type should embed the public key for self-contained verification (worth clarifying in code comments).
  • setInterval missing .unref(): The module-level cleanup timer prevents the Node.js process from exiting gracefully when no other work is pending.
  • package.json issues: peerDependencies range >=0.1.0 is far too broad for a 2.x API consumer; missing "type": "module", "exports" map, and "files" field; openttt version in README (0.1.3) disagrees with the declared dependency (^0.2.6). No test files are included despite jest being configured as a devDependency.

Confidence Score: 2/5

  • Not ready to merge — the core evaluator feature is broken and the ephemeral signing key undermines the attestation guarantee.
  • The potEvaluator always fires its warning regardless of whether a PoT was generated (wrong cache key), making the advertised coverage-check feature entirely non-functional. The ephemeral-signer design means PoTs cannot be verified after any process restart, which is a significant reliability concern for an attestation system. Multiple package.json issues would cause resolution or publishing problems in the monorepo. These require concrete fixes before the plugin delivers on its stated goals.
  • packages/plugin-openttt/src/evaluators/potEvaluator.ts (broken cache key), packages/plugin-openttt/src/actions/generatePot.ts (ephemeral signer, unref'd timer), packages/plugin-openttt/package.json (missing fields, broad peer range)

Important Files Changed

Filename Overview
packages/plugin-openttt/src/evaluators/potEvaluator.ts Critical logic bug: evaluator always looks up openttt:pot:${message.id} but PoTs are stored under openttt:pot:${pot.potHash} — the evaluator will unconditionally warn even when a PoT was just generated, making it entirely non-functional.
packages/plugin-openttt/src/actions/generatePot.ts Core PoT generation logic is correct; module-level ephemeral signer means PoTs become unverifiable after process restart. The setInterval cleanup timer is missing .unref(), preventing graceful process exit.
packages/plugin-openttt/src/actions/verifyPot.ts Verification logic is sound: checks signature, age, future-timestamp guard, and degraded consensus. Hash and last-pointer fallback lookup matches how generatePot stores tokens.
packages/plugin-openttt/src/providers/timeProvider.ts Multi-source time consensus implementation is well-structured with proper fallback to local time. Correctly uses AbortController for per-request timeouts and computes median across sources.
packages/plugin-openttt/package.json Missing "type": "module", "exports" map, and "files" field. peerDependencies range >=0.1.0 is far too broad for a 2.x API consumer. README version (openttt@0.1.3) disagrees with declared dependency (^0.2.6).
packages/plugin-openttt/src/index.ts Clean barrel file; correctly wires all actions, provider, and evaluator into the plugin object with proper named re-exports.

Sequence Diagram

sequenceDiagram
    participant User
    participant Agent as ElizaOS Agent
    participant GP as GENERATE_POT
    participant TP as TimeProvider
    participant Sources as NIST/Apple/Google/Cloudflare
    participant Cache as potCache (module-level Map)
    participant VP as VERIFY_POT
    participant PE as potEvaluator

    User->>Agent: "Generate a proof of time before this trade"
    Agent->>GP: handler()
    GP->>TP: getVerifiedTime()
    TP->>Sources: HEAD request (parallel, 3s timeout)
    Sources-->>TP: Date headers
    TP-->>GP: VerifiedTime { timestamp, sources, consensus, deviation_ms }
    GP->>GP: randomBytes(16) → nonce
    GP->>GP: SHA-256(agentId:timestamp:sources:nonce) → potHash
    GP->>GP: _signer.signPot(potHash) → PotSignature
    GP->>Cache: set openttt:pot:{potHash}
    GP->>Cache: set openttt:last:{agentId}
    GP-->>Agent: PoTToken + response text

    User->>Agent: "Verify the proof of time on my last transaction"
    Agent->>VP: handler()
    VP->>Cache: get openttt:last:{agentId} → potHash
    VP->>Cache: get openttt:pot:{potHash} → PoTToken
    VP->>VP: PotSigner.verifyPotSignature(potHash, signature)
    VP->>TP: getVerifiedTime() → current time
    VP->>VP: check age < 5 min
    VP-->>Agent: VerifyResult { valid, reason, age_ms }

    Note over PE,Cache: BUG: evaluator looks up openttt:pot:{message.id}<br/>but cache key is openttt:pot:{potHash} — always misses
    Agent->>PE: handler(message)
    PE->>Cache: get openttt:pot:{message.id} ← wrong key
    Cache-->>PE: null (always)
    PE-->>Agent: ⚠ warns every time
Loading

Reviews (1): Last reviewed commit: "feat: add @elizaos/plugin-openttt — Proo..." | Re-trigger Greptile

Greptile also left 5 inline comments on this PR.

Summary by CodeRabbit

  • New Features

    • Introduced OpenTTT Proof-of-Time (PoT) plugin enabling agents to generate and verify transaction timestamps
    • Automatic evaluator warns when trade messages lack PoT coverage
    • Multi-source consensus timestamp verification across independent time providers
  • Documentation

    • Added plugin documentation with usage examples and configuration details

…n (v3)

- Real Ed25519 signing via openttt SDK (PotSigner.signPot / verifyPotSignature)
- SHA-256 potHash as stable cache key for GENERATE_POT/VERIFY_POT correlation
- potHash-keyed + agentId last-pointer cache (no message.id mismatch)
- GENERATE_POT, VERIFY_POT, QUERY_POT actions
- TIME_PROVIDER (4-source: NIST, Apple, Google, Cloudflare)
- POT_COVERAGE_EVALUATOR for trade message coverage check
- @elizaos/core 2.0.x compatible handler signatures
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 22, 2026

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a227cf97-a6ec-442b-a9bf-5004ea91d2a7

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

This PR introduces @elizaos/plugin-openttt, a new ElizaOS plugin implementing a Proof-of-Time (PoT) workflow for agent transactions. It provides actions to generate, verify, and query PoT tokens using consensus timestamps from four independent time sources, a time provider for multi-source verification, and an evaluator that detects trade-related messages and warns when PoT coverage is missing.

Changes

Cohort / File(s) Summary
Configuration & Documentation
packages/plugin-openttt/README.md, package.json, tsconfig.json
Plugin metadata, build configuration, dependencies (openttt ^0.2.6, @elizaos/core peer dependency), and comprehensive documentation covering PoT workflow, integrity pipeline, consensus behavior, and API endpoints.
Actions
src/actions/generatePot.ts, src/actions/queryPot.ts, src/actions/verifyPot.ts
Three Eliza actions: generatePot creates PoT tokens via Ed25519 signing and TTL caching; queryPot retrieves cached tokens; verifyPot validates token signatures and checks age (5-min max), with fallback to "last-generated" tracking.
Time Provider
src/providers/timeProvider.ts
Multi-source time verification via concurrent HEAD requests to NIST, Apple, Google, Cloudflare; computes consensus timestamp (median) with 2-second deviation threshold; falls back to local system time when fewer than 2 sources respond.
Evaluator & Plugin Entry
src/evaluators/potEvaluator.ts, src/index.ts
potEvaluator detects trade/transaction keywords and warns of missing PoT coverage; openTTTPlugin composes all actions, provider, and evaluator with type re-exports (PoTToken, VerifyResult, VerifiedTime).

Sequence Diagram(s)

sequenceDiagram
    participant Agent
    participant generatePot as generatePot Action
    participant timeProvider as Time Provider
    participant TimeSources as Time Sources<br/>(NIST/Apple/Google/CF)
    participant PotSigner as PotSigner
    participant Cache as PoT Cache
    participant verifyPot as verifyPot Action

    Agent->>generatePot: Request PoT token
    generatePot->>timeProvider: getVerifiedTime()
    timeProvider->>TimeSources: Concurrent HEAD requests
    TimeSources-->>timeProvider: Date headers
    timeProvider->>timeProvider: Compute consensus & deviation
    timeProvider-->>generatePot: VerifiedTime (timestamp, sources, consensus)
    generatePot->>generatePot: Generate nonce & potHash
    generatePot->>PotSigner: Sign potHash (Ed25519)
    PotSigner-->>generatePot: Signature
    generatePot->>Cache: Store PoTToken (TTL: 5 min)
    generatePot-->>Agent: success + pot data

    Agent->>verifyPot: Verify PoT token
    verifyPot->>Cache: Retrieve cached PoT
    Cache-->>verifyPot: PoTToken
    verifyPot->>PotSigner: Verify signature
    PotSigner-->>verifyPot: Signature valid
    verifyPot->>timeProvider: getVerifiedTime() for age check
    timeProvider-->>verifyPot: Current time
    verifyPot->>verifyPot: Check age ≤ 5 min
    verifyPot-->>Agent: VerifyResult (valid: true, age_ms, details)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Suggested labels

Plugin_new, Needs Testing

Suggested reviewers

  • HashWarlock
  • camharris

Poem

Time tokens sealed with cryptographic grace,
Four sources whisper—consensus finds its place,
Trade flows verified in blockchain's embrace, 🔐
Proof-of-Time plugin races! 🚀

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The PR description is comprehensive and well-structured, covering summary, included features, technical notes, related resources, and test plan; however, required template sections for 'Risks', 'Documentation changes needed', and detailed 'Testing' steps are incomplete or minimally addressed. Complete the required template sections: clearly state risk level and affected areas, confirm documentation status, and provide detailed step-by-step testing instructions for reviewers to reproduce the functionality.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly and specifically describes the main change: adding a Proof-of-Time plugin for temporal attestation in AI agents.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@Heime-Jorgen
Copy link
Copy Markdown
Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 22, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Comment on lines +40 to +41
const potKey = `openttt:pot:${message.id}`;
const cached = potCacheGet(potKey);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P0 Evaluator always warns — wrong cache key

The evaluator looks up openttt:pot:${message.id}, but generatePot never stores a PoT under that key. It stores under openttt:pot:${pot.potHash} and openttt:last:${agentId}. Because message.id is never used as a cache key in generatePot.ts, this lookup will always return null and the evaluator will unconditionally print the "No Proof-of-Time found" warning, even when a PoT was just generated. The PR description even explicitly calls out "SHA-256 potHash as stable cache key (avoids message.id mismatch)" — the evaluator was never updated to match.

The fix is to fall back to the openttt:last:{agentId} pointer (as verifyPot and queryPot already do):

  handler: async (
    _runtime: IAgentRuntime,
    message: Memory,
    _state?: State
  ): Promise<ActionResult | void | undefined> => {
    // First try to find PoT via last-generated pointer for this agent
    const agentId = (_runtime.agentId ?? "unknown") as string;
    const lastHash = potCacheGet(`openttt:last:${agentId}`);
    const cached = lastHash ? potCacheGet(`openttt:pot:${lastHash}`) : null;

// Module-level signer: ephemeral Ed25519 keypair per agent process.
// A fresh keypair is generated on startup; the public key ships with every PoT
// so any counterparty can verify the signature without a PKI.
const _signer = new PotSigner();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Ephemeral keypair invalidates all persisted PoTs on restart

_signer = new PotSigner() generates a fresh Ed25519 keypair at module load time. If the agent process restarts — even briefly — all previously issued PoTs become unverifiable because PotSigner.verifyPotSignature will be called with the old signature against a brand-new public key. This directly undermines the core value proposition: a counterparty storing a PoT hash cannot verify it after any service restart.

Consider either:

  1. Persisting the signing keypair to runtime.getSetting("OPENTTT_PRIVATE_KEY") / the agent's secret store so the same key survives restarts.
  2. Or embedding the public key inside PotSignature so that verification is self-contained and works with any signer instance (if the openttt SDK already does this, add a comment clarifying it).

Comment on lines +24 to +29
setInterval(() => {
const now = Date.now();
for (const [key, entry] of potCache.entries()) {
if (now > entry.expiresAt) potCache.delete(key);
}
}, 60_000);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 setInterval without .unref() prevents graceful process exit

The cleanup interval is never unref'd, so Node.js will keep the event loop alive indefinitely just for this timer. Call .unref() so the process can exit cleanly when no other work is pending:

Suggested change
setInterval(() => {
const now = Date.now();
for (const [key, entry] of potCache.entries()) {
if (now > entry.expiresAt) potCache.delete(key);
}
}, 60_000);
const _cleanupInterval = setInterval(() => {
const now = Date.now();
for (const [key, entry] of potCache.entries()) {
if (now > entry.expiresAt) potCache.delete(key);
}
}, 60_000).unref();

Comment on lines +19 to +21
"peerDependencies": {
"@elizaos/core": ">=0.1.0"
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 peerDependencies version range is too broad

>=0.1.0 implies compatibility with every version of @elizaos/core starting from the very beginning, but the handler signatures, ActionResult, ProviderResult, and other types used in this plugin target the 2.x API surface. Older versions lack these types and would produce cryptic build errors. The range should be tightened:

Suggested change
"peerDependencies": {
"@elizaos/core": ">=0.1.0"
},
"peerDependencies": {
"@elizaos/core": ">=2.0.0"
},

Comment on lines +1 to +35
{
"name": "@elizaos/plugin-openttt",
"version": "0.1.0",
"description": "OpenTTT Proof-of-Time plugin for ElizaOS — temporal attestation for AI agent transactions",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc",
"test": "jest"
},
"keywords": [
"elizaos",
"plugin",
"proof-of-time",
"temporal-attestation",
"openttt"
],
"license": "MIT",
"peerDependencies": {
"@elizaos/core": ">=0.1.0"
},
"dependencies": {
"openttt": "^0.2.6"
},
"devDependencies": {
"@elizaos/core": "^2.0.0-alpha.86",
"@types/jest": "^29.0.0",
"jest": "^29.0.0",
"ts-jest": "^29.0.0",
"typescript": "^5.0.0"
},
"overrides": {
"@types/node": "^25.0.6"
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Missing standard package.json fields

The package is missing several fields required for correct module resolution and publishing in the monorepo:

  • "type": "module" — the source uses ESM (.js extension imports, ESNext module target in tsconfig.json), so this field is needed so Node.js resolves dist/index.js as ESM.
  • "exports" map — modern bundlers and Node.js use exports for package entry points.
  • "files" — without this, npm publish will include all files (including src/, tsconfig.json, etc.).

Additionally, the README.md states openttt@0.1.3 but package.json declares "openttt": "^0.2.6". These should be kept in sync to avoid confusing users.

Evaluator was looking up openttt:pot:${message.id} which was never
stored. generatePot stores under openttt:pot:${potHash} with a
last-pointer at openttt:last:${agentId}.

Now resolves: agentId last-pointer → potHash → PoT record.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (3)
packages/plugin-openttt/src/actions/generatePot.ts (1)

23-29: Consider unref() on cleanup interval for graceful shutdown.

The setInterval keeps the Node.js event loop alive, potentially preventing the process from exiting gracefully when the agent stops.

Proposed fix
 // Periodic cleanup: evict expired entries every minute
-setInterval(() => {
+const cleanupInterval = setInterval(() => {
   const now = Date.now();
   for (const [key, entry] of potCache.entries()) {
     if (now > entry.expiresAt) potCache.delete(key);
   }
 }, 60_000);
+cleanupInterval.unref();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/plugin-openttt/src/actions/generatePot.ts` around lines 23 - 29, The
periodic cleanup uses setInterval(...) which keeps the Node event loop alive;
capture the returned timer from setInterval in generatePot.ts (the variable
holding the interval for the potCache cleanup) and call timer.unref() so the
interval won't prevent graceful shutdown; locate the setInterval that iterates
potCache.entries(), assign it to a const (e.g., cleanupTimer) and call
cleanupTimer.unref() after creation to allow the process to exit when no other
work remains.
packages/plugin-openttt/README.md (1)

43-53: Add language specifier to fenced code blocks.

Static analysis flags these conversation examples as missing language specifiers. Use text or plaintext for non-code content.

Proposed fix
-```
+```text
 User: Generate a proof of time before I submit this swap
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/plugin-openttt/README.md` around lines 43 - 53, The fenced
conversation example in README.md is missing a language specifier which triggers
static analysis; update the triple-backtick fence(s) around the conversation
snippet (the block starting with "User: Generate a proof of time before I submit
this swap") to include a language token such as text or plaintext (e.g., change
``` to ```text) so the example is recognized as non-code/plain text; ensure all
similar fenced blocks in the file use the same specifier.
packages/plugin-openttt/src/actions/verifyPot.ts (1)

94-163: Consolidate repeated failure-return paths

The failure branches repeat callback + ActionResult construction many times. This is easy to drift and hard to maintain; extract a local helper (e.g., fail(result)) and reuse it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/plugin-openttt/src/actions/verifyPot.ts` around lines 94 - 163,
Multiple failure branches in verifyPot are duplicating the same pattern (build
VerifyResult, optionally await callback({...}), then return { success:false,...
}); refactor by adding a local helper function (e.g., fail(result:
VerifyResult)) inside the verifyPot scope that performs the callback invocation
when callback is present and returns the ActionResult shape expected; then
replace each repeated block (the "no pot" branch, missing signature branch,
sigValid false branch, expired age_ms > POT_MAX_AGE_MS branch, and future
timestamp age_ms < 0 branch) to call fail(result) instead of inlining the
callback + return logic. Ensure the helper preserves awaiting the callback and
returns exactly { success: false, error: result.reason, data: { result } } so
existing callers and behavior (including use of pot, age_ms, and current_time
fields) remain unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/plugin-openttt/package.json`:
- Around line 1-10: The package.json for the `@elizaos/plugin-openttt` package is
missing the top-level "type": "module" field causing Node to treat emitted
ESNext .js files as CommonJS; update the package.json (the object containing
"name": "@elizaos/plugin-openttt", "version", "main": "dist/index.js", "types":
"dist/index.d.ts", etc.) to include "type": "module" at the top level so Node
will load the dist/*.js files as ESM (ensure it's added alongside the existing
fields, not nested).

In `@packages/plugin-openttt/README.md`:
- Around line 82-87: Update the README example so the documented action list for
openTTTPlugin matches the registered actions: include queryPot alongside
generatePot and verifyPot in the openTTTPlugin.actions comment (i.e., change the
comment showing [generatePot, verifyPot] to [generatePot, verifyPot, queryPot])
so the API section accurately reflects the registered actions.

In `@packages/plugin-openttt/src/actions/verifyPot.ts`:
- Around line 67-70: The SHA-256 pot hash extraction is too permissive: replace
the loose regex (currently matching 32+ hex chars via textMatch/potHashFromText
in verifyPot.ts) with an exact 64-hex-character match (use a regex that enforces
64 hex chars) and normalize the resulting value to lowercase for consistent
cache keys; also normalize potHashArg (options?.pot_hash) to lowercase when
using it so both sources produce the same canonical lowercase 64-char hash
string.
- Around line 108-130: The current signature check calls
PotSigner.verifyPotSignature(pot.potHash, pot.signature) but later uses mutable
fields pot.timestamp, pot.sources, and pot.agent_id for expiry/drift decisions,
allowing tampering; update verifyPot logic to bind the signature to the actual
payload fields by re-computing the canonical payload hash from the full token
fields (at least pot.potHash plus pot.timestamp, pot.sources, pot.agent_id — or
serialize the canonical payload used at signing) and verify the signature
against that recomputed hash via PotSigner.verifyPotSignature (or a new
PotSigner.verifySignedPayload) before performing age/checks (getVerifiedTime(),
age_ms). Ensure the verification path rejects tokens where the signed payload
does not include those mutable fields.

In `@packages/plugin-openttt/src/evaluators/potEvaluator.ts`:
- Around line 39-41: The evaluator uses potCacheGet with key
`openttt:pot:${message.id}` which never matches the key generated by generatePot
(`openttt:pot:${pot.potHash}`), so it always misses PoT tokens; update the
lookup in potEvaluator to follow the same resolution used by verifyPot/queryPot:
read the pointer `openttt:last:${agentId}` (resolve agentId from the message or
context), use that pointer value (potHash) to build the real key
`openttt:pot:${potHash}`, then call potCacheGet with that key (keeping existing
potCacheGet usage and error handling). Ensure you reference the same pointer
resolution logic as in verifyPot/queryPot and remove reliance on message.id.

---

Nitpick comments:
In `@packages/plugin-openttt/README.md`:
- Around line 43-53: The fenced conversation example in README.md is missing a
language specifier which triggers static analysis; update the triple-backtick
fence(s) around the conversation snippet (the block starting with "User:
Generate a proof of time before I submit this swap") to include a language token
such as text or plaintext (e.g., change ``` to ```text) so the example is
recognized as non-code/plain text; ensure all similar fenced blocks in the file
use the same specifier.

In `@packages/plugin-openttt/src/actions/generatePot.ts`:
- Around line 23-29: The periodic cleanup uses setInterval(...) which keeps the
Node event loop alive; capture the returned timer from setInterval in
generatePot.ts (the variable holding the interval for the potCache cleanup) and
call timer.unref() so the interval won't prevent graceful shutdown; locate the
setInterval that iterates potCache.entries(), assign it to a const (e.g.,
cleanupTimer) and call cleanupTimer.unref() after creation to allow the process
to exit when no other work remains.

In `@packages/plugin-openttt/src/actions/verifyPot.ts`:
- Around line 94-163: Multiple failure branches in verifyPot are duplicating the
same pattern (build VerifyResult, optionally await callback({...}), then return
{ success:false,... }); refactor by adding a local helper function (e.g.,
fail(result: VerifyResult)) inside the verifyPot scope that performs the
callback invocation when callback is present and returns the ActionResult shape
expected; then replace each repeated block (the "no pot" branch, missing
signature branch, sigValid false branch, expired age_ms > POT_MAX_AGE_MS branch,
and future timestamp age_ms < 0 branch) to call fail(result) instead of inlining
the callback + return logic. Ensure the helper preserves awaiting the callback
and returns exactly { success: false, error: result.reason, data: { result } }
so existing callers and behavior (including use of pot, age_ms, and current_time
fields) remain unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4c529a98-ae17-48f9-9193-c6f40046ebcd

📥 Commits

Reviewing files that changed from the base of the PR and between 83bbc22 and f557391.

📒 Files selected for processing (9)
  • packages/plugin-openttt/README.md
  • packages/plugin-openttt/package.json
  • packages/plugin-openttt/src/actions/generatePot.ts
  • packages/plugin-openttt/src/actions/queryPot.ts
  • packages/plugin-openttt/src/actions/verifyPot.ts
  • packages/plugin-openttt/src/evaluators/potEvaluator.ts
  • packages/plugin-openttt/src/index.ts
  • packages/plugin-openttt/src/providers/timeProvider.ts
  • packages/plugin-openttt/tsconfig.json

Comment on lines +1 to +10
{
"name": "@elizaos/plugin-openttt",
"version": "0.1.0",
"description": "OpenTTT Proof-of-Time plugin for ElizaOS — temporal attestation for AI agent transactions",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc",
"test": "jest"
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing "type": "module" for ESM package.

The tsconfig outputs ESNext modules, but the package.json lacks "type": "module". This can cause Node.js to misinterpret .js files as CommonJS.

Proposed fix
 {
   "name": "@elizaos/plugin-openttt",
   "version": "0.1.0",
+  "type": "module",
   "description": "OpenTTT Proof-of-Time plugin for ElizaOS — temporal attestation for AI agent transactions",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/plugin-openttt/package.json` around lines 1 - 10, The package.json
for the `@elizaos/plugin-openttt` package is missing the top-level "type":
"module" field causing Node to treat emitted ESNext .js files as CommonJS;
update the package.json (the object containing "name":
"@elizaos/plugin-openttt", "version", "main": "dist/index.js", "types":
"dist/index.d.ts", etc.) to include "type": "module" at the top level so Node
will load the dist/*.js files as ESM (ensure it's added alongside the existing
fields, not nested).

Comment on lines +82 to +87
```typescript
import { openTTTPlugin } from "@elizaos/plugin-openttt";
// openTTTPlugin.actions → [generatePot, verifyPot]
// openTTTPlugin.providers → [timeProvider]
// openTTTPlugin.evaluators → [potEvaluator]
```
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Documentation shows incomplete action list.

The API section shows only [generatePot, verifyPot] but queryPot is also registered. Update for accuracy.

Proposed fix
 ```typescript
 import { openTTTPlugin } from "@elizaos/plugin-openttt";
-// openTTTPlugin.actions    → [generatePot, verifyPot]
+// openTTTPlugin.actions    → [generatePot, verifyPot, queryPot]
 // openTTTPlugin.providers  → [timeProvider]
 // openTTTPlugin.evaluators → [potEvaluator]
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against the current code and only fix it if needed.

In @packages/plugin-openttt/README.md around lines 82 - 87, Update the README
example so the documented action list for openTTTPlugin matches the registered
actions: include queryPot alongside generatePot and verifyPot in the
openTTTPlugin.actions comment (i.e., change the comment showing [generatePot,
verifyPot] to [generatePot, verifyPot, queryPot]) so the API section accurately
reflects the registered actions.


</details>

<!-- fingerprinting:phantom:poseidon:ocelot -->

<!-- This is an auto-generated comment by CodeRabbit -->

Comment on lines +67 to +70
const potHashArg = options?.pot_hash as string | undefined;
// Try to extract potHash from message text (e.g. "verify pot abc123")
const textMatch = (message.content?.text as string ?? "").match(/\b([0-9a-f]{32,})\b/i);
const potHashFromText = textMatch?.[1];
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Use strict SHA-256 hash parsing and normalize case

At Line 69, \b([0-9a-f]{32,})\b is too permissive and may capture unrelated hex text. Since potHash is SHA-256 hex, parse exactly 64 hex chars and normalize to lowercase for cache-key consistency.

Suggested fix
-        const potHashArg = options?.pot_hash as string | undefined;
+        const potHashArg =
+          typeof options?.pot_hash === "string" ? options.pot_hash.toLowerCase() : undefined;
...
-        const textMatch = (message.content?.text as string ?? "").match(/\b([0-9a-f]{32,})\b/i);
-        const potHashFromText = textMatch?.[1];
+        const textMatch = (message.content?.text as string ?? "").match(/\b([0-9a-f]{64})\b/i);
+        const potHashFromText = textMatch?.[1]?.toLowerCase();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const potHashArg = options?.pot_hash as string | undefined;
// Try to extract potHash from message text (e.g. "verify pot abc123")
const textMatch = (message.content?.text as string ?? "").match(/\b([0-9a-f]{32,})\b/i);
const potHashFromText = textMatch?.[1];
const potHashArg =
typeof options?.pot_hash === "string" ? options.pot_hash.toLowerCase() : undefined;
// Try to extract potHash from message text (e.g. "verify pot abc123")
const textMatch = (message.content?.text as string ?? "").match(/\b([0-9a-f]{64})\b/i);
const potHashFromText = textMatch?.[1]?.toLowerCase();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/plugin-openttt/src/actions/verifyPot.ts` around lines 67 - 70, The
SHA-256 pot hash extraction is too permissive: replace the loose regex
(currently matching 32+ hex chars via textMatch/potHashFromText in verifyPot.ts)
with an exact 64-hex-character match (use a regex that enforces 64 hex chars)
and normalize the resulting value to lowercase for consistent cache keys; also
normalize potHashArg (options?.pot_hash) to lowercase when using it so both
sources produce the same canonical lowercase 64-char hash string.

Comment on lines +108 to +130
// Verify Ed25519 signature — reject tampered tokens
if (!pot.signature) {
const result: VerifyResult = {
valid: false,
reason: "PoT token has no signature. Token was not generated by a trusted issuer.",
};
if (callback) await callback({ text: `Verification FAILED: ${result.reason}`, content: { result } });
return { success: false, error: result.reason, data: { result } };
}
const sigValid = PotSigner.verifyPotSignature(pot.potHash, pot.signature);
if (!sigValid) {
const result: VerifyResult = {
valid: false,
reason: "Ed25519 signature verification failed. Token may have been tampered with.",
};
if (callback) await callback({ text: `Verification FAILED: ${result.reason}`, content: { result } });
return { success: false, error: result.reason, data: { result } };
}

// Check age
const vt = await getVerifiedTime();
const age_ms = vt.timestamp - pot.timestamp;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Bind verified signature to token payload fields

At Line 117, verification only checks pot.potHash against pot.signature, but expiry and drift decisions use mutable fields (pot.timestamp, pot.sources, pot.agent_id). A tampered token can keep a valid signed hash while altering those fields and still pass policy checks.

Suggested fix
+import { createHash } from "node:crypto";
...
+      // Validate required fields before policy checks
+      if (
+        typeof pot.potHash !== "string" ||
+        typeof pot.timestamp !== "number" ||
+        !Number.isFinite(pot.timestamp) ||
+        typeof pot.agent_id !== "string" ||
+        !Array.isArray(pot.sources)
+      ) {
+        const result: VerifyResult = {
+          valid: false,
+          reason: "PoT token payload is malformed.",
+        };
+        if (callback) await callback({ text: `Verification FAILED: ${result.reason}`, content: { result } });
+        return { success: false, error: result.reason, data: { result } };
+      }
+
+      // Recompute canonical hash from signed fields (must match issuance format)
+      const canonical = `${pot.agent_id}:${pot.timestamp}:${pot.sources.join(",")}:${pot.nonce}`;
+      const expectedHash = createHash("sha256").update(canonical).digest("hex");
+      if (expectedHash !== pot.potHash) {
+        const result: VerifyResult = {
+          valid: false,
+          reason: "PoT hash does not match token payload. Token may be tampered.",
+        };
+        if (callback) await callback({ text: `Verification FAILED: ${result.reason}`, content: { result } });
+        return { success: false, error: result.reason, data: { result } };
+      }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/plugin-openttt/src/actions/verifyPot.ts` around lines 108 - 130, The
current signature check calls PotSigner.verifyPotSignature(pot.potHash,
pot.signature) but later uses mutable fields pot.timestamp, pot.sources, and
pot.agent_id for expiry/drift decisions, allowing tampering; update verifyPot
logic to bind the signature to the actual payload fields by re-computing the
canonical payload hash from the full token fields (at least pot.potHash plus
pot.timestamp, pot.sources, pot.agent_id — or serialize the canonical payload
used at signing) and verify the signature against that recomputed hash via
PotSigner.verifyPotSignature (or a new PotSigner.verifySignedPayload) before
performing age/checks (getVerifiedTime(), age_ms). Ensure the verification path
rejects tokens where the signed payload does not include those mutable fields.

Comment on lines +39 to +41
// Check if a PoT was generated for this message using module-level cache
const potKey = `openttt:pot:${message.id}`;
const cached = potCacheGet(potKey);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Cache key mismatch — evaluator will never find PoT tokens.

generatePot stores tokens with key openttt:pot:${pot.potHash} (see generatePot.ts:127), but this evaluator looks up openttt:pot:${message.id}. These keys never match, so the evaluator always emits false "missing PoT" warnings.

Use the same resolution pattern as verifyPot and queryPot: look up via openttt:last:${agentId} pointer.

Proposed fix
   handler: async (
-    _runtime: IAgentRuntime,
+    runtime: IAgentRuntime,
     message: Memory,
     _state?: State
   ): Promise<ActionResult | void | undefined> => {
-    // Check if a PoT was generated for this message using module-level cache
-    const potKey = `openttt:pot:${message.id}`;
-    const cached = potCacheGet(potKey);
+    // Resolve PoT via last-generated pointer (same pattern as verifyPot/queryPot)
+    const agentId = (runtime.agentId ?? "unknown") as string;
+    const lastHash = potCacheGet(`openttt:last:${agentId}`);
+    const cached = lastHash ? potCacheGet(`openttt:pot:${lastHash}`) : null;

     if (cached) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/plugin-openttt/src/evaluators/potEvaluator.ts` around lines 39 - 41,
The evaluator uses potCacheGet with key `openttt:pot:${message.id}` which never
matches the key generated by generatePot (`openttt:pot:${pot.potHash}`), so it
always misses PoT tokens; update the lookup in potEvaluator to follow the same
resolution used by verifyPot/queryPot: read the pointer
`openttt:last:${agentId}` (resolve agentId from the message or context), use
that pointer value (potHash) to build the real key `openttt:pot:${potHash}`,
then call potCacheGet with that key (keeping existing potCacheGet usage and
error handling). Ensure you reference the same pointer resolution logic as in
verifyPot/queryPot and remove reliance on message.id.

Heime-Jorgen and others added 3 commits March 22, 2026 22:59
…package.json cleanup

- generatePot: lazy-init PotSigner via getOrCreateSigner(runtime)
  reads OPENTTT_PRIVATE_KEY from runtime settings for cross-restart
  continuity; verifyPotSignature is self-contained via issuerPubKey
- setInterval: add .unref() to prevent keeping event loop alive on idle
- package.json: type:module, exports map, files field, peerDeps >=2.0.0
  remove @types/node direct dep (conflicts with monorepo root override)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…queryPot

verifyPot.ts:
- Recompute potHash from fields (agent_id:timestamp:sources:nonce) before
  signature check — detects mutable-field tampering while signature stays valid
- Hash regex tightened to exactly 64 hex chars (SHA-256) with toLowerCase()
- Add createHash import from node:crypto

README.md:
- Add queryPot to openTTTPlugin.actions list

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…alize potHash, fix README

- generatePot.ts: assign setInterval to cleanupInterval then call .unref() explicitly
- verifyPot.ts: normalize potHashArg to lowercase via .toLowerCase() to handle mixed-case hashes
- README.md: add 'text' language specifier to three bare fenced code blocks (conversation examples)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@Heime-Jorgen
Copy link
Copy Markdown
Author

Hey @shaw @lalalune 👋 Plugin is ready for review!

@elizaos/plugin-openttt adds Proof-of-Time (PoT) temporal attestation for AI agent transactions — think of it as a temporal integrity layer: before executing a trade or action, an agent can generate a cryptographically signed timestamp from 4 independent sources (NIST, Apple, Google, Cloudflare), then verify it was issued within the last 5 minutes.

✅ Already merged into awesome-mcp-servers (Finance/DeFi category)
✅ Glama AAA score
✅ 112 tests passing

Would love your feedback when you get a chance! 🙏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants