use crate::primitives::{Address, Timestamp, u128_serde};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AgentTemplateStatus {
Published,
Pending,
Deprecated,
Suspended,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AgentTemplateType {
Autonomous,
ToolAgent,
Orchestrator,
Specialist,
MultiModal,
Custom(String),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AgentCapability {
pub id: String,
pub name: String,
pub description: String,
pub input_schema: Option<String>,
pub output_schema: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct AgentRuntimeRequirements {
#[serde(default)]
pub min_model_size: Option<String>,
#[serde(default)]
pub preferred_model_id: Option<String>,
#[serde(default)]
pub required_mcp_tools: Vec<String>,
#[serde(default)]
pub required_permissions: Vec<String>,
#[serde(default)]
pub estimated_cost_per_run: Option<u128>,
#[serde(default)]
pub requires_tee: bool,
#[serde(default)]
pub min_memory_mb: Option<u64>,
#[serde(default)]
pub min_storage_mb: Option<u64>,
#[serde(default)]
pub gpu_required: bool,
#[serde(default)]
pub network_access: bool,
#[serde(default)]
pub supported_platforms: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[derive(Default)]
pub enum AgentPricingModel {
#[default]
Free,
PerExecution { price: u128 },
PerToken { price_per_token: u128 },
Subscription { monthly_rate: u128 },
RevenueShare { creator_share_bps: u16 },
}
impl AgentPricingModel {
pub fn is_free(&self) -> bool {
matches!(self, AgentPricingModel::Free)
}
pub fn fee_for_invocation(&self, tokens: u64) -> u128 {
match self {
AgentPricingModel::Free => 0,
AgentPricingModel::PerExecution { price } => *price,
AgentPricingModel::PerToken { price_per_token } => {
price_per_token.saturating_mul(tokens as u128)
}
AgentPricingModel::Subscription { .. } => 0,
AgentPricingModel::RevenueShare { .. } => 0,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AgentTemplate {
pub template_id: String,
pub name: String,
pub description: String,
pub template_type: AgentTemplateType,
pub creator: Address,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub creator_did: Option<String>,
#[serde(default)]
pub creator_wallet: Option<Address>,
#[serde(default)]
pub invocation_count: u64,
#[serde(default)]
pub total_revenue: u128,
pub version: String,
pub status: AgentTemplateStatus,
pub created_at: Timestamp,
pub updated_at: Timestamp,
pub capabilities: Vec<AgentCapability>,
pub runtime_requirements: AgentRuntimeRequirements,
pub pricing: AgentPricingModel,
pub system_prompt: String,
pub examples: Vec<AgentExample>,
pub tags: Vec<String>,
pub download_count: u64,
pub rating: u8,
pub content_hash: Option<String>,
pub docs_url: Option<String>,
pub metadata: HashMap<String, String>,
#[serde(default)]
pub execution_spec: Option<ExecutionSpec>,
#[serde(default = "default_last_seen_ts")]
pub last_seen_at: Timestamp,
}
fn default_last_seen_ts() -> Timestamp {
Timestamp(chrono::Utc::now().timestamp())
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AgentExample {
pub description: String,
pub user_input: String,
pub expected_output: String,
}
impl AgentTemplate {
pub fn new(
name: String,
description: String,
template_type: AgentTemplateType,
creator: Address,
system_prompt: String,
) -> Self {
let template_id = uuid::Uuid::new_v4().to_string();
let now = Timestamp(chrono::Utc::now().timestamp());
Self {
template_id,
name,
description,
template_type,
creator,
creator_did: None,
creator_wallet: None,
invocation_count: 0,
total_revenue: 0,
version: "1.0.0".to_string(),
status: AgentTemplateStatus::Published,
created_at: now,
updated_at: now,
capabilities: Vec::new(),
runtime_requirements: AgentRuntimeRequirements::default(),
pricing: AgentPricingModel::Free,
system_prompt,
examples: Vec::new(),
tags: Vec::new(),
download_count: 0,
rating: 0,
content_hash: None,
docs_url: None,
metadata: HashMap::new(),
execution_spec: None,
last_seen_at: now,
}
}
pub fn touch(&mut self) {
self.last_seen_at = default_last_seen_ts();
}
pub fn with_creator_did(mut self, did: impl Into<String>) -> Self {
self.creator_did = Some(did.into());
self
}
pub fn with_creator_wallet(mut self, wallet: Address) -> Self {
self.creator_wallet = Some(wallet);
self
}
pub fn with_pricing(mut self, pricing: AgentPricingModel) -> Self {
self.pricing = pricing;
self
}
pub fn is_available(&self) -> bool {
self.status == AgentTemplateStatus::Published
}
pub fn validate_marketplace_invariants(&self) -> Result<(), String> {
if !self.pricing.is_free() && self.creator_wallet.is_none() {
return Err(format!(
"creator_wallet is mandatory for non-Free pricing (pricing={:?})",
self.pricing
));
}
Ok(())
}
pub fn record_invocation(&mut self, creator_share: u128) {
self.invocation_count = self.invocation_count.saturating_add(1);
self.total_revenue = self.total_revenue.saturating_add(creator_share);
self.updated_at = Timestamp(chrono::Utc::now().timestamp());
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct AgentTemplateFilter {
pub template_type: Option<AgentTemplateType>,
pub creator: Option<String>,
pub tag: Option<String>,
pub required_model: Option<String>,
pub free_only: Option<bool>,
pub status: Option<AgentTemplateStatus>,
pub limit: Option<usize>,
pub offset: Option<usize>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AgentTemplateInstance {
pub instance_id: String,
pub template_id: String,
pub owner: Address,
pub custom_name: Option<String>,
pub config_overrides: HashMap<String, String>,
pub deployed_at: Timestamp,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ExecutionSpec {
pub backend: ExecutionBackend,
pub delegation: DelegationSpec,
#[serde(default)]
pub required_tool_tags: Vec<String>,
#[serde(default)]
pub required_skill_tags: Vec<String>,
pub steps: Vec<ExecutionStep>,
pub hard_caps: HardCaps,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ExecutionBackend {
Evm { chain_id: u64 },
Svm { cluster: String },
Daml {
participant_host: String,
participant_port: u16,
},
MultiVm,
MppPayment { endpoint: String },
X402Payment { endpoint: String },
Bridge {
adapters: Vec<String>,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DelegationSpec {
#[serde(with = "u128_serde")]
pub max_transaction_value: u128,
#[serde(with = "u128_serde")]
pub max_daily_spend: u128,
pub allowed_operations: Vec<String>,
#[serde(default)]
pub allowed_chains: Vec<String>,
pub time_bound_days: u32,
pub kyc_tier: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct HardCaps {
#[serde(with = "u128_serde")]
pub per_operation_value: u128,
#[serde(with = "u128_serde")]
pub per_day_value: u128,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SvmAccountMeta {
pub pubkey: String,
#[serde(default)]
pub is_signer: bool,
#[serde(default)]
pub is_writable: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ExecutionStep {
EvmDispatch {
to: String,
value: String,
calldata_template: String,
gas_limit: u64,
},
SvmDispatch {
program_id: String,
accounts: Vec<SvmAccountMeta>,
instruction_data_template: String,
},
NodeRpc {
method: String,
#[serde(default)]
params: serde_json::Value,
},
BridgeTransfer {
from_chain: String,
to_chain: String,
asset: String,
amount: String,
strategy: String,
},
MppPay {
amount: String,
asset: String,
recipient: String,
#[serde(default)]
session_ttl_secs: Option<u64>,
},
X402Pay {
#[serde(default)]
resource_url: Option<String>,
amount: String,
asset: String,
#[serde(default)]
recipient: Option<String>,
#[serde(default)]
facilitator: Option<String>,
},
DamlSubmit {
#[serde(default)]
template_package: Option<String>,
#[serde(default)]
module: Option<String>,
#[serde(default)]
entity: Option<String>,
#[serde(default)]
command_variant: Option<String>,
#[serde(default)]
fields_json: Option<String>,
#[serde(default)]
template_id: Option<String>,
#[serde(default)]
choice: Option<String>,
#[serde(default)]
party: Option<String>,
#[serde(default)]
args_template: Option<String>,
},
ToolInvoke {
tool_tag: String,
#[serde(default)]
method: Option<String>,
#[serde(default)]
arguments_json: Option<String>,
#[serde(default)]
params_template: Option<String>,
},
SkillInvoke {
skill_tag: String,
#[serde(default)]
method: Option<String>,
#[serde(default)]
input_json: Option<String>,
#[serde(default)]
params_template: Option<String>,
},
ForEach {
source_tool_tag: String,
body: Vec<ExecutionStep>,
},
When {
jmespath_expr: String,
body: Vec<ExecutionStep>,
},
}
#[cfg(test)]
mod execution_spec_tests {
use super::*;
#[test]
fn execution_spec_round_trips_through_json() {
let spec = ExecutionSpec {
backend: ExecutionBackend::Bridge {
adapters: vec!["layer_zero".to_string(), "debridge".to_string()],
},
delegation: DelegationSpec {
max_transaction_value: 500_000_000_000_000_000_000u128,
max_daily_spend: 2_000_000_000_000_000_000_000u128,
allowed_operations: vec!["bridge".to_string(), "deposit".to_string()],
allowed_chains: vec!["ethereum".to_string(), "arbitrum".to_string()],
time_bound_days: 7,
kyc_tier: "Full".to_string(),
},
required_tool_tags: vec!["yield-source".to_string()],
required_skill_tags: vec![],
hard_caps: HardCaps {
per_operation_value: 500_000_000_000_000_000_000u128,
per_day_value: 2_000_000_000_000_000_000_000u128,
},
steps: vec![ExecutionStep::ForEach {
source_tool_tag: "yield-source".to_string(),
body: vec![ExecutionStep::When {
jmespath_expr: "apy > `5.0`".to_string(),
body: vec![ExecutionStep::BridgeTransfer {
from_chain: "ethereum".to_string(),
to_chain: "{{opportunity.chain}}".to_string(),
asset: "USDC".to_string(),
amount: "{{opportunity.amount}}".to_string(),
strategy: "cheapest".to_string(),
}],
}],
}],
};
let json = serde_json::to_string(&spec).expect("serialize");
let back: ExecutionSpec = serde_json::from_str(&json).expect("deserialize");
assert_eq!(back, spec);
}
#[test]
fn agent_template_without_execution_spec_still_deserializes() {
let original = AgentTemplate::new(
"Spec-less".to_string(),
"no execution_spec field".to_string(),
AgentTemplateType::Autonomous,
crate::primitives::Address::default(),
"".to_string(),
);
let mut value: serde_json::Value = serde_json::to_value(&original).unwrap();
let removed = value
.as_object_mut()
.unwrap()
.remove("execution_spec");
assert!(removed.is_some(), "execution_spec should have been present");
let stripped = serde_json::to_string(&value).unwrap();
assert!(
!stripped.contains("\"execution_spec\""),
"execution_spec key should be absent from stripped JSON"
);
let tmpl: AgentTemplate =
serde_json::from_str(&stripped).expect("spec-less template should deserialize");
assert!(tmpl.execution_spec.is_none());
assert_eq!(tmpl.name, "Spec-less");
}
#[test]
fn new_template_initializes_execution_spec_to_none() {
let tmpl = AgentTemplate::new(
"Test".to_string(),
"desc".to_string(),
AgentTemplateType::Autonomous,
crate::primitives::Address::default(),
"system prompt".to_string(),
);
assert!(tmpl.execution_spec.is_none());
}
#[test]
fn validate_rejects_paid_template_without_wallet() {
let mut tmpl = AgentTemplate::new(
"Paid".to_string(),
"desc".to_string(),
AgentTemplateType::Autonomous,
crate::primitives::Address::default(),
"prompt".to_string(),
);
tmpl.pricing = AgentPricingModel::PerExecution { price: 1_000 };
assert!(tmpl.validate_marketplace_invariants().is_err());
}
#[test]
fn validate_accepts_paid_template_with_wallet() {
let mut tmpl = AgentTemplate::new(
"Paid".to_string(),
"desc".to_string(),
AgentTemplateType::Autonomous,
crate::primitives::Address::default(),
"prompt".to_string(),
);
tmpl.pricing = AgentPricingModel::PerExecution { price: 1_000 };
tmpl.creator_wallet = Some(crate::primitives::Address::default());
assert!(tmpl.validate_marketplace_invariants().is_ok());
}
#[test]
fn validate_accepts_free_template_without_wallet() {
let tmpl = AgentTemplate::new(
"Free".to_string(),
"desc".to_string(),
AgentTemplateType::Autonomous,
crate::primitives::Address::default(),
"prompt".to_string(),
);
assert!(tmpl.validate_marketplace_invariants().is_ok());
}
#[test]
fn record_invocation_increments_counters() {
let mut tmpl = AgentTemplate::new(
"Paid".to_string(),
"desc".to_string(),
AgentTemplateType::Autonomous,
crate::primitives::Address::default(),
"prompt".to_string(),
);
assert_eq!(tmpl.invocation_count, 0);
assert_eq!(tmpl.total_revenue, 0);
tmpl.record_invocation(950_000_000_000_000_000u128);
assert_eq!(tmpl.invocation_count, 1);
assert_eq!(tmpl.total_revenue, 950_000_000_000_000_000u128);
tmpl.record_invocation(950_000_000_000_000_000u128);
assert_eq!(tmpl.invocation_count, 2);
assert_eq!(tmpl.total_revenue, 1_900_000_000_000_000_000u128);
}
#[test]
fn fee_for_invocation_computes_correctly() {
let free = AgentPricingModel::Free;
assert_eq!(free.fee_for_invocation(1000), 0);
let per_exec = AgentPricingModel::PerExecution { price: 500_000_000_000_000_000u128 };
assert_eq!(per_exec.fee_for_invocation(1000), 500_000_000_000_000_000u128);
let per_token = AgentPricingModel::PerToken { price_per_token: 1_000_000_000u128 };
assert_eq!(per_token.fee_for_invocation(500), 500_000_000_000u128);
let subscription = AgentPricingModel::Subscription { monthly_rate: 1_000_000u128 };
assert_eq!(subscription.fee_for_invocation(1000), 0); }
}