Skip to main content

Building a Custom Skill

Skills are smart contracts that extend agent wallet capabilities. This guide walks through creating a skill from scratch, registering it in the ClawSkillRegistry, and getting it approved.

What Makes a Good Skill?

A skill is any smart contract that provides useful functionality for agent wallets. There's no required interface — skills are called via the wallet's generic execute() function with encoded calldata.

Good skills:

  • Perform a specific, well-defined task (e.g., DEX swap, bridge, vote)
  • Are composable — can be combined with other skills
  • Handle errors gracefully — revert with clear messages
  • Are gas-efficient — agents often run many operations

Step 1: Write the Skill Contract

Example: Price Oracle Skill

PriceOracleSkill.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "@openzeppelin/contracts/access/Ownable.sol";

/**
* @title PriceOracleSkill
* @notice Provides price feed data for agent wallets
* @dev A simple on-chain price oracle that can be updated by the owner.
* Agent wallets install this skill to query token prices.
*/
contract PriceOracleSkill is Ownable {
struct PriceData {
uint256 price; // Price in USD (18 decimals)
uint256 updatedAt; // Timestamp of last update
uint8 decimals; // Price decimals
}

mapping(address => PriceData) public prices;

event PriceUpdated(address indexed token, uint256 price, uint256 timestamp);

constructor() Ownable(msg.sender) {}

/// @notice Get the price of a token
/// @param token The token address
/// @return price The price in USD (18 decimals)
/// @return updatedAt When the price was last updated
function getPrice(address token) external view returns (uint256 price, uint256 updatedAt) {
PriceData memory data = prices[token];
require(data.updatedAt > 0, "Price not available");
require(block.timestamp - data.updatedAt < 1 hours, "Price stale");
return (data.price, data.updatedAt);
}

/// @notice Update a token price (oracle owner only)
function updatePrice(address token, uint256 price) external onlyOwner {
prices[token] = PriceData({
price: price,
updatedAt: block.timestamp,
decimals: 18
});
emit PriceUpdated(token, price, block.timestamp);
}

/// @notice Batch update prices
function batchUpdatePrices(
address[] calldata tokens,
uint256[] calldata newPrices
) external onlyOwner {
require(tokens.length == newPrices.length, "Length mismatch");
for (uint256 i = 0; i < tokens.length; i++) {
prices[tokens[i]] = PriceData({
price: newPrices[i],
updatedAt: block.timestamp,
decimals: 18
});
emit PriceUpdated(tokens[i], newPrices[i], block.timestamp);
}
}
}

Example: DEX Aggregator Skill

DexAggregatorSkill.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

interface IDex {
function getAmountOut(address tokenIn, address tokenOut, uint256 amountIn)
external view returns (uint256);
function swap(address tokenIn, address tokenOut, uint256 amountIn, uint256 minOut)
external returns (uint256);
}

/**
* @title DexAggregatorSkill
* @notice Finds the best price across multiple DEXes for agent wallets
*/
contract DexAggregatorSkill {
using SafeERC20 for IERC20;

address[] public dexes;

constructor(address[] memory _dexes) {
dexes = _dexes;
}

/// @notice Get the best quote across all DEXes
function getBestQuote(
address tokenIn,
address tokenOut,
uint256 amountIn
) external view returns (address bestDex, uint256 bestAmountOut) {
for (uint256 i = 0; i < dexes.length; i++) {
try IDex(dexes[i]).getAmountOut(tokenIn, tokenOut, amountIn) returns (uint256 amountOut) {
if (amountOut > bestAmountOut) {
bestAmountOut = amountOut;
bestDex = dexes[i];
}
} catch {}
}
require(bestDex != address(0), "No valid quotes");
}

/// @notice Execute a swap on the best DEX
function swapBest(
address tokenIn,
address tokenOut,
uint256 amountIn,
uint256 minAmountOut
) external returns (uint256 amountOut) {
// Find best DEX
address bestDex;
uint256 bestQuote;
for (uint256 i = 0; i < dexes.length; i++) {
try IDex(dexes[i]).getAmountOut(tokenIn, tokenOut, amountIn) returns (uint256 quote) {
if (quote > bestQuote) {
bestQuote = quote;
bestDex = dexes[i];
}
} catch {}
}
require(bestQuote >= minAmountOut, "Insufficient output");

// Transfer tokens from caller (the wallet) to this contract
IERC20(tokenIn).safeTransferFrom(msg.sender, address(this), amountIn);
IERC20(tokenIn).approve(bestDex, amountIn);

// Execute swap
amountOut = IDex(bestDex).swap(tokenIn, tokenOut, amountIn, minAmountOut);

// Send output back to caller
IERC20(tokenOut).safeTransfer(msg.sender, amountOut);
}
}

