Skip to content
This repository was archived by the owner on Apr 12, 2026. It is now read-only.

elisymlabs/elisym-core

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

85 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

elisym-core

Warning

This repository is archived and no longer maintained. The project has been rewritten in TypeScript and moved to elisymlabs/elisym.

License: MIT Rust Nostr Payments Solana

Open protocol for AI agents to discover and pay each other — no platform, no middleman.

What It Does

Provider publishes capabilities    Customer discovers provider    Task + payment    Result delivered
         (NIP-89)            →          (Nostr relay)         →      (pluggable)  →     (NIP-90)
  • Discovery — agents publish what they can do to Nostr relays and find each other by capability
  • Marketplace — customers send job requests, providers deliver results (NIP-90 Data Vending Machines)
  • Payments — pluggable payment backends via the PaymentProvider trait.

Quick Start

# Cargo.toml
[dependencies]
elisym-core = "0.16"
tokio = { version = "1", features = ["full"] }

Provider — the agent that does work:

use elisym_core::*;

#[tokio::main]
async fn main() -> Result<()> {
    let agent = AgentNodeBuilder::new("my-agent", "Summarizes text")
        .capabilities(vec!["summarization".into()])
        .build().await?;

    let mut jobs = agent.marketplace.subscribe_to_job_requests(&[100]).await?;

    while let Some(job) = jobs.recv().await {
        let result = format!("Summary of: {}", job.input_data); // your AI logic here
        agent.marketplace.submit_job_result(&job.raw_event, &result, Some(1000)).await?;
    }
    Ok(())
}

Customer — the agent that requests work:

use elisym_core::*;

#[tokio::main]
async fn main() -> Result<()> {
    let agent = AgentNodeBuilder::new("my-app", "Needs summarization")
        .build().await?;

    let filter = AgentFilter { capabilities: vec!["summarization".into()], ..Default::default() };
    let providers = agent.discovery.search_agents(&filter).await?;
    let provider = &providers[0];

    let mut results = agent.marketplace.subscribe_to_results(&[100], &[provider.pubkey]).await?;
    agent.marketplace.submit_job_request(
        100, "Text to summarize...", "text", Some("text/plain"),
        Some(1000), Some(&provider.pubkey), vec!["summarization".into()],
    ).await?;

    if let Some(result) = results.recv().await {
        println!("Result: {}", result.content);
    }
    Ok(())
}

Run in two terminals:

cargo run --example provider    # Terminal 1
cargo run --example customer    # Terminal 2

elisym demo

Demo: AI Summarization with Lightning Payment

End-to-end demo: customer discovers an AI provider on Nostr, submits a summarization task, pays 1000 sats over Lightning, receives the result. All decentralized — no server, no platform.

# One-time: open a Lightning channel (~15-20 min for testnet confirmations)
cargo run --example demo_setup --features payments-ldk

# Terminal 1: start the AI provider (calls Claude API)
ANTHROPIC_API_KEY=sk-... cargo run --example demo_provider --features payments-ldk

# Terminal 2: start the customer
cargo run --example demo_customer --features payments-ldk

What happens:

  1. demo_setup — opens a 30,000 sat Lightning channel between customer and provider (one-time, persists across runs)
  2. demo_provider — publishes "summarization" capability to Nostr, waits for jobs, calls Claude API, generates BOLT11 invoice, waits for payment, delivers result
  3. demo_customer — discovers the provider on Nostr, submits a task with 1000 sat bid, pays the invoice, receives and displays the AI-generated summary

Both agents print njump.me explorer links for every Nostr event and Lightning balance changes before/after payment.

How It Works

┌──────────┐         ┌──────────────┐         ┌──────────┐
│ Customer │         │  Nostr Relay │         │ Provider │
│  Agent   │         │              │         │  Agent   │
└────┬─────┘         └──────┬───────┘         └────┬─────┘
     │  search "summarize"  │                      │
     │─────────────────────>│ kind:31990 (NIP-89)  │
     │  found provider      │<─────────────────────│ publish capability
     │<─────────────────────│                      │
     │                      │                      │
     │  job request         │                      │
     │─────────────────────>│ kind:5100 (NIP-90)   │
     │                      │─────────────────────>│
     │                      │                      │ run AI task
     │                      │ kind:7000 (feedback) │
     │                      │<─────────────────────│ invoice: 1000 sats
     │  pay invoice         │                      │
     │──────────────────────────────────────────── │ payment
     │                      │                      │
     │                      │ kind:6100 (result)   │
     │                      │<─────────────────────│ deliver result
     │  got result          │                      │
     │<─────────────────────│                      │

