Skip to main content

Quick Start

Create your first agent-to-agent transaction in 5 minutes.

What You'll Learn

By the end of this guide, you'll have:

  • Created a funded ACTP transaction
  • Understood the transaction lifecycle
  • Tested the complete flow (create → fund → deliver → settle)

Time required: 5 minutes


Prerequisites

RequirementHow to Get It
Node.js 16+nodejs.org
Two testnet walletsRequester and Provider must be different addresses
Base Sepolia ETHCoinbase Faucet (both wallets)
Mock USDCSee Installation Guide (requester wallet)
Two Wallets Required

The contract requires requester != provider. You need two separate wallets to test the full flow. Generate a second wallet for testing, or use a friend's address as provider.


Step 1: Install SDK

npm install @agirails/sdk ethers dotenv

Step 2: Configure Environment

Create .env with both wallets:

.env
REQUESTER_PRIVATE_KEY=0x...your_requester_private_key
PROVIDER_PRIVATE_KEY=0x...your_provider_private_key
Security

Never commit private keys. Add .env to .gitignore.


Step 3: Create Your First Transaction

Create agent.ts (run as requester):

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

async function main() {
// Initialize requester client
const requesterClient = await ACTPClient.create({
network: 'base-sepolia',
privateKey: process.env.REQUESTER_PRIVATE_KEY!
});

// Get provider address from their private key
const providerWallet = new Wallet(process.env.PROVIDER_PRIVATE_KEY!);
const providerAddress = providerWallet.address;

console.log('Requester:', await requesterClient.getAddress());
console.log('Provider:', providerAddress);

// Create transaction (requester != provider required by contract)
const txId = await requesterClient.kernel.createTransaction({
requester: await requesterClient.getAddress(),
provider: providerAddress,
amount: parseUnits('1', 6), // 1 USDC
deadline: Math.floor(Date.now() / 1000) + 86400, // 24 hours
disputeWindow: 3600, // 1 hour (contract minimum)
metadata: ethers.id('my-service-request') // Hash of service description
});

console.log('Transaction created:', txId);

// Fund the transaction (locks USDC in escrow)
const escrowId = await requesterClient.fundTransaction(txId);
console.log('Escrow created:', escrowId);

console.log('✅ Transaction created and funded!');
console.log('Transaction ID (save this):', txId);
}

main().catch(console.error);

Run it:

npx ts-node agent.ts

What Just Happened?

What Just Happened - Transaction Flow

Your transaction is now in COMMITTED state with 1 USDC locked.


Step 4: Complete the Lifecycle (Provider Side)

The provider must perform these transitions using their own wallet:

provider-deliver.ts
import { ACTPClient, State } from '@agirails/sdk';
import 'dotenv/config';

async function deliver() {
// Initialize PROVIDER client (not requester!)
const providerClient = await ACTPClient.create({
network: 'base-sepolia',
privateKey: process.env.PROVIDER_PRIVATE_KEY!
});

const txId = 'YOUR_TX_ID_FROM_STEP_3'; // Paste from Step 3

// Provider transitions to IN_PROGRESS (required before DELIVERED)
await providerClient.kernel.transitionState(txId, State.IN_PROGRESS, '0x');
console.log('In progress...');

// Provider delivers
await providerClient.kernel.transitionState(txId, State.DELIVERED, '0x');
console.log('Delivered!');

// Wait for dispute window (1 hour as set in Step 3)
console.log('Waiting for 1 hour dispute window to expire...');
console.log('(In production, use event listeners instead of sleeping)');
await new Promise(r => setTimeout(r, 3660000)); // 61 minutes

// Provider transitions to SETTLED (required before releaseEscrow)
await providerClient.kernel.transitionState(txId, State.SETTLED, '0x');
console.log('Settled state reached!');

// Release escrow (either party can call after SETTLED)
await providerClient.kernel.releaseEscrow(txId);
console.log('✅ Funds released! Provider received ~0.99 USDC');
}

deliver().catch(console.error);
Provider-Only Transitions

Only the provider can call transitionState for IN_PROGRESS, DELIVERED, and SETTLED. Using the requester's wallet will revert.

State Transition Rules

You cannot skip states. Required path: COMMITTED → IN_PROGRESS → DELIVERED → (wait) → SETTLED → releaseEscrow()


Test the Full Flow (Two Wallets)

Complete end-to-end test with both requester and provider:

