A Mimic function is an AssemblyScript module compiled to WebAssembly. It runs inside a sandboxed environment on every trigger execution, receives typed inputs from the manifest, queries on-chain and off-chain data, and emits intents — declarations of what you want to happen on-chain.
Project anatomy
my-function/
├── manifest.yaml # Inputs, ABIs, and metadata
├── src/
│ ├── function.ts # Your function logic (entry point)
│ └── types/ # Auto-generated by `mimic codegen`
│ ├── index.ts # Typed inputs
│ └── ERC20.ts # Generated ABI wrapper (one per ABI)
└── build/
├── function.wasm # Compiled output
└── manifest.json # Validated manifest
Manifest
The manifest describes your function's metadata, inputs, and contract ABIs. The CLI uses it to validate, generate types, compile, and deploy.
version:1.0.0name:My Automation Functiondescription:Swaps USDC to ETH when balance exceeds a threshold.inputs:-chainId:int32-smartAccount:address-tokenIn:address-tokenOut:address-threshold:string-feeAmount:stringabis:-ERC20:"./abis/ERC20.json"
Inputs can optionally include a description:
All token amounts should be declared as string and converted in code with fromStringDecimal. This keeps the UI human-readable — a user types 1 for 1 USDC, not 1000000.
Input type mapping — manifest types and the AssemblyScript types they produce:
Manifest type
AssemblyScript type
int32
i32
int64
i64
uint8
u8
uint16
u16
uint32
u32
uint64
u64
uint256 / int256
BigInt
address
Address
bytes / bytesN
Bytes
string
string
inputs and abis are merged into maps during validation — duplicate keys are rejected.
ABI paths are resolved relative to the directory of your manifest.yaml.
Function structure
Every function must export a main function. The inputs type is auto-generated from your manifest.
Library reference (@mimicprotocol/lib-ts)
Primitives
Tokens and amounts
Pre-defined tokens
There are two ways to reference a known token depending on whether the chain is fixed or dynamic.
Chain namespaces — use when the chain is known at compile time:
Tokens class — use when the chain comes from inputs.chainId at runtime:
Available tokens in Tokens: USDC, USDT, DAI, WBTC, WETH, ETH, AVAX, WAVAX, POL, WPOL, BNB, WBNB, XDAI, WXDAI, SONIC, WSONIC.
ERC20Token.fromString — last resort for tokens not in the registry:
DenominationToken
DenominationToken.USD() is the standard way to express max fees. It is not a real on-chain token — it deducts from the user's Mimic credits. It can only be used with .addMaxFee(). You cannot transfer or swap it.
TokenAmount
Result type
All queries (except getContext) return Result<V, string>. Always handle errors.
Environment queries
Token price
Relevant token balances
Returns all token balances for an address, with optional chain, allow/deny list, and USD minimum filters.
EVM contract read (raw)
Use generated ABI wrappers (see below) instead of raw calls where possible.
Native token balance
Account code
Subgraph query
Execution context
getContext() does not return a Result — it cannot fail.
Generated ABI wrappers
After running mimic codegen, each ABI declared in the manifest produces a file in src/types/<ContractName>.ts containing four things:
The contract class — instantiate with (address, chainId, timestamp?) to call methods
A <ContractName>Utils static class — raw encode/decode helpers (rarely needed directly)
Struct/tuple classes — one per Solidity struct or multi-output tuple
Event classes — one per event, with a static decode(topics, data) method
Contract class — read and write methods
Struct classes
Solidity structs and multi-output tuples become generated classes with typed fields. You typically receive them as unwrapped return values from read methods — you don't construct them manually.
Event classes
Each event gets a class with a static decode(topics, data) method, matching the shape of the TriggerType.EVENT payload:
Utils class
The <ContractName>Utils static class exposes encodeX() and decodeX() methods for raw ABI encoding. Use these only when you need the raw encoded bytes — for example to pass a call directly to addCall() without instantiating the contract class.
Solidity → AssemblyScript type mapping
Solidity
AssemblyScript
address
Address
bool
bool
string
string
bytes / bytesN
Bytes
uint8–uint16, int8–int16
u8/u16/i8/i16
uint32–uint64, int32–int64
u32/u64/i32/i64
uint256 / int256
BigInt
T[]
T[]
tuple / struct
generated class
Intent builders
Intents are declarations of what you want to happen. The protocol finds the best way to fulfill them.
Transfer — move tokens between addresses
Swap — exchange tokens
The tokenOut amount is a minimum — relayers may deliver more.
EVM Call — execute contract functions
Common builder options
All intent builders support these optional methods before .build():
Method
Purpose
.addUser(address)
Override the user (defaults to ctx.user)
.addDeadline(timestamp)
Override the deadline (defaults to 5 minutes from now)
.addNonce(string)
Override the nonce (defaults to auto-generated)
.addEvent(topic, data)
Attach an on-chain event to the intent
.addEvents(events[])
Attach multiple events
When to use .addUser():
Transfer / Swap intents — ctx.user is the default and is correct for most cases. ctx.user is the EOA that signed and triggered the function, so you normally do not need to call .addUser().
EVM Call intents — you must call .addUser(inputs.smartAccount). EVM Call intents execute generic contract calls, which require a smart account. ctx.user is an EOA and cannot fulfill this role.
Guarding against empty intents:
Never send an intent with no transfers, calls, or swaps. If you are conditionally adding items (e.g. iterating over a list), check the builder before sending. If you are unconditionally adding at least one item, no guard is needed.
Batching large numbers of items:
A single intent may not fit in one transaction if it contains too many calls, transfers, or swaps. If you are building a variable-length list, split it into batches of at most 20 items and send one intent per batch.
Persistent storage
Use the storage namespace to read and write arbitrary bytes to the user's on-chain storage via the Mimic Helper contract. Useful for maintaining state between executions.
Logging
Both styles work. Template literals are preferred for multi-variable messages; format strings are preferred when you need to call .toString() on custom types. Levels: DEBUG < INFO < WARNING < ERROR < CRITICAL.
inputs:
- threshold:
type: string
description: Minimum balance in human-readable units, e.g. "100.5"
- feeAmount:
type: string
description: Max fee in USD, e.g. "1.5"
// In the manifest: threshold: string
// In the function:
const threshold = TokenAmount.fromStringDecimal(Tokens.USDC.on(inputs.chainId), inputs.threshold)
const fee = TokenAmount.fromStringDecimal(DenominationToken.USD(), inputs.feeAmount)
import { environment, log, TokenAmount, TransferBuilder } from '@mimicprotocol/lib-ts'
import { inputs } from './types'
export default function main(): void {
// 1. Read inputs
const account = inputs.account
const threshold = inputs.threshold
// 2. Query on-chain or off-chain data
const balanceResult = environment.relevantTokensQuery(account, [inputs.chainId])
if (balanceResult.isError) {
log.error('Failed to query balances: {}', [balanceResult.error])
return
}
// 3. Apply logic and emit intents
const balances = balanceResult.unwrap()
// ... build and send an intent
}
import { Address, BigInt, Bytes } from '@mimicprotocol/lib-ts'
// Address
const addr = Address.fromString('0x...')
const zero = Address.zero()
// BigInt — used for all token amounts and large numbers
const amount = BigInt.fromString('1000000000000000000') // 1 ETH in wei
const doubled = amount.times(BigInt.fromI32(2))
const power = BigInt.fromI32(10).pow(18)
// Bytes — arbitrary byte data
const data = Bytes.fromHexString('0x095ea7b3...')
const empty = Bytes.empty()
const utf8 = Bytes.fromUTF8('hello')
import { Ethereum, Arbitrum, Base, Optimism, Gnosis, Polygon, Avalanche, BNB, Sonic } from '@mimicprotocol/lib-ts'
const usdc = Ethereum.USDC // ERC20Token with address, chainId, decimals and symbol pre-filled
const eth = Ethereum.ETH
const wbtc = Arbitrum.WBTC
import { Tokens } from '@mimicprotocol/lib-ts'
// Resolves the correct USDC address for whatever chain the user configured
const usdc = Tokens.USDC.on(inputs.chainId)
// Check before resolving if the token might not be supported on all chains
if (Tokens.USDC.isSupported(inputs.chainId)) {
const usdc = Tokens.USDC.on(inputs.chainId)
}
import { ERC20Token } from '@mimicprotocol/lib-ts'
// Decimals and symbol are optional — if omitted they are fetched on-chain lazily when first accessed.
// Provide them when known to avoid the extra RPC calls.
const myToken = ERC20Token.fromString('0x...', inputs.chainId) // lazy on-chain fetch
const myTokenOpt = ERC20Token.fromString('0x...', inputs.chainId, 18, 'MYT') // no extra calls
import { DenominationToken, TokenAmount } from '@mimicprotocol/lib-ts'
const fee = TokenAmount.fromStringDecimal(DenominationToken.USD(), inputs.feeAmount)
// Only valid use — passing to .addMaxFee()
builder.addMaxFee(fee)
import { Address, ChainId, Ethereum, EvmCallBuilder, TokenAmount } from '@mimicprotocol/lib-ts'
import { MyContract } from './types/MyContract'
const contract = new MyContract(contractAddress, ChainId.ETHEREUM)
const fee = TokenAmount.fromStringDecimal(Ethereum.USDC, '1')
// Single call from a generated wrapper
contract.myFunction(arg1, arg2) // returns EvmCallBuilder
.addMaxFee(fee)
.build()
.send()
// Multiple calls in one intent — use addCallsFromBuilder to merge generated wrappers.
// This is preferred over extracting and re-encoding data manually with addCall.
EvmCallBuilder.forChain(ChainId.ETHEREUM)
.addCallsFromBuilder(contract.functionA(arg1))
.addCallsFromBuilder(contract.functionB(arg2))
.addMaxFee(fee)
.addUser(inputs.smartAccount)
.build()
.send()
// Or pass all builders at once
EvmCallBuilder.forChain(ChainId.ETHEREUM)
.addCallsFromBuilders([contract.functionA(arg1), contract.functionB(arg2)])
.addMaxFee(fee)
.addUser(inputs.smartAccount)
.build()
.send()
// Use addCall directly only when you have raw encoded data (e.g. from a multicall response)
EvmCallBuilder.forChain(ChainId.ETHEREUM)
.addCall(contractAddress, encodedData) // value defaults to 0
.addCall(contractAddress, encodedData2, BigInt.fromI32(1000)) // optional ETH value for payable calls
.addMaxFee(fee)
.build()
.send()
// Transfer — ctx.user is the default, no .addUser() needed
TransferBuilder.forChain(inputs.chainId)
.addTransferFromTokenAmount(tokenAmount, recipientAddress)
.addMaxFee(fee)
.build()
.send()
// EVM Call — smart account required
EvmCallBuilder.forChain(inputs.chainId)
.addCall(contractAddress, callData)
.addMaxFee(fee)
.addUser(inputs.smartAccount) // required for generic calls
.build()
.send()
// Guard needed — items are added conditionally inside a loop
const builder = TransferBuilder.forChain(inputs.chainId).addMaxFee(fee)
for (let i = 0; i < tokenBalances.length; i++) {
if (tokenBalances[i].amount.gt(BigInt.zero())) {
builder.addTransferFromTokenAmount(tokenBalances[i], inputs.recipient)
}
}
if (builder.transfers.length > 0) { // only send if something was added
builder.build().send()
}
// No guard needed — the call is always added
EvmCallBuilder.forChain(inputs.chainId)
.addCall(contractAddress, callData)
.addMaxFee(fee)
.addUser(inputs.smartAccount)
.build()
.send()
const BATCH_SIZE = 20
for (let i = 0; i < calls.length; i += BATCH_SIZE) {
const builder = EvmCallBuilder.forChain(inputs.chainId).addMaxFee(fee)
const end = i + BATCH_SIZE < calls.length ? i + BATCH_SIZE : calls.length
for (let j = i; j < end; j++) {
builder.addCall(calls[j].address, calls[j].data)
}
builder.addUser(inputs.smartAccount).build().send()
}
import { environment, storage, Bytes, ChainId, TokenAmount, DenominationToken } from '@mimicprotocol/lib-ts'
const userAddress = environment.getContext().user
const fee = TokenAmount.fromStringDecimal(DenominationToken.USD(), '0.5')
// Write — emits an EvmCall intent that stores the data on-chain
storage.createSetDataCall(
userAddress,
fee,
'last-execution', // storage key
Bytes.fromUTF8('2024-01-01'), // value (arbitrary bytes)
ChainId.OPTIMISM // chain (defaults to Optimism)
).send()
// Read — synchronous on-chain call, returns Result<Bytes, string>
const dataResult = storage.getData(userAddress, 'last-execution', ChainId.OPTIMISM)
if (dataResult.isOk) {
const value = dataResult.unwrap().toString()
}
import { log } from '@mimicprotocol/lib-ts'
// Format string style — {} placeholders replaced in order
log.debug('Processing account {}', [account.toHexString()])
log.info('Balance: {} USDC', [balance.toString()])
log.warning('Slippage is high: {}%', [slippage.toString()])
log.error('Token price query failed: {}', [result.error])
// Template literal style — equivalent and more readable for inline expressions
log.info(`Balance: ${balance.toString()} USDC`)
log.info(`Processing pool ${pool}, token ${token}, fees ${fees.toString()}`)
// CRITICAL terminates execution immediately
log.critical('Unexpected state, aborting')