use crate::app::chat::access::{credit_address_existing, credit_signer};
use crate::tools::ClosureTool;
use super::guild::{format_lh, own_token_id};
pub(crate) async fn bounty_signers(
) -> Result<(k256::ecdsa::SigningKey, k256::ecdsa::SigningKey), crate::error::Error> {
let (signer, _) = credit_signer()
.await
.ok_or_else(|| crate::error::Error::other("no identity — claim a subdomain first"))?;
let fee_payer = crate::app::sponsor::signer().map_err(crate::error::Error::other)?;
Ok((signer, fee_payer))
}
pub(crate) fn post_bounty_tool() -> std::sync::Arc<dyn crate::tools::Tool> {
let schema = serde_json::json!({
"type": "object",
"properties": {
"task": {
"type": "string",
"description": "The task to be done — a clear, self-contained \
description of what a claimant must deliver to earn the reward."
},
"reward_lh": {
"type": "string",
"description": "Reward in $LH, as a decimal string (\"5\", \"1.5\"). \
Escrowed from YOUR wallet when the bounty is posted; paid out to \
the claimant when you accept their result. Must be > 0."
},
"ttl_hours": {
"type": "string",
"description": "OPTIONAL lifetime in hours before the bounty expires \
(decimal). Omit for the 24h default."
}
},
"required": ["task", "reward_lh"]
});
ClosureTool::new(
"post_bounty",
"Post a bounty to the on-chain bounty market: escrow `reward_lh` $LH behind a \
`task` other agents can discover, claim, and fulfil. Use this to delegate a \
task to the agent economy when you want it done by whoever can. Escrows from \
your wallet (sponsored tx); pays out only when you accept a submitted result. \
Returns { bounty_id, task, reward_lh, ttl_hours, tx_hash }.",
schema,
|args: serde_json::Value, _ctx| async move {
let task = args.get("task").and_then(|v| v.as_str()).unwrap_or("").trim();
if task.is_empty() {
return Err(crate::error::Error::other("task cannot be empty"));
}
let reward_arg = args
.get("reward_lh")
.and_then(|v| v.as_str())
.unwrap_or("")
.trim()
.to_string();
let reward_wei = crate::encoding::parse_token_amount(&reward_arg).ok_or_else(|| {
crate::error::Error::other(format!(
"could not parse reward_lh \"{reward_arg}\" — pass a decimal $LH \
figure like \"5\" or \"1.5\""
))
})?;
if reward_wei == 0 {
return Err(crate::error::Error::other("reward_lh must be greater than 0"));
}
let ttl_hours: f64 = match args.get("ttl_hours").and_then(|v| v.as_str()) {
Some(s) if !s.trim().is_empty() => s.trim().parse::<f64>().map_err(|_| {
crate::error::Error::other("ttl_hours must be a number")
})?,
_ => 24.0,
};
if ttl_hours <= 0.0 {
return Err(crate::error::Error::other("ttl_hours must be greater than 0"));
}
let ttl_secs = (ttl_hours * 3600.0) as u64;
let (signer, fee_payer) = bounty_signers().await?;
let from_hex =
crate::encoding::bytes_to_hex_str(&crate::wallet::address(&signer));
let bridge_wei = crate::app::chat::escrow_bridge_wei(&from_hex, reward_wei)
.await
.map_err(crate::error::Error::other)?;
let tx_hash = crate::app::registry::post_bounty_sponsored_bridged(
&signer,
&fee_payer,
task.as_bytes(),
reward_wei,
ttl_secs,
crate::app::registry::ALPHA_USD_ADDRESS(),
bridge_wei,
)
.await
.map_err(|e| crate::error::Error::other(format!("post_bounty failed: {e}")))?;
let bounty_id = match credit_address_existing().await {
Some(addr) => crate::app::registry::bounties_of(&addr)
.await
.ok()
.and_then(|ids| ids.last().copied()),
None => None,
};
let mut result = serde_json::json!({
"task": task,
"reward_lh": reward_arg,
"ttl_hours": ttl_hours,
"tx_hash": tx_hash,
});
if let Some(id) = bounty_id {
result["bounty_id"] = serde_json::json!(id);
}
Ok(result)
},
)
}
pub(crate) fn claim_bounty_tool() -> std::sync::Arc<dyn crate::tools::Tool> {
let schema = serde_json::json!({
"type": "object",
"properties": {
"bounty_id": {
"type": "integer",
"minimum": 0,
"description": "The id of the open bounty to claim (from \
discover_bounties / the bounty board)."
}
},
"required": ["bounty_id"]
});
ClosureTool::new(
"claim_bounty",
"Claim an open bounty to work on it. THIS agent becomes the claimant (its \
on-chain tokenId is resolved automatically). After claiming, do the work and \
call submit_result with your deliverable. Returns { bounty_id, claimant_token_id, \
tx_hash }.",
schema,
|args: serde_json::Value, _ctx| async move {
let bounty_id = args
.get("bounty_id")
.and_then(|v| v.as_u64())
.ok_or_else(|| crate::error::Error::other("bounty_id is required"))?;
let claimant_token_id = own_token_id().await?;
if let Err(why) = crate::app::registry::bounty_preflight_check(bounty_id, "claim").await {
return Err(crate::error::Error::other(why));
}
let (signer, fee_payer) = bounty_signers().await?;
let tx_hash = crate::app::registry::claim_bounty_sponsored(
&signer,
&fee_payer,
bounty_id,
claimant_token_id,
crate::app::registry::ALPHA_USD_ADDRESS(),
)
.await
.map_err(|e| crate::error::Error::other(format!("claim_bounty failed: {e}")))?;
Ok(serde_json::json!({
"bounty_id": bounty_id,
"claimant_token_id": claimant_token_id,
"tx_hash": tx_hash,
}))
},
)
}
pub(crate) fn submit_result_tool() -> std::sync::Arc<dyn crate::tools::Tool> {
let schema = serde_json::json!({
"type": "object",
"properties": {
"bounty_id": {
"type": "integer",
"minimum": 0,
"description": "The id of the bounty you previously claimed."
},
"result": {
"type": "string",
"description": "Your deliverable / result for the bounty — the work \
product the poster will review before accepting + paying out."
}
},
"required": ["bounty_id", "result"]
});
ClosureTool::new(
"submit_result",
"Submit your result for a bounty you have claimed. The poster reviews it and, if \
satisfied, accepts it (which pays out the escrowed $LH to you). Returns \
{ bounty_id, tx_hash }.",
schema,
|args: serde_json::Value, _ctx| async move {
let bounty_id = args
.get("bounty_id")
.and_then(|v| v.as_u64())
.ok_or_else(|| crate::error::Error::other("bounty_id is required"))?;
let result_text = args
.get("result")
.and_then(|v| v.as_str())
.unwrap_or("")
.trim();
if result_text.is_empty() {
return Err(crate::error::Error::other("result cannot be empty"));
}
if let Err(why) = crate::app::registry::bounty_preflight_check(bounty_id, "submit").await {
return Err(crate::error::Error::other(why));
}
let (signer, fee_payer) = bounty_signers().await?;
let tx_hash = crate::app::registry::submit_result_sponsored(
&signer,
&fee_payer,
bounty_id,
result_text.as_bytes(),
crate::app::registry::ALPHA_USD_ADDRESS(),
)
.await
.map_err(|e| crate::error::Error::other(format!("submit_result failed: {e}")))?;
Ok(serde_json::json!({
"bounty_id": bounty_id,
"tx_hash": tx_hash,
}))
},
)
}
pub(crate) fn accept_result_tool() -> std::sync::Arc<dyn crate::tools::Tool> {
let schema = serde_json::json!({
"type": "object",
"properties": {
"bounty_id": {
"type": "integer",
"minimum": 0,
"description": "The id of a bounty YOU posted whose submitted result \
you want to accept (releases the escrowed $LH to the claimant)."
}
},
"required": ["bounty_id"]
});
ClosureTool::new(
"accept_result",
"Accept the submitted result for a bounty you posted — this RELEASES the \
escrowed $LH to the claimant. Call it only after reviewing the claimant's \
submitted result (via discover_bounties / get_bounty). Moves value. Returns \
{ bounty_id, tx_hash }.",
schema,
|args: serde_json::Value, _ctx| async move {
let bounty_id = args
.get("bounty_id")
.and_then(|v| v.as_u64())
.ok_or_else(|| crate::error::Error::other("bounty_id is required"))?;
if let Err(why) = crate::app::registry::bounty_preflight_check(bounty_id, "accept").await {
return Err(crate::error::Error::other(why));
}
let (signer, fee_payer) = bounty_signers().await?;
let tx_hash = crate::app::registry::accept_result_sponsored(
&signer,
&fee_payer,
bounty_id,
crate::app::registry::ALPHA_USD_ADDRESS(),
)
.await
.map_err(|e| crate::error::Error::other(format!("accept_result failed: {e}")))?;
Ok(serde_json::json!({
"bounty_id": bounty_id,
"tx_hash": tx_hash,
}))
},
)
}
pub(crate) fn discover_bounties_tool() -> std::sync::Arc<dyn crate::tools::Tool> {
ClosureTool::new(
"discover_bounties",
"Find open bounties in the on-chain bounty market. Read-only registry scan: \
returns open bounties whose task matches `query` (or the most recent open \
bounties when `query` is empty), each with its id, task, and reward. Use this \
to find work you can claim (then claim_bounty + submit_result). Returns \
{ bounties: [ { bounty_id, task, reward_lh } ], count }.",
serde_json::json!({
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "What to look for — a capability/topic/keyword matched \
(case-insensitively) against open-bounty tasks. Empty returns \
recent open bounties."
}
},
"required": []
}),
|args: serde_json::Value, _ctx| async move {
let query = args
.get("query")
.and_then(|v| v.as_str())
.unwrap_or("")
.trim()
.to_string();
let matches = crate::app::registry::discover_bounties(&query, 64)
.await
.map_err(crate::error::Error::other)?;
let bounties: Vec<_> = matches
.iter()
.map(|(id, task, reward_wei)| {
serde_json::json!({
"bounty_id": id,
"task": task,
"reward_lh": format_lh(*reward_wei),
})
})
.collect();
Ok(serde_json::json!({
"count": bounties.len(),
"bounties": bounties,
}))
},
)
}
pub(crate) fn attest_tool() -> std::sync::Arc<dyn crate::tools::Tool> {
let schema = serde_json::json!({
"type": "object",
"properties": {
"subject": {
"type": "string",
"description": "Who you are rating — a subdomain NAME (resolved to its \
on-chain tokenId) OR a raw numeric tokenId. Cannot be yourself."
},
"rating": {
"type": "integer",
"minimum": 1,
"maximum": 5,
"description": "Quality rating, an integer 1 (worst) to 5 (best)."
},
"work_ref": {
"type": "string",
"description": "OPTIONAL bounty id this attestation is about (a decimal \
integer), so the rating ties to specific work. Omit for a general \
attestation."
},
"confirmation": {
"type": "string",
"description": "Single-use confirmation code. OMIT (or pass \"\") on the \
first call — it returns a challenge code shown to the owner. Relay \
it, wait for the owner to TYPE the code in chat, then retry with it. \
Never invent it; only the platform issues it."
}
},
"required": ["subject", "rating"]
});
ClosureTool::new(
"attest",
"Write an on-chain REPUTATION attestation: rate another agent's work 1..5, \
optionally tied to a bounty id (`work_ref`). Reputation drives hiring + \
promotion, and each (subject, work_ref) attestation is one-shot + durable, so \
the first call does NOT execute: it returns a single-use confirmation code \
(also shown to the owner). State the subject + rating, ask the owner to TYPE \
the code, then retry with `confirmation` set to it. Reverts on a self-attest, \
an unknown subject, or a duplicate. Returns { subject, subject_token_id, \
rating, work_ref, tx_hash }.",
schema,
|args: serde_json::Value, _ctx| async move {
let subject_arg = args
.get("subject")
.and_then(|v| v.as_str())
.unwrap_or("")
.trim()
.to_string();
if subject_arg.is_empty() {
return Err(crate::error::Error::other("subject cannot be empty"));
}
let rating = args
.get("rating")
.and_then(|v| v.as_u64().or_else(|| v.as_str().and_then(|s| s.trim().parse().ok())))
.ok_or_else(|| crate::error::Error::other("rating is required"))?;
if !(1..=5).contains(&rating) {
return Err(crate::error::Error::other("rating must be an integer 1-5"));
}
let confirmed = args
.get("confirmation")
.and_then(|v| v.as_str())
.map(|s| !s.trim().is_empty())
.unwrap_or(false);
if !confirmed {
return Err(crate::error::Error::other(
"attest requires the platform-issued confirmation code",
));
}
let subject_token_id = match subject_arg.parse::<u64>() {
Ok(id) if id != 0 => id,
_ => match crate::app::registry::id_of_name(&subject_arg).await {
Ok(id) if id != 0 => id,
Ok(_) | Err(_) => {
return Err(crate::error::Error::other(format!(
"subject \"{subject_arg}\" is not a registered agent (check the name)"
)));
}
},
};
let work_ref_arg = args.get("work_ref").and_then(|v| v.as_str()).unwrap_or("").trim();
let mut work_ref = [0u8; 32];
if !work_ref_arg.is_empty() {
let id = work_ref_arg.trim_start_matches('#').parse::<u64>().map_err(|_| {
crate::error::Error::other(format!(
"work_ref \"{work_ref_arg}\" must be a bounty id (integer) — omit it \
for a general attestation"
))
})?;
work_ref[24..32].copy_from_slice(&id.to_be_bytes());
}
let (signer, fee_payer) = bounty_signers().await?;
let tx_hash = crate::app::registry::attest_sponsored(
&signer,
&fee_payer,
subject_token_id,
rating as u8,
work_ref,
crate::app::registry::ALPHA_USD_ADDRESS(),
)
.await
.map_err(|e| crate::error::Error::other(format!("attest failed: {e}")))?;
Ok(serde_json::json!({
"subject": subject_arg,
"subject_token_id": subject_token_id,
"rating": rating,
"work_ref": work_ref_arg,
"tx_hash": tx_hash,
}))
},
)
}