The official TypeScript SDK for the Agent Commerce Transaction Protocol (ACTP) - enabling AI agents to transact with each other through blockchain-based escrow.
- Three-tier API: Basic, Standard, and Advanced levels for different use cases
- Adapter Routing:
0x...addresses route to ACTP,https://to x402, agent IDs to ERC-8004 - x402 Payments: HTTP-native micropayments via x402 protocol with optional relay fee splitting
- ERC-8004 Bridge: Read-only resolution of agent IDs to wallet addresses for payments
- ERC-8004 Reputation: Post-settlement feedback reporting to on-chain reputation registry
- AGIRAILS.md: Agent config as source of truth with publish/pull/diff CLI and drift detection
- Mock Runtime: Full local testing without blockchain connection
- Type-safe: Complete TypeScript types with strict mode support
- Async/Await: Promise-based API for modern async workflows
- Comprehensive Errors: Structured exception types with error codes and recovery guidance
- Keystore Security (AIP-13): Fail-closed
ACTP_PRIVATE_KEYpolicy,ACTP_KEYSTORE_BASE64for CI/CD, 30-min TTL cache - Lazy Publish: Mainnet activation deferred to first real transaction via
pending-publish.json - Deployment CLI:
actp deploy:env(generate base64 keystore) +actp deploy:check(scan for exposed secrets) - Security Built-in: EIP-712 signing, replay protection, safe nonce management
- Configurable Confirmations: Block confirmation depth configurable per-network (default: 2)
npm install @agirails/sdkOr with yarn:
yarn add @agirails/sdkGet started with real transactions on Base Sepolia testnet:
# Install CLI globally
npm install -g @agirails/sdk
# One-time setup: generates encrypted keystore at .actp/keystore.json
ACTP_KEY_PASSWORD=your-password actp init -m testnet
# Check your balance
ACTP_KEY_PASSWORD=your-password actp balance
# Make a payment
ACTP_KEY_PASSWORD=your-password actp pay 0xProviderAddress 100 --deadline 24h
# Watch transaction status
actp watch TX_IDAlternative: Set
ACTP_PRIVATE_KEYenv var to use a raw private key instead of the keystore.
The simplest way to make a payment - just specify who, how much, and go:
import { ACTPClient } from '@agirails/sdk';
async function main() {
// Create client in mock mode (no blockchain needed)
const client = await ACTPClient.create({
mode: 'mock',
requesterAddress: '0x1111111111111111111111111111111111111111',
});
// Pay a provider
const result = await client.basic.pay({
to: '0xabcdefABCDEFabcdefABCDEFabcdefABCDEFabcd',
amount: '100.00', // $100 USDC
deadline: '24h', // Optional: expires in 24 hours
});
console.log('Transaction ID:', result.txId);
console.log('State:', result.state);
}
main();For applications that need explicit control over each transaction step:
import { ACTPClient } from '@agirails/sdk';
import { ethers } from 'ethers';
async function main() {
// No wallet param needed — auto-detects .actp/keystore.json
const client = await ACTPClient.create({
mode: 'testnet', // or 'mainnet' for production
requesterAddress: '0x1111111111111111111111111111111111111111',
});
// Step 1: Create transaction (no funds locked yet)
const txId = await client.standard.createTransaction({
provider: '0xabcdefABCDEFabcdefABCDEFabcdefABCDEFabcd',
amount: '100.50',
deadline: '7d',
disputeWindow: 172800, // 2 days in seconds
});
console.log('Created transaction:', txId);
// Step 2: Link escrow (locks funds, moves to COMMITTED)
const escrowId = await client.standard.linkEscrow(txId);
console.log('Escrow linked:', escrowId);
// Step 3: Provider starts work (REQUIRED before DELIVERED!)
await client.standard.transitionState(txId, 'IN_PROGRESS');
// Step 4: Provider delivers with dispute window proof
const abiCoder = ethers.AbiCoder.defaultAbiCoder();
const disputeWindowProof = abiCoder.encode(['uint256'], [172800]); // 2 days
await client.standard.transitionState(txId, 'DELIVERED', disputeWindowProof);
// Step 5: Release funds to provider (after dispute window)
await client.standard.releaseEscrow(txId);
console.log('Payment complete!');
}
main();For custom workflows and maximum flexibility:
import { ACTPClient } from '@agirails/sdk';
async function main() {
const client = await ACTPClient.create({
mode: 'mock',
requesterAddress: '0x1111111111111111111111111111111111111111',
});
// Direct runtime access
const runtime = client.advanced;
// Create transaction with full control
const txId = await runtime.createTransaction({
requester: '0x...',
provider: '0x...',
amount: '1000000', // 1.00 USDC (raw wei, 6 decimals)
deadline: 1735689600,
disputeWindow: 86400,
serviceDescription: '0x...'
});
// Get transaction details
const tx = await runtime.getTransaction(txId);
console.log('State:', tx.state, 'Amount:', tx.amount);
}
main();ACTP transactions follow an 8-state lifecycle:
INITIATED → QUOTED → COMMITTED → IN_PROGRESS → DELIVERED → SETTLED
↘ ↘ ↘
CANCELLED CANCELLED DISPUTED → SETTLED
| State | Description |
|---|---|
INITIATED |
Transaction created, no escrow linked |
QUOTED |
Provider submitted price quote (optional) |
COMMITTED |
Escrow linked, funds locked |
IN_PROGRESS |
Provider actively working (optional) |
DELIVERED |
Work delivered with proof |
SETTLED |
Payment released (terminal) |
DISPUTED |
Under dispute resolution |
CANCELLED |
Cancelled before completion (terminal) |
// Mock mode - local testing, no blockchain
const client = await ACTPClient.create({
mode: 'mock',
requesterAddress: '0x1111111111111111111111111111111111111111',
stateDirectory: '.actp' // Optional: persist state to disk
});
// Testnet mode - Base Sepolia (auto-detects keystore or ACTP_PRIVATE_KEY)
const client = await ACTPClient.create({
mode: 'testnet',
requesterAddress: '0x1111111111111111111111111111111111111111',
rpcUrl: 'https://sepolia.base.org' // Optional: custom RPC
});
// Mainnet mode - Base (auto-detects keystore or ACTP_PRIVATE_KEY)
const client = await ACTPClient.create({
mode: 'mainnet',
requesterAddress: '0x1111111111111111111111111111111111111111',
});The SDK accepts amounts in multiple formats:
// All equivalent to $100.50 USDC
await client.basic.pay({ to: '0x...', amount: 100.50 });
await client.basic.pay({ to: '0x...', amount: '100.50' });
await client.basic.pay({ to: '0x...', amount: '$100.50' });
await client.basic.pay({ to: '0x...', amount: '100500000' }); // Wei// Relative formats
deadline: '1h' // 1 hour from now
deadline: '24h' // 24 hours from now
deadline: '7d' // 7 days from now
// Absolute timestamp
deadline: 1735689600 // Unix timestamp
// ISO date string
deadline: '2027-01-01T00:00:00Z'The SDK provides structured exceptions with error codes:
import {
ACTPError,
TransactionNotFoundError,
InvalidStateTransitionError,
InsufficientBalanceError,
ValidationError
} from '@agirails/sdk';
try {
await client.basic.pay({ to: 'invalid', amount: 100 });
} catch (error) {
if (error instanceof ValidationError) {
console.log('Validation failed:', error.message);
console.log('Error code:', error.code);
console.log('Details:', error.details);
} else if (error instanceof InsufficientBalanceError) {
console.log(`Need ${error.required}, have ${error.available}`);
} else if (error instanceof ACTPError) {
console.log(`ACTP error [${error.code}]: ${error.message}`);
}
}ACTPError (base)
├── TransactionNotFoundError
├── InvalidStateTransitionError
├── EscrowNotFoundError
├── InsufficientBalanceError
├── DeadlinePassedError
├── DisputeWindowActiveError
├── ContractPausedError
├── ValidationError
│ ├── InvalidAddressError
│ └── InvalidAmountError
├── NetworkError
│ ├── TransactionRevertedError
│ └── SignatureVerificationError
├── StorageError
│ ├── InvalidCIDError
│ ├── UploadTimeoutError
│ └── ContentNotFoundError
└── AgentLifecycleError
The SDK includes a full-featured CLI for interacting with ACTP:
# Payment operations
actp pay <provider> <amount> [--deadline TIME] [--service TEXT]
actp balance [ADDRESS]
actp mint --amount AMOUNT # Mock mode only
# Transaction management (subcommands of `actp tx`)
actp tx list [--state STATE] [--limit N]
actp tx status <tx_id>
actp tx cancel <tx_id>
# Time manipulation (mock mode only)
actp time advance <seconds>
actp time set <timestamp>
actp time now# Watch transaction state changes (streams updates)
actp watch <tx_id> [--interval SECONDS] [--format json|text]
# Batch operations from file
actp batch <command_file> [--parallel N] [--continue-on-error]
# Dry-run simulation
actp simulate pay <provider> <amount>
actp simulate fee <amount># Publish agent config to on-chain registry
actp publish [--dry-run]
# Pull config from on-chain registry
actp pull [--network base-sepolia]
# Compare local config vs on-chain
actp diff [--network base-sepolia]# Generate ACTP_KEYSTORE_BASE64 for CI/CD
actp deploy:env
# Scan repo for exposed secrets
actp deploy:check [--fix] [--quiet]# Initialize keystore (one-time setup)
ACTP_KEY_PASSWORD=your-password actp init -m testnet # or mainnet
ACTP_KEY_PASSWORD=your-password actp init -m mainnet
# General config
actp config set <key> <value>
actp config get <key>
actp config list
actp config reset
# Available config keys:
# network: base-sepolia | base-mainnet | mock
# rpc-url: RPC endpoint URL
# state-directory: Directory for mock state persistence# Human-readable (default)
actp tx list
# JSON output for scripting
actp tx list --format json
# NDJSON streaming for watch
actp watch TX_ID --format ndjsonRun the test suite:
# Run all tests
npm test
# Run with verbose output
npm test -- --verbose
# Run specific test file
npm test -- src/__tests__/client.test.ts
# Run tests matching pattern
npm test -- --testNamePattern="pay"
# Run with coverage
npm run test:coverage| Method | Description |
|---|---|
ACTPClient.create(config) |
Factory method to create client |
client.basic |
Access basic adapter |
client.standard |
Access standard adapter |
client.advanced |
Access runtime directly |
client.getBalance() |
Get USDC balance |
client.reset() |
Reset mock state |
| Method | Description |
|---|---|
pay(params) |
Create and fund transaction |
checkStatus(txId) |
Get transaction status |
getBalance() |
Get formatted balance |
| Method | Description |
|---|---|
createTransaction(params) |
Create transaction |
linkEscrow(txId) |
Link escrow and lock funds |
transitionState(txId, state, proof?) |
Transition to new state |
releaseEscrow(txId) |
Release funds |
getTransaction(txId) |
Get transaction details |
getAllTransactions() |
List all transactions |
import { serviceDirectory, request, provide } from '@agirails/sdk';
// serviceDirectory is an in-memory, per-process singleton.
// Providers register automatically when calling provide().
// Find providers for a service
const providers = serviceDirectory.findProviders('text-gen');
// Returns string[] of provider addressesimport { Agent, AgentConfig } from '@agirails/sdk';
// Create an agent (auto-detects keystore, no wallet param needed)
const agent = new Agent({
name: 'my-agent',
network: 'testnet',
});
// Register services via agent.provide()
agent.provide('text-generation', async (job) => {
return { result: `Processed: ${job.input}` };
});
await agent.start();This TypeScript SDK maintains full parity with the Python SDK:
| Feature | TypeScript SDK | Python SDK |
|---|---|---|
| DeliveryProof Schema | AIP-4 v1.1 (12 fields) | AIP-4 v1.1 (12 fields) |
| Result Hashing | keccak256 | keccak256 |
| JSON Canonicalization | Insertion order | Insertion order |
| EIP-712 Signing | Full support | Full support |
| Level0 API | Full ACTP flow | Full ACTP flow |
| Level1 Agent API | Complete | Complete |
| CLI Commands | watch, batch, simulate, publish, pull, diff, deploy:* | watch, batch, simulate, publish, pull, diff, deploy:* |
| Adapter Routing | ACTP + x402 + ERC-8004 | ACTP + x402 + ERC-8004 |
| ERC-8004 Bridge | Identity + Reputation | Identity + Reputation |
| AGIRAILS.md Config | publish/pull/diff/drift detection | publish/pull/diff |
| Keystore AIP-13 | Full (30-min TTL cache) | Full (30-min TTL cache) |
| Lazy Publish | pending-publish lifecycle | pending-publish lifecycle |
| Nonce Tracking | SecureNonce, ReceivedNonceTracker | SecureNonce, ReceivedNonceTracker |
| Attestation Tracking | UsedAttestationTracker | UsedAttestationTracker |
Shared Test Vectors: Both SDKs use the same JSON test fixtures to ensure identical behavior.
| Network | Chain ID | Status |
|---|---|---|
| Base Sepolia | 84532 | ✅ Active (Testnet) |
| Base Mainnet | 8453 | ✅ Active |
- Platform Fee: 1% of transaction amount
- Minimum Fee: $0.05 USDC
- EIP-712 Signing: Typed structured data for secure message signing
- Replay Protection: Nonce management prevents transaction replay
- Non-custodial Escrow: 2-of-2 release pattern
- EAS Integration: Ethereum Attestation Service for delivery proofs
- ERC-8004 Reputation: On-chain settlement/dispute feedback after ACTP transactions
- Input Validation: All user inputs validated before processing
- SSRF Guard on Negotiation Channels: Both
QuoteChannelandRelayChannelroute consumer-supplied base URLs throughassertSafePeerUrl, rejecting loopback, RFC1918, link-local (incl. cloud metadata169.254.169.254), and IPv4-mapped IPv6 bypass shapes by default. Opt-in dev escape:allowInsecureTargets: true.
How the SDK treats wallet keys and other sensitive material:
What the SDK reads:
ACTP_KEYSTORE_BASE64+ACTP_KEY_PASSWORD— encrypted keystore (preferred for CI / deploy targets). The base64 blob and the password should live in separate secret scopes (different vaults, env groups, or teams) so neither alone is sufficient.ACTP_PRIVATE_KEY— raw hex private key. Testnet only; the SDK refuses this path onmainnetmode and routes you to the keystore pattern instead..actp/keystore.json+ACTP_KEY_PASSWORD— the on-disk file the keystore env vars are derived from.AGIRAILS_PUBLISH_KEY— public client identifier for the publish proxy (same threat model as a Firebase / Stripe publishable key; safe to embed, no privileged scope).
What the SDK never reads:
- CLI inline flags for keys, mnemonics, signed payloads, or tokens. No
--key,--mnemonic,--secret, or--tokenflag exists on anyactpsubcommand. This avoids theps/ shell history / CI-log leakage class (CWE-532, CWE-312).
What the SDK logs:
- The cached address derived from the resolved key (for diagnostic confirmation). Never the key, mnemonic, or password.
- Bundler / paymaster RPC errors verbatim, which can include the smart-wallet address but not the signer key.
What actp init does for downstream consumers:
- Adds
.actp/,.env, and.env.*to.gitignoreso a forgetful operator can't accidentally commit a populated.env. - Writes a starter
.env.exampledocumenting the keystore + RPC schema with placeholder values only.
If a CI / deployment context needs sensitive material, prefer file-based delivery (mounted secrets, encrypted-at-rest stores) over env vars where the platform supports it, and never echo command lines through set -x while ACTP env vars are populated.
All state-changing operations in ACTPKernel wait for block confirmations before events are emitted. The default is 2 confirmations (~4-6s on Base L2's ~2s blocks), configurable via BlockchainRuntimeConfig.confirmations. The SDK's EventMonitor receives already-confirmed events; no additional confirmation handling is needed at the application layer.
AGIRAILS uses did:ethr DIDs based on the ERC-1056 standard for identity management.
Every Ethereum address automatically IS a DID - no registration required:
did:ethr:84532:0x742d35cc6634c0532925a3b844bc9e7595f0beb
↑ ↑
chainId address
import { DIDResolver } from '@agirails/sdk';
// Build DID from address (no registration needed!)
const did = DIDResolver.buildDID('0x742d35cc6634c0532925a3b844bc9e7595f0beb', 84532);
// → 'did:ethr:84532:0x742d35cc6634c0532925a3b844bc9e7595f0beb'
// Parse DID components
const parsed = DIDResolver.parseDID(did);
console.log(parsed.method); // 'ethr'
console.log(parsed.chainId); // 84532
console.log(parsed.address); // '0x742d35cc6634c0532925a3b844bc9e7595f0beb'
// Validate DID format
const isValid = DIDResolver.isValidDID(did); // trueimport { DIDResolver } from '@agirails/sdk';
// Create resolver for Base Sepolia
const resolver = await DIDResolver.create({ network: 'base-sepolia' });
// Resolve DID to W3C DID Document
const result = await resolver.resolve('did:ethr:84532:0x742d35cc...');
if (result.didDocument) {
console.log('Controller:', result.didDocument.controller);
console.log('Verification Methods:', result.didDocument.verificationMethod);
console.log('Service Endpoints:', result.didDocument.service);
}import { DIDResolver } from '@agirails/sdk';
const resolver = await DIDResolver.create({ network: 'base-sepolia' });
// Verify a signature was made by a DID's controller (or authorized delegate)
const result = await resolver.verifySignature(
'did:ethr:84532:0x742d35cc...', // DID
'Hello AGIRAILS', // Original message
'0x1234...', // Signature
{ chainId: 84532 } // Verification options
);
if (result.valid) {
console.log('Signature valid!');
console.log('Signer:', result.signer);
console.log('Is delegate:', result.isDelegate);
}For advanced use cases, use DIDManager to manage delegates and attributes:
import { DIDManager } from '@agirails/sdk';
// Create manager with signer
const manager = new DIDManager(signer, { network: 'base-sepolia' });
// Add a signing delegate (valid for 24 hours)
await manager.addDelegate(
'did:ethr:84532:0x742d35cc...', // Your DID
'0xDelegateAddress...', // Delegate address
'sigAuth', // Delegate type
86400 // Validity in seconds
);
// Rotate key ownership
await manager.changeOwner(
'did:ethr:84532:0x742d35cc...',
'0xNewOwnerAddress...'
);
// Add service endpoint attribute
await manager.setAttribute(
'did:ethr:84532:0x742d35cc...',
'did/svc/AgentService',
'https://my-agent.example.com/api',
86400
);DIDs are used internally for:
- Provider/Consumer Identity: Transaction parties identified by DIDs
- Message Signing: EIP-712 messages reference DIDs
- Delivery Proofs: Attestations link to provider DIDs
- Reputation: Future reputation system will be DID-based
# Wallet resolution (checked in order):
ACTP_PRIVATE_KEY=0x... # Raw private key (fail-closed on mainnet — see AIP-13)
ACTP_KEYSTORE_BASE64=... # Base64 keystore for CI/CD (use actp deploy:env to generate)
ACTP_KEY_PASSWORD=... # Decrypts keystore (ACTP_KEYSTORE_BASE64 or .actp/keystore.json)
# Optional
BASE_SEPOLIA_RPC=... # Custom RPC for Base Sepolia
BASE_MAINNET_RPC=... # Custom RPC for Base Mainnet
CDP_API_KEY=... # Coinbase Developer Platform API key
PIMLICO_API_KEY=... # Pimlico bundler/paymaster API key
IPFS_GATEWAY=... # Custom IPFS gateway URL- Node.js 18+ (required for global
fetchandAbortController) - TypeScript 5.0+ (for development)
- Dependencies: ethers
The SDK ships as CommonJS only. It works with require() and with bundlers (webpack, esbuild, Rollup) that support CJS. If you are using ESM (import syntax), Node.js will auto-interop with CJS modules — no additional configuration is needed.
Apache 2.0 License - see LICENSE for details.