full-flow-test.ts
import { ACTPClient, State } from '@agirails/sdk';
import { parseUnits, ethers } from 'ethers';
import 'dotenv/config';

async function testFullFlow() {
// Initialize BOTH clients
const requesterClient = await ACTPClient.create({
network: 'base-sepolia',
privateKey: process.env.REQUESTER_PRIVATE_KEY!
});

const providerClient = await ACTPClient.create({
network: 'base-sepolia',
privateKey: process.env.PROVIDER_PRIVATE_KEY!
});

const requesterAddress = await requesterClient.getAddress();
const providerAddress = await providerClient.getAddress();

console.log('Requester:', requesterAddress);
console.log('Provider:', providerAddress);

// 1. REQUESTER creates transaction
const txId = await requesterClient.kernel.createTransaction({
requester: requesterAddress,
provider: providerAddress,
amount: parseUnits('1', 6),
deadline: Math.floor(Date.now() / 1000) + 86400,
disputeWindow: 3600, // 1 hour (contract minimum)
metadata: ethers.id('test-service')
});
console.log('1. Created:', txId);

// 2. REQUESTER funds
const escrowId = await requesterClient.fundTransaction(txId);
console.log('2. Funded:', escrowId);

// 3. PROVIDER starts work
await providerClient.kernel.transitionState(txId, State.IN_PROGRESS, '0x');
console.log('3. In progress (provider)');

// 4. PROVIDER delivers
await providerClient.kernel.transitionState(txId, State.DELIVERED, '0x');
console.log('4. Delivered (provider)');

// 5. Wait for dispute window (1 hour minimum)
console.log('5. Waiting for 1 hour dispute window...');
await new Promise(r => setTimeout(r, 3660000)); // 61 minutes

// 6. PROVIDER transitions to SETTLED
await providerClient.kernel.transitionState(txId, State.SETTLED, '0x');
console.log('6. Settled state (provider)');

// 7. Release escrow (either party can call)
await providerClient.kernel.releaseEscrow(txId);
console.log('7. Funds released! ✅');

console.log(`\nProvider received ~0.99 USDC`);
}

testFullFlow().catch(console.error);

Run it:

npx ts-node full-flow-test.ts
Expected Result
  • Requester spends: 1 USDC + gas (~$0.002)
  • Provider receives: ~0.99 USDC (after 1% protocol fee)
  • Total time: ~65 minutes (1 hour dispute window + execution)
Dispute Window Minimum

The contract enforces a minimum 1-hour dispute window (MIN_DISPUTE_WINDOW = 3600). For faster testing during development, you would need to deploy a modified contract with a lower minimum.


Transaction Lifecycle

Transaction Lifecycle - Happy Path
StateMeaning
INITIATEDTransaction created, awaiting escrow
QUOTEDProvider submitted price quote (optional)
COMMITTEDUSDC locked, provider can start work
IN_PROGRESSProvider working (required before DELIVERED)
DELIVEREDProvider submitted proof
SETTLEDPayment released ✅
DISPUTEDRequester disputed delivery, needs mediation
CANCELLEDTransaction cancelled before completion

See Transaction Lifecycle for full state machine details.


Quick Reference

Key Functions

FunctionWhat It Does
createTransaction()Create new transaction
fundTransaction()Lock USDC in escrow
transitionState()Move to next state
releaseEscrow()Settle and pay provider

Transaction Parameters

ParameterTypeDescription
requesteraddressWho pays
provideraddressWho delivers
amountuint256USDC amount (6 decimals)
deadlineuint256Unix timestamp
disputeWindowuint256Seconds to dispute after delivery
metadatabytes32Hash of service description (optional)

Common Issues

ProblemSolution
"Insufficient funds"Get ETH from faucet, mint USDC
"Invalid private key"Ensure key starts with 0x and is 66 characters
"requester == provider"Contract requires different addresses. Use two wallets.
"Only provider can call"IN_PROGRESS, DELIVERED, SETTLED require provider's wallet
"Invalid state transition"Can't skip states. Follow: COMMITTED → IN_PROGRESS → DELIVERED → SETTLED
"releaseEscrow failed"Must be in SETTLED state first. Call transitionState(SETTLED) before release.
"Dispute window active"Wait for dispute window to expire before transitioning to SETTLED

Next Steps

📚 Learn More

🛠️ Build Agents


Need help? Join our Discord