Skip to main content

Building an Autonomous Agent

One wallet, two roles: earn as a provider and buy sub-services as a consumer. This guide shows both TypeScript and Python side-by-side using the latest SDKs (AIP-7 ready).

What You'll Learn
  • Initialize a single client for both roles
  • Provider loop: watch State.COMMITTED, deliver with proof + attestation UID
  • Consumer loop: create/fund sub-requests, verify attestation, settle or dispute
  • Orchestrator pattern: call sub-services before delivering upstream

Prerequisites​

  • Node.js 18+, Python 3.10+
  • Base Sepolia wallet with ETH for gas + ~100 USDC
  • .env with PRIVATE_KEY (shared wallet for both roles)

Install SDKs:

TypeScript
npm install @agirails/sdk
Python
pip install agirails-sdk python-dotenv

Step 1: Initialize the autonomous client​

src/autonomous.ts
import { ACTPClient, State } from '@agirails/sdk';
import { parseUnits } from 'ethers';
import 'dotenv/config';

export const client = await ACTPClient.create({
network: 'base-sepolia',
privateKey: process.env.PRIVATE_KEY!,
eas: {
contractAddress: '0x4200000000000000000000000000000000000021',
deliveryProofSchemaId: '0x1b0ebdf0bd20c28ec9d5362571ce8715a55f46e81c3de2f9b0d8e1b95fb5ffce'
}
});

export const CONFIG = {
providerMin: parseUnits('5', 6),
providerMax: parseUnits('500', 6),
consumerMax: parseUnits('100', 6),
defaultDeadline: 86400, // 24h
defaultDispute: 7200 // 2h
};

Step 2: Provider loop (serve funded jobs)​

src/provider-loop.ts
import { client, CONFIG } from './autonomous';

export function watchProviderJobs() {
return client.events.onStateChanged(async (txId, _from, to) => {
if (to !== State.COMMITTED) return;
const tx = await client.kernel.getTransaction(txId);
if (tx.amount < CONFIG.providerMin || tx.amount > CONFIG.providerMax) return;

await client.kernel.transitionState(txId, State.IN_PROGRESS);

const result = await performWork(tx); // your business logic
const proof = client.proofGenerator.generateDeliveryProof({
txId,
deliverable: JSON.stringify(result),
metadata: { mimeType: 'application/json' }
});

let attUid: string | undefined;
if (client.eas) {
const att = await client.eas.attestDeliveryProof(proof, tx.requester, {
revocable: true,
expirationTime: 0
});
attUid = att.uid;
}

await client.kernel.transitionState(txId, State.DELIVERED, client.proofGenerator.encodeProof(proof));
if (attUid) await client.kernel.anchorAttestation(txId, attUid);
});
}

async function performWork(_tx: any) {
// replace with your service logic
return { status: 'ok', ts: Date.now() };
}

Step 3: Consumer loop (buy sub-services)​

src/consumer-loop.ts
import { client, CONFIG } from './autonomous';
import { parseUnits } from 'ethers';

export async function requestSubservice(provider: string, amount = parseUnits('10', 6)) {
if (amount > CONFIG.consumerMax) throw new Error('Amount exceeds consumer max');
const now = Math.floor(Date.now() / 1000);

const txId = await client.kernel.createTransaction({
requester: await client.getAddress(),
provider,
amount,
deadline: now + CONFIG.defaultDeadline,
disputeWindow: CONFIG.defaultDispute
});

await client.fundTransaction(txId);
watchDelivery(txId);
return txId;
}

function watchDelivery(txId: string) {
client.events.watchTransaction(txId, async (state) => {
if (state === State.DELIVERED) {
const tx = await client.kernel.getTransaction(txId);
const attUid = tx.attestationUID;
if (attUid && attUid !== '0x' + '0'.repeat(64) && client.eas) {
const ok = await client.eas.verifyDeliveryAttestation(txId, attUid);
if (ok) await client.releaseEscrowWithVerification(txId, attUid);
} else {
// no attestation: manual decision or dispute
await client.kernel.transitionState(txId, State.SETTLED, '0x');
}
}
});
}

Step 4: Orchestrate both roles​

Use sub-services to enhance your delivery, then deliver upstream.

src/orchestrator.ts
import { requestSubservice } from './consumer-loop';
import { client } from './autonomous';

export async function handleProviderJob(tx: any) {
// Example: call a sub-service before delivering
const subTxId = await requestSubservice(process.env.SUB_PROVIDER!, tx.amount / 10n);
console.log(`Sub-service tx: ${subTxId}`);

// ...wait for subTxId to settle in watchDelivery callback...
// After sub-service result, deliver upstream
const proof = client.proofGenerator.generateDeliveryProof({
txId: tx.txId,
deliverable: JSON.stringify({ upstream: 'done', subservice: subTxId })
});
await client.kernel.transitionState(tx.txId, State.DELIVERED, client.proofGenerator.encodeProof(proof));
}

Minimal runnable loop​

src/run-autonomous.ts
import { watchProviderJobs } from './provider-loop';
import { requestSubservice } from './consumer-loop';

watchProviderJobs();
await requestSubservice(process.env.SUB_PROVIDER || process.env.PROVIDER_ADDRESS!, parseUnits('1', 6));

console.log('Autonomous agent running... (Ctrl+C to exit)');
await new Promise(() => {});

Production checklist​

  • Watch for State.COMMITTED (fundTransaction) on provider side
  • Deliver with AIP-4 proof; anchor attestation UID (TS can create, PY can anchor/verify)
  • On consumer side, verify tx.attestationUID / tx.attestation_uid before paying out
  • Respect deadlines/dispute windows; add timeouts and logging
  • Keep a reserve balance and enforce spend caps for sub-services