Examples
Counter — the bundled minimal example
// services/Counter/Counter.sol
pragma solidity 0.8.30;
contract Counter {
uint256 public count;
address public immutable deployer;
event Bumped(uint256 newCount, address indexed by);
constructor() {
deployer = msg.sender;
}
function bump() external {
count += 1;
emit Bumped(count, msg.sender);
}
function reset() external {
require(msg.sender == deployer, "only deployer");
count = 0;
}
}
No imports, no manifest needed. After npm run wizard you'll find a
working page at /services/Counter with:
- Read:
count,deployer - Write:
bump(no args, single click),reset(no args, single click — admin-gated by the contract itself) - Events: live
Bumpedtail
Service that extends EvvmService — full dual-signature flow
This is the canonical shape. The constructor takes both Core and
Staking (EvvmService requires both), and the function carries the
canonical EVVM plumbing for action + payment.
// services/Tipjar/Tipjar.sol
// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;
import "@scaffold-evvm/testnet-contracts/library/EvvmService.sol";
contract Tipjar is EvvmService {
error Unauthorized();
address public immutable owner;
mapping(address => uint256) public received;
event Tipped(address indexed from, address indexed to, uint256 amount);
constructor(address _core, address _staking, address _owner)
EvvmService(_core, _staking)
{
owner = _owner;
}
function tip(
// action params
address user,
address to,
uint256 amount,
// canonical action plumbing
address senderExecutor, // usually address(this)
address originExecutor, // address(0) = any fisher may execute
uint256 nonce,
bool isAsyncExec,
bytes calldata signature,
// canonical payment plumbing (independent of action plumbing)
uint256 priorityFeePay,
uint256 noncePay,
bool isAsyncExecPay,
bytes calldata signaturePay
) external {
// (1) Verify the user signed the action + consume nonce atomically.
// The action hash holds ONLY the operation name + business args.
// The unified envelope (executor pair, nonce, isAsyncExec, evvmId)
// is added by Core.validateAndConsumeNonce — not by this hash.
core.validateAndConsumeNonce(
user,
senderExecutor,
keccak256(abi.encode("tip", to, amount)),
originExecutor,
nonce,
isAsyncExec,
signature
);
// (2) Pull the user's payment via Core.pay — separate signature, separate nonce.
requestPay(
user,
getPrincipalTokenAddress(),
amount,
priorityFeePay,
originExecutor,
noncePay,
isAsyncExecPay,
signaturePay
);
// (3) Reward the fisher (only meaningful if this contract is a registered staker).
if (core.isAddressStaker(address(this))) {
makeCaPay(msg.sender, getPrincipalTokenAddress(), priorityFeePay);
}
// (4) Domain logic + events.
received[to] += amount;
emit Tipped(user, to, amount);
}
}
Add a manifest.json to wire the auto-UI for the dual-signature flow:
{
"name": "Tipjar",
"description": "Send a gasless tip to any EVVM user, paid in MATE.",
"tags": {
"publicPay": ["tip"],
"admin": []
},
"actions": {
"tip": {
"actionPayload": ["address to", "uint256 amount"]
}
}
}
After deploying, /services/Tipjar shows a tip form with two
business inputs (to, amount) — the auto-UI handles both
signatures and submits them together. The client-side signing uses
the evvm-js signers
and services
modules; if you outgrow the auto-UI and write a hand-rolled page for
Tipjar, you'd call the same SDK directly.
The four invariants
Every EVVM service must honor these four — they are the contract you sign with the protocol:
- Validate before side effects.
core.validateAndConsumeNonce(...)is the first line of the function. Skip it and the same signature can be replayed. - Action hash holds the action, nothing else. Only the function
name + domain arguments go inside
keccak256(abi.encode(...)). Plumbing (executors, nonce, isAsyncExec, evvmId) lives in the unified envelope automatically. - The pay signature is separate.
requestPaytakes its own nonce + signature — a compromised pay nonce doesn't invalidate the action. - Use
originExecutordeliberately. Passaddress(0)to let any fisher execute. Pass a specific address only if you want to lock execution to one EOA.
For the protocol-side rationale behind each invariant — why the action hash is structured this way, how the unified envelope is built, what the fisher economy expects — see How to make an EVVM service.
Patterns to copy
- Read returns lots of data? Group it into a
structand return that — the auto-UI renders structs as a key-value table, which is more legible than many separate fields. - Need an admin role? Put the addresses behind a constructor
argument and
require(msg.sender == admin, ...)inside the function. Then list those functions undertags.adminso the UI groups them. - Want to hide internal
externalhelpers? Put them undertags.hidden. - Need a fancy UX? Outgrow the auto-UI and write a normal Next.js
page using
packages/nextjs/src/components/ui/. The deployment pipeline still records your address, ABI, and the EVVMScan hookup works regardless of which UI you ship.