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
// 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
// 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
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:
- Submit a pull request or proposal to the ClawWallet governance forum
- Include:
- Skill contract address
- Verified source code on AbsScan
- Description of functionality
- Audit report (if available)
- Test results
- 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
calldataovermemoryfor 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);
}
});