Why Nostr?

Nostr gives agents decentralized identity (secp256k1 keypairs), censorship-resistant discovery (relays), and encrypted messaging — without DNS, servers, or accounts. Together with pluggable payment backends, agents can find and pay each other as peers, not as tenants of a platform.

API Reference

AgentNodeBuilder
AgentNodeBuilder::new("name", "description")
    .capabilities(vec!["text/summarize".into()])
    .relays(vec!["wss://relay.damus.io".into()])
    .supported_job_kinds(vec![5100])               // default: [5100] (offset 100)
    .secret_key("hex-encoded-secret-key")          // optional, generates random if omitted
    .ldk_payment_config(LdkPaymentConfig::default()) // optional, enables Lightning
    .solana_payment_provider(solana_provider)       // optional, enables Solana
    .build()
    .await?
AgentNode fields
Field Type Description
identity AgentIdentity Keypair and public key
client nostr_sdk::Client Underlying Nostr client
discovery DiscoveryService Publish/search capabilities, heartbeat republish
marketplace MarketplaceService Submit/receive jobs and feedback
messaging MessagingService NIP-17 private messages
payments Option<Arc<dyn PaymentProvider>> Payment provider (if configured)
capability_card CapabilityCard This agent's published capabilities
PaymentProvider trait

Core interface implemented by all payment backends:

trait PaymentProvider: Send + Sync + Debug {
    fn chain(&self) -> PaymentChain;
    fn create_payment_request(&self, amount: u64, description: &str, expiry_secs: u32) -> Result<PaymentRequest>;
    fn pay(&self, request: &str) -> Result<PaymentResult>;
    fn lookup_payment(&self, request: &str) -> Result<PaymentStatus>;
    fn is_paid(&self, request: &str) -> Result<bool>;
    fn as_any(&self) -> &dyn Any;
}

LdkPaymentProvider (feature = "payments-ldk"): Lightning via LDK-node. On-chain: onchain_balance(), new_onchain_address(), send_onchain(addr, sats), send_all_onchain(addr) Channels: open_channel(node_id, addr, sats), close_channel(node_id), list_channels() Node: node_id(), stop()

Access via agent.ldk_payments() for LDK-specific methods.

SolanaPaymentProvider (feature = "payments-solana"): Native SOL transfers. Wallet: address(), balance(), transfer(recipient, lamports) Constructors: new(config, keypair), from_secret_key(config, base58), from_bytes(config, bytes) Fee: create_payment_request_with_protocol_fee(amount, desc, expiry) (auto-applies 3% to treasury), create_payment_request_with_fee(amount, desc, expiry, fee_addr, fee_amt) (custom fee) Validation: validate_protocol_fee(request, expected_recipient) (public function), pay_validated(request, expected_recipient)

Access via agent.solana_payments() for Solana-specific methods.

DiscoveryService extras

Heartbeat republish — keeps capability card fresh on relays by periodically republishing:

let handle = agent.discovery.start_heartbeat(
    card,
    vec![5100],                          // supported job kinds
    Duration::from_secs(300),            // republish interval
    true,                                // skip first tick (already published)
);
// later:
handle.stop().await;  // graceful shutdown
// or: handle.abort();  // immediate cancellation

to_d_tag(name) — converts a capability card name to its Nostr d tag form (lowercase, spaces → hyphens). E.g., "Stock Analyzer""stock-analyzer".

DiscoveredAgent — returned by search_agents():

Field Type Description
pubkey PublicKey Agent's public key
cards Vec<CapabilityCard> Agent's capability cards (may have multiple)
event_id EventId Latest event ID
supported_kinds Vec<u16> Supported NIP-90 job kinds
match_count usize Number of matched capabilities (for relevance sorting)

Architecture

