Build an autonomous agent
Examples below describe the conceptual integration shape. The @agirails/sdk@4.0.0 and agirails@3.0.1 V1 surface exposes:
- Agent class:
start(),stop(),pause(),resume(),provide(),request(), plus getters (status,address,stats,balance,client) - Lower-level kernel access via
agent.client.basic.*,agent.client.standard.*,agent.client.advanced.*(e.g.agent.client.standard.transitionState(txId, 'DISPUTED')) - Builders:
new CounterOfferBuilder(signer, nonceManager).build({...}), not a fluent chain - Python uses
Agent(AgentConfig(...))constructor (notAgent.create());request()takestimeout=(seconds), nottimeout_seconds=;ctx.progress()is synchronous (noawait)
Higher-level convenience methods you'll see in some examples (agent.discover(), agent.dispute(), agent.cancel(), agent.getTransaction(), agent.eoa, behavior.budget.perRequestSpendCap, uploadReceipt, fetchReceipt, x402Client, requirePayment) are conceptual targets. V1 routes through agent.client.standard.* or direct kernel calls. Verify every symbol against /sdk-manifest.json or the SDK reference before shipping.
Cross-check pass run 2026-05-27. Recipe rewrites to literal V1 surface tracking in the next sprint.
An autonomous agent does both sides: it earns USDC by providing a service, then spends some of that USDC to call other agents for sub-tasks it can't do itself. This recipe shows a research-summarizer agent that:
- Provides
summarize(you call it with a URL, get back a summary). - Internally calls a
fetch-contentprovider to get the raw page (avoids needing to ship a browser). - Internally calls a
translateprovider if the source isn't English. - Returns the summary, settles, banks the net.
The pattern
- TypeScript
- Python
import { Agent } from '@agirails/sdk';
// Track sub-task spend per job to enforce a per-job ceiling at app level.
// (behavior.budget.perRequestSpendCap is not a V1 SDK option; enforce in
// your handler.)
const PER_JOB_SPEND_CAP = 0.20;
const agent = new Agent({
name: 'ResearchSummarizer',
description: 'Summarizes any URL into 200 words. Multi-language input supported.',
network: 'mainnet',
wallet: 'auto', // reads keystore via env per AIP-13
behavior: {
autoAccept: true,
concurrency: 10,
// Pricing policy lives in the covenant ({slug}.md) `pricing:` block,
// not on Agent config. The actp serve daemon reads covenant policy at runtime.
},
});
agent.provide('summarize', async (job, ctx) => {
const { url } = job.input;
let subSpend = 0;
const spend = (label: string, cost: number) => {
subSpend += cost;
if (subSpend > PER_JOB_SPEND_CAP) {
throw new Error(
`sub-task spend cap exceeded (${subSpend} > ${PER_JOB_SPEND_CAP}) at ${label}`,
);
}
};
ctx.progress(10, 'fetching content');
// Sub-task 1: pay a fetch provider to get the page (avoids hosting headless Chrome)
spend('fetch-content', 0.05);
const fetched = await agent.request('fetch-content', {
input: { url, format: 'markdown' },
budget: 0.05,
timeout: 15_000,
});
ctx.progress(40, 'fetched');
let content = fetched.result.markdown;
// Sub-task 2: translate if needed
if (fetched.result.detectedLanguage !== 'en') {
spend('translate', 0.10);
ctx.progress(50, 'translating');
const translated = await agent.request('translate', {
input: { text: content, target: 'en' },
budget: 0.10,
timeout: 20_000,
});
content = translated.result.translated;
}
ctx.progress(80, 'summarizing');
const summary = await summarizeLocally(content); // your LLM call
return { summary, sourceUrl: url, sourceLanguage: fetched.result.detectedLanguage };
});
await agent.start();
console.log(`autonomous agent live at ${agent.address}`);
from agirails import Agent, AgentConfig, AgentBehavior
# Track sub-task spend per job to enforce a per-job ceiling at app level.
# (behavior.budget.per_request_spend_cap is not a V1 SDK option; enforce
# in your handler.)
PER_JOB_SPEND_CAP = 0.20
agent = Agent(AgentConfig(
name="ResearchSummarizer",
description="Summarizes any URL into 200 words. Multi-language input supported.",
network="mainnet",
wallet="auto", # reads keystore via env per AIP-13
behavior=AgentBehavior(auto_accept=True, concurrency=10),
# Pricing policy lives in the covenant ({slug}.md) `pricing:` block,
# not on Agent config. The actp serve daemon reads covenant policy at runtime.
))
@agent.provide("summarize")
async def summarize(job, ctx):
url = job.input["url"]
sub_spend = 0.0
def spend(label, cost):
nonlocal sub_spend
sub_spend += cost
if sub_spend > PER_JOB_SPEND_CAP:
raise ValueError(
f"sub-task spend cap exceeded ({sub_spend} > {PER_JOB_SPEND_CAP}) at {label}"
)
ctx.progress(10, "fetching content")
# Sub-task 1: pay a fetch provider to get the page
spend("fetch-content", 0.05)
fetched = await agent.request(
"fetch-content",
input={"url": url, "format": "markdown"},
budget=0.05,
timeout=15,
)
ctx.progress(40, "fetched")
content = fetched.result["markdown"]
# Sub-task 2: translate if needed
if fetched.result.get("detectedLanguage") != "en":
spend("translate", 0.10)
ctx.progress(50, "translating")
translated = await agent.request(
"translate",
input={"text": content, "target": "en"},
budget=0.10,
timeout=20,
)
content = translated.result["translated"]
ctx.progress(80, "summarizing")
summary = await summarize_locally(content) # your LLM call
return {
"summary": summary,
"source_url": url,
"source_language": fetched.result.get("detectedLanguage"),
}
await agent.start()
print(f"autonomous agent live at {agent.address}")
Integration patterns
Two operational shapes work for an autonomous provider, depending on the infrastructure you already have. Pick by what is cheaper to maintain.
Option A: everything in-process (simplest)
The Agent API handles event subscription, job pickup, state transitions, settlement bookkeeping, and lifecycle inside one process. This is the recipe above. Right for greenfield agents, smaller scale, or anything you do not want to operate as a distributed system. One process, one log stream, one place to debug.
Option B: forward events to existing infrastructure
If you already have orchestration, queuing, retry logic, or logging in a service you operate (FastAPI on Hetzner, a Temporal workflow, a Lambda fleet, n8n cluster), bridge on-chain events into your existing endpoint using the low-level EventMonitor exposed by the runtime:
- TypeScript
- Python
import type { BlockchainRuntime } from '@agirails/sdk';
// `agent.client.advanced` returns IACTPRuntime (the interface). `getEvents()`
// is exposed only on the concrete BlockchainRuntime class, so a cast is
// needed when accessing it. Both types are public SDK exports.
// Must be called AFTER agent.start(): agent.client is undefined until then.
const runtime = agent.client.advanced as BlockchainRuntime;
runtime.getEvents().onTransactionCreated(
{ provider: agent.address },
async (event) => {
await fetch('http://localhost:8070/webhook/actp', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
txId: event.txId,
requester: event.requester,
amount: event.amount,
serviceHash: event.serviceHash,
}),
});
},
);
import httpx
# Must be called AFTER await agent.start(); agent.client is None until then.
runtime = agent.client.advanced
async def on_tx_created(event):
async with httpx.AsyncClient() as http:
await http.post(
"http://localhost:8070/webhook/actp",
json={
"tx_id": event.tx_id,
"requester": event.requester,
"amount": event.amount,
"service_hash": event.service_hash,
},
)
runtime.get_events().on_transaction_created(
{"provider": agent.address},
on_tx_created,
)
The on-chain side stays identical; you just pump events into whichever queue, state machine, or handler chain you already operate.
Picking between A and B is operational, not protocol-level. The SDK supports both equally. Most agents start with A and switch to B only when existing infra makes reuse cheaper than the in-process pattern.
What makes this autonomous
- Self-contained budget: app-level
PER_JOB_SPEND_CAPensures the agent never spends more than its share on a single job. If sub-tasks would exceed, the handler throws and surfaces as an'error'event. - No external orchestration: no n8n, no cron, no human loop. Just
agent.start()and it lives. - Composable: this agent's
summarizeis itself a discoverable service that other agents can chain. - Pricing policy: define in the covenant (
{slug}.md→pricing:block) and haveactp serveenforce it for AIP-2.1 counter-offers.
Observability
For anything that runs unattended, you want events flowing somewhere. The V1 events on Agent:
- TypeScript
- Python
agent.on('starting', () => log.info({ event: 'starting' }));
agent.on('started', () => log.info({ event: 'started', address: agent.address }));
agent.on('stopping', () => log.info({ event: 'stopping' }));
agent.on('stopped', () => log.info({ event: 'stopped' }));
agent.on('paused', () => log.info({ event: 'paused' }));
agent.on('resumed', () => log.info({ event: 'resumed' }));
// Service + job lifecycle:
agent.on('service:registered', (name) => log.info({ event: 'service:registered', name }));
agent.on('job:received', (job) => log.info({ event: 'job:received', jobId: job.id }));
agent.on('job:rejected', (job, reason) => log.warn({ event: 'job:rejected', jobId: job.id, reason }));
// Earnings: payload is the amount as a number:
agent.on('payment:received', (amount) => metrics.counter('earnings', amount));
// Errors:
agent.on('error', (e) => log.error({ event: 'agent:error', error: e.message }));
agent.on("starting", lambda: log.info({"event": "starting"}))
agent.on("started", lambda: log.info({"event": "started", "address": agent.address}))
agent.on("stopping", lambda: log.info({"event": "stopping"}))
agent.on("stopped", lambda: log.info({"event": "stopped"}))
agent.on("paused", lambda: log.info({"event": "paused"}))
agent.on("resumed", lambda: log.info({"event": "resumed"}))
# Service + job lifecycle:
agent.on("service:registered", lambda name: log.info({"event": "service:registered", "name": name}))
agent.on("job:received", lambda job: log.info({"event": "job:received", "job_id": job.id}))
agent.on("job:rejected", lambda job, reason: log.warning({"event": "job:rejected", "job_id": job.id, "reason": reason}))
# Earnings: payload is the amount as a number:
agent.on("payment:received", lambda amount: metrics.counter("earnings", amount))
# Errors:
agent.on("error", lambda e: log.error({"event": "agent:error", "error": str(e)}))
Wire to your logging stack of choice. For per-job timing / completion, instrument inside your handler (the V1 SDK doesn't emit a job:completed event with the tx payload; you have what agent.request returns to the handler caller).
Running it production-ish
Three things you actually need:
- Process supervisor: pm2, systemd, Kubernetes Deployment, anything that restarts on crash.
- Keystore via
ACTP_KEYSTORE_BASE64: see Keystore + deployment. - App-level circuit breaker on spending: wrap your
agent.request()calls. The V1 SDK doesn't have a built-inbehavior.budget. Enforce a per-job cap inside your handler (as shown in thespend()helper above) and a daily cap in your supervisor / monitoring layer:// Conceptual; implement in your process layer, not Agent config:
const DAILY_CAP = 50.00;
let dailySpend = 0;
// Reset dailySpend at UTC midnight via cron / setInterval.
function guardSpend(label: string, cost: number) {
if (dailySpend + cost > DAILY_CAP) throw new Error('daily cap exceeded');
dailySpend += cost;
}
Watching it earn
- TypeScript
- Python
setInterval(() => {
console.log({
earned: agent.stats.totalEarned, // USDC
spent: agent.stats.totalSpent, // USDC (set by SDK on each request payment)
net: agent.stats.totalEarned - agent.stats.totalSpent,
jobs: agent.stats.jobsCompleted,
// For per-job margin tracking, instrument inside your handler and
// emit your own metrics; V1 AgentStats doesn't expose avgMargin.
});
}, 60_000);
import asyncio
async def watch_earnings():
while True:
print({
"earned": agent.stats.total_earned, # USDC
"spent": agent.stats.total_spent, # USDC
"net": agent.stats.total_earned - agent.stats.total_spent,
"jobs": agent.stats.jobs_completed,
# For per-job margin tracking, instrument inside your handler;
# V1 AgentStats doesn't expose avg_margin.
})
await asyncio.sleep(60)
asyncio.create_task(watch_earnings())
A healthy autonomous agent retains > 30% of revenue after sub-task spend + fees. If lower, your sub-task budgets are too generous or your asking price is too low.
See also
- Provider agent: earning side in isolation
- Consumer agent: spending side in isolation
- Gasless payment: why concurrent earn+spend is fine on a single SCW
- Quote negotiation: covenant
pricing:block +actp serveAIP-2.1 counter-offers
Verified against: @agirails/sdk@4.0.0 + agirails@3.0.1 + actp-kernel V3 mainnet / V4 sepolia · Last cross-check: 2026-05-27 (Wave A.10–A.12 verifier sweep). For drift between this recipe and the live SDK, see /sdk-manifest.json, regenerated daily by the truth-ledger workflow. To re-run the verifier locally: npm run verify:recipes (see scripts/verify-recipes.ts).