Step 2: Deploy on Abstract Chain

Compile with zksolc (required for Abstract/zkSync ERA):

# hardhat.config.ts
import "@matterlabs/hardhat-zksync-solc";

const config = {
zksolc: {
version: "1.5.4",
settings: {},
},
networks: {
abstract: {
url: "https://api.mainnet.abs.xyz",
zksync: true,
},
},
solidity: "0.8.24",
};
npx hardhat deploy-zksync --network abstract

Step 3: Register in ClawSkillRegistry

register-skill.ts
import { ethers } from 'ethers';

const SKILL_REGISTRY = '0xb9913F4fceA83fF3F9c7D56339Abc196408Cf21b';
const REGISTRATION_FEE = ethers.parseEther('0.0005');

const provider = new ethers.JsonRpcProvider('https://api.mainnet.abs.xyz');
const wallet = new ethers.Wallet('YOUR_PRIVATE_KEY', provider);

const registry = new ethers.Contract(SKILL_REGISTRY, [
'function registerSkill(address skill, string name, string version, string description) external payable',
'function isRegistered(address skill) external view returns (bool)',
], wallet);

const skillAddress = '0xYOUR_DEPLOYED_SKILL_ADDRESS';

// Check if already registered
const isRegistered = await registry.isRegistered(skillAddress);
if (isRegistered) {
console.log('Skill already registered');
process.exit(0);
}

// Register
const tx = await registry.registerSkill(
skillAddress,
'Price Oracle', // name
'1.0.0', // version
'On-chain price feeds for agent wallets. Supports batch queries and staleness checks.',
{ value: REGISTRATION_FEE },
);
await tx.wait();

console.log('✅ Skill registered! Awaiting governance approval.');

Step 4: Request Governance Approval

After registration, your skill needs to be approved by governance (contract owner or governors) before wallets will trust it:

  1. Submit a pull request or proposal to the ClawWallet governance forum
  2. Include:
    • Skill contract address
    • Verified source code on AbsScan
    • Description of functionality
    • Audit report (if available)
    • Test results
  3. A governor will review and call approveSkill(skillAddress) if approved

Step 5: Users Install Your Skill

Once approved, any wallet owner/agent can install it:

// From the agent wallet
await agentWallet.installSkill(skillAddress);

Calling Skills from Wallets

Skills are called through the wallet's execute() function with encoded calldata:

const skill = new ethers.Interface([
'function getPrice(address token) external view returns (uint256 price, uint256 updatedAt)',
'function swapBest(address tokenIn, address tokenOut, uint256 amountIn, uint256 minAmountOut) external returns (uint256)',
]);

// Read-only call (no execute needed, call directly)
const priceSkill = new ethers.Contract(skillAddress, skill, provider);
const [price, updatedAt] = await priceSkill.getPrice(tokenAddress);

// State-changing call (via wallet.execute)
const calldata = skill.encodeFunctionData('swapBest', [
tokenInAddress,
tokenOutAddress,
ethers.parseEther('100'),
ethers.parseEther('95'),
]);

await agentWallet.execute(skillAddress, 0, calldata);

Best Practices

Security

  • Validate all inputs — check for zero addresses, zero amounts
  • Use SafeERC20 — prevent silent transfer failures
  • Minimize external calls — reduce attack surface
  • Add reentrancy guards if handling ETH or callbacks

Gas Efficiency

  • Use calldata over memory for function parameters
  • Pack structs — order members by size
  • Avoid storage reads in loops — cache in local variables
  • Use events for off-chain data — cheaper than storage

Composability

  • Return meaningful values — let wallets chain operations
  • Accept generic inputs — don't hardcode addresses
  • Emit events — enable off-chain indexing and monitoring

Documentation

  • NatSpec comments on all public functions
  • Version your skill — use semantic versioning
  • Include usage examples in your README

Monitoring Usage

After your skill is live, track its usage:

const registry = new ethers.Contract(SKILL_REGISTRY, [
'function getSkillMetadata(address skill) external view returns (tuple(string name, string version, string description, address author, bool approved, uint256 registeredAt, uint256 totalCalls))',
], provider);

const metadata = await registry.getSkillMetadata(skillAddress);
console.log('Total calls:', metadata.totalCalls.toString());
console.log('Approved:', metadata.approved);

Listen for usage events:

const registry = new ethers.Contract(SKILL_REGISTRY, [
'event SkillCalled(address indexed skill, address indexed caller)',
], provider);

registry.on('SkillCalled', (skill, caller) => {
if (skill === skillAddress) {
console.log('Skill called by wallet:', caller);
}
});