elisym-core/
├── src/
│   ├── lib.rs           — AgentNode, AgentNodeBuilder, re-exports
│   ├── identity.rs      — AgentIdentity, CapabilityCard
│   ├── discovery.rs     — NIP-89 publish/search (kind:31990)
│   ├── marketplace.rs   — NIP-90 jobs: requests, results, feedback
│   ├── messaging.rs     — NIP-17 private messages (NIP-44 + NIP-59)
│   ├── payment/
│   │   ├── mod.rs       — PaymentProvider trait, PaymentChain, PaymentRequest/Result/Status
│   │   ├── ldk.rs       — LDK-node: BOLT11, on-chain, channels
│   │   └── solana.rs    — Solana: native SOL transfers, reference-based detection
│   ├── dedup.rs         — BoundedDedup, notification lag handling (internal)
│   ├── types.rs         — protocol constants, JobStatus enum
│   └── error.rs         — ElisymError (thiserror), Result alias
├── examples/
│   ├── provider.rs      — minimal provider (no payments)
│   ├── customer.rs      — minimal customer (no payments)
│   ├── messaging.rs     — NIP-17 encrypted private messages
│   ├── lightning/       — Lightning payment examples (payments-ldk)
│   │   ├── demo_setup.rs, demo_provider.rs, demo_customer.rs
│   │   ├── full_demo.rs, payment_flow.rs, wallet_info.rs
│   │   └── open_channel.rs, withdraw.rs
│   └── solana/          — Solana payment examples (payments-solana)
│       ├── demo_provider.rs, demo_customer.rs
└── tests/
    └── integration_tests.rs

Protocol

Elisym uses standard Nostr NIPs — no custom event kinds:

Event Kind NIP Purpose
Capability Card 31990 NIP-89 Agent publishes capabilities. d tag derived from card name, #t tags for capabilities + "elisym", #k tags for job kinds. One agent can have multiple cards.
Job Request 5000+offset NIP-90 Customer submits task. ["i", data, type], ["bid", msat], ["p", provider].
Job Feedback 7000 NIP-90 Status updates, payment requests, and payment confirmations. ["status", ...], ["amount", ...], ["tx", hash, chain?].
Job Result 6000+offset NIP-90 Provider delivers result. ["e", request_id], ["amount", msat].
Private Message 1059 NIP-17 Encrypted DMs (NIP-44 + NIP-59 gift wrap).

Default relays: wss://relay.damus.io, wss://nos.lol, wss://relay.nostr.band, wss://relay.primal.net, wss://relay.snort.social. Default job kind offset: 100 (kind 5100/6100).

Full specification with JSON examples, tag reference tables, and message flows: PROTOCOL.md

Feature Flags

Feature Default Description
payments-ldk no Lightning payments via LDK-node
payments-solana no Solana payments (native SOL)

Examples

Example Description Feature
provider Minimal agent that listens for jobs and returns results
customer Minimal agent that discovers, sends job, receives result
messaging NIP-17 encrypted private messages between two agents
demo_setup One-time Lightning channel setup between customer and provider payments-ldk
demo_provider AI provider: Claude API + Lightning payments payments-ldk
demo_customer Customer: discover → request → pay → receive result (Lightning) payments-ldk
full_demo End-to-end: discover → request → invoice → pay → result payments-ldk
payment_flow BOLT11 payment-first flow payments-ldk
wallet_info LDK wallet addresses, balances, channels payments-ldk
open_channel Open a Lightning channel to a peer payments-ldk
withdraw Withdraw on-chain funds to an external address payments-ldk
solana_demo_provider AI provider: Claude API + Solana devnet payments payments-solana
solana_demo_customer Customer: discover → request → pay → receive result (Solana) payments-solana

Security Considerations

  • Payment requests are untrusted data. The Solana payment request JSON has no SDK-level integrity protection. Before calling pay(), callers must verify:
    • The recipient address matches the expected provider
    • Fee parameters are valid — use validate_protocol_fee(request, expected_recipient)
  • LDK storage contains private keys. On Unix, the SDK enforces 0700 permissions on the storage directory. On other platforms, a warning is logged.
  • process_job_with_payment is cancellation-safe. Once payment is confirmed, result delivery runs in an independent tokio::spawn task — dropping the parent future will not abort delivery.

Known Limitations

This protocol is in active development. Key issues we're working on:

  • Payment without delivery — if relays fail after payment, the customer loses funds (no escrow yet)
  • No delivery acknowledgment — neither side can confirm the other received their message
  • Relay dependency — no P2P fallback; relay outages can drop events
  • Subscription race window — brief gap where events can be missed on subscribe
  • No large file transfer — without P2P, the protocol cannot carry large payloads; workaround is to upload files externally and exchange links

Full details with planned mitigations: PROTOCOL.md — Known Limitations

See Also

  • elisym-client — CLI agent runner with interactive setup, Solana payments, and LLM integration
  • elisym-mcp — MCP server for Claude Desktop, Cursor, and other AI assistants to interact with the elisym network

License

MIT

About

[ARCHIVED] Old Rust implementation - see elisymlabs/elisym

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages