Wallet Integration Test Agent with WaaP CLI
What are we cooking?
An autonomous Node.js agent that tests your wallet integration on every pull request — by being a real user. The agent has its own WaaP wallet on Base. When a new PR appears, it connects that wallet to your staging environment and runs a five-part test suite: SIWE authentication, testnet transactions, token approvals, contract calls, and balance verification. Results get posted as a PR comment so reviewers see wallet-level pass/fail before merging.
The key insight: most wallet integration bugs only surface when a real wallet interacts with the application. Unit tests mock the provider. This agent does not mock anything — it signs real messages, sends real testnet transactions, and verifies real on-chain state.
The agent also handles the standard CI/CD loop: dispatching GitHub Actions workflows on new PRs, triggering deployments on merge, and posting failure comments when CI runs break.
Key Components
- WaaP CLI — Signs messages and broadcasts on-chain transactions through a 2PC-MPC enclave. No raw private key in your
.env. - GitHub REST API — Polls repositories for PRs and workflow runs, dispatches workflows, and posts test results as comments.
- Vercel Deploy Hooks (or custom webhooks) — Triggers production deployments on merge.
- Base network — The target chain for wallet integration tests (chain ID 8453).
Prerequisites
Before starting, you need:
- A WaaP wallet — created via
waap-cli signup - Testnet ETH on Base — to fund the test transactions (send to the address from
waap-cli whoami) - A GitHub Personal Access Token — with
repoandactionsscopes - One or more GitHub repositories to monitor
- A staging URL — the preview environment the agent tests against
- A test contract and ERC-20 token — deployed on your target chain for the contract interaction and approval tests
Project Setup
mkdir waap-test-agent && cd waap-test-agent
npm init -y
npm install dotenv undici execa
npm install -g @human.tech/waap-cli@latestSet Up a WaaP Wallet for Your Agent
# Create a dedicated agent account
waap-cli signup --email youremail+test-agent@example.com --password '12345678!'
# Or log in to an existing one
waap-cli login --email youremail+test-agent@example.com --password '12345678!'
# Get the wallet address — fund it with testnet ETH
waap-cli whoamiEnvironment Variables
Create a .env file (never commit this to source control):
# Required
GITHUB_TOKEN=ghp_your_personal_access_token
GITHUB_REPOS=your-org/repo-one,your-org/repo-two
STAGING_URL=https://staging.myapp.xyz
TEST_CONTRACT_ADDRESS=0xYourTestContract
TEST_TOKEN_ADDRESS=0xYourTestERC20Token
# Optional — sensible defaults provided
POLL_INTERVAL_MS=30000 # 30 seconds between polling cycles
CI_WORKFLOW_ID=ci.yml # GitHub Actions workflow to dispatch
DEPLOY_WEBHOOK_URL= # Vercel deploy hook or custom endpoint
TEST_SEND_AMOUNT_ETH=0.0001 # ETH to send in the transaction test
TEST_CONTRACT_METHOD=0x # Calldata for the contract interaction test
AGENT_LOG_FILE=./logs/cicd-agent.jsonlThe Test Suite
When the agent detects a new PR, it runs five wallet integration tests in sequence. Each test uses waap-cli to perform a real wallet operation. If any test fails, the agent still runs the remaining tests and reports all results.
Test 1: Wallet Connect (SIWE)
The agent signs a Sign-In with Ethereum message targeting your staging URL. This proves the wallet can authenticate with the application.
async function testWalletConnect(address, prNumber) {
const siweMessage = [
`${STAGING_URL} wants you to sign in with your Ethereum account:`,
address,
'',
'Wallet integration test — PR #' + prNumber,
'',
`URI: ${STAGING_URL}`,
`Version: 1`,
`Chain ID: ${CHAIN_ID}`,
`Nonce: ${Date.now()}`,
`Issued At: ${new Date().toISOString()}`,
].join('\n');
const { stdout } = await execa('waap-cli', [
'sign-message',
'--message', siweMessage,
'--json',
]);
const parsed = parseResult(stdout);
if (!parsed.signature) throw new Error('No signature returned');
return `Signed SIWE message, signature: ${parsed.signature.slice(0, 16)}...`;
}If this test fails, wallet login is broken. Fix it before anything else.
Test 2: Send Transaction
The agent sends a small ETH amount to itself using waap-cli send-tx. This validates the full transaction lifecycle: signing, broadcasting, and receipt confirmation.
async function testSendTransaction(address) {
const { stdout } = await execa('waap-cli', [
'send-tx',
'--chain-id', String(CHAIN_ID),
'--to', address, // send to self — no funds leave the wallet
'--value', TEST_SEND_AMOUNT_ETH,
'--json',
]);
const parsed = parseResult(stdout);
if (!parsed.txHash) throw new Error('No txHash returned');
return `Sent ${TEST_SEND_AMOUNT_ETH} ETH to self, tx: ${parsed.txHash.slice(0, 16)}...`;
}The transaction goes to the agent’s own address, so funds stay in the wallet minus gas. Configure the amount with TEST_SEND_AMOUNT_ETH.
Test 3: Token Approval
The agent calls approve() on the configured ERC-20 token, then reads back the allowance to confirm. This covers the standard DeFi pattern where a user approves a contract to spend tokens on their behalf.
async function testTokenApproval(address) {
// approve(address spender, uint256 amount) — selector 0x095ea7b3
const approveCalldata = '0x095ea7b3'
+ TEST_CONTRACT_ADDRESS.slice(2).padStart(64, '0')
+ 'de0b6b3a7640000'.padStart(64, '0'); // 1e18
const { stdout } = await execa('waap-cli', [
'send-tx',
'--chain-id', String(CHAIN_ID),
'--to', TEST_TOKEN_ADDRESS,
'--data', approveCalldata,
'--json',
]);
const parsed = parseResult(stdout);
if (!parsed.txHash) throw new Error('No txHash returned');
// Verify allowance on-chain
const allowanceCalldata = '0xdd62ed3e'
+ address.slice(2).padStart(64, '0')
+ TEST_CONTRACT_ADDRESS.slice(2).padStart(64, '0');
const { stdout: callStdout } = await execa('waap-cli', [
'send-tx',
'--chain-id', String(CHAIN_ID),
'--to', TEST_TOKEN_ADDRESS,
'--data', allowanceCalldata,
'--call', // read-only, no transaction
'--json',
]);
return `Approval tx: ${parsed.txHash.slice(0, 16)}..., allowance confirmed`;
}Test 4: Contract Interaction
The agent sends a transaction to TEST_CONTRACT_ADDRESS with the calldata in TEST_CONTRACT_METHOD. Use this to test any contract method your application depends on — a swap, a mint, a stake, or a custom function.
async function testContractInteraction() {
const { stdout } = await execa('waap-cli', [
'send-tx',
'--chain-id', String(CHAIN_ID),
'--to', TEST_CONTRACT_ADDRESS,
'--data', TEST_CONTRACT_METHOD,
'--json',
]);
const parsed = parseResult(stdout);
if (!parsed.txHash) throw new Error('No txHash returned');
return `Contract call tx: ${parsed.txHash.slice(0, 16)}...`;
}Set TEST_CONTRACT_METHOD to the hex-encoded calldata for the function you want to test. For example, a simple balanceOf call or a more complex swap.
Test 5: Balance Check
After all other tests, the agent snapshots its wallet balance. Compare against the pre-test balance to verify that only expected amounts were spent (gas costs). Large unexpected drops indicate a bug or misconfigured contract.
async function testBalanceCheck(address) {
const { stdout } = await execa('waap-cli', [
'balance',
'--chain-id', String(CHAIN_ID),
'--json',
]);
const parsed = parseResult(stdout);
return `Post-test balance: ${parsed.balance} (verify no unexpected drains)`;
}GitHub PR Detection
The agent polls three endpoints per repository on each cycle: open PRs, recently closed (merged) PRs, and failed workflow runs. It maintains in-memory sets of already-seen items to avoid re-processing.
const seenPRs = new Set();
const seenMerges = new Set();
const seenFailures = new Set();
// Open PRs — trigger wallet tests + CI
async function pollOpenPRs(repo) {
return githubGet(`/repos/${repo}/pulls?state=open&sort=created&direction=desc&per_page=25`);
}
// Recently merged PRs — trigger deployment
async function pollMergedPRs(repo) {
const closed = await githubGet(`/repos/${repo}/pulls?state=closed&sort=updated&direction=desc&per_page=10`);
return closed.filter((pr) => pr.merged_at !== null);
}
// Failed workflow runs — post failure comments
async function pollFailedRuns(repo) {
const data = await githubGet(`/repos/${repo}/actions/runs?status=failure&per_page=10`);
return data.workflow_runs ?? [];
}Posting Test Results
After the test suite finishes, the agent formats the results and posts them as a PR comment:
function formatTestResults(results, prNumber) {
const passed = results.filter((r) => r.passed).length;
const total = results.length;
const status = passed === total ? 'PASSED' : 'FAILED';
const lines = [
`Wallet Integration Tests: ${status} (${passed}/${total})`,
'',
'Test results:',
'',
];
for (const r of results) {
const icon = r.passed ? 'PASS' : 'FAIL';
lines.push(` ${icon}: ${r.name} (${r.durationMs}ms)`);
lines.push(` ${r.detail}`);
}
lines.push('');
lines.push(`Staging URL: ${STAGING_URL}`);
lines.push(`Chain ID: ${CHAIN_ID}`);
return lines.join('\n');
}
async function postTestResultsComment(repo, prNumber, results) {
const body = formatTestResults(results, prNumber);
await githubPost(`/repos/${repo}/issues/${prNumber}/comments`, { body });
}The PR comment looks like this:
Wallet Integration Tests: PASSED (5/5)
Test results:
PASS: Wallet Connect (SIWE) (1204ms)
Signed SIWE message, signature: 0x3a4b5c6d7e8f...
PASS: Send Transaction (3412ms)
Sent 0.0001 ETH to self, tx: 0x1234abcd5678...
PASS: Token Approval (2891ms)
Approval tx: 0xabcd1234efgh..., allowance confirmed
PASS: Contract Interaction (2103ms)
Contract call tx: 0x5678efgh9012...
PASS: Balance Check (156ms)
Post-test balance: 0.0492 (verify no unexpected drains)Deploy Trigger on Merge
When a PR merges to main or master, the agent POSTs to your deploy webhook. This works with Vercel deploy hooks or any custom endpoint.
async function triggerDeploy(repo, sha, trigger) {
if (!DEPLOY_WEBHOOK_URL) return;
const res = await request(DEPLOY_WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ repo, sha, trigger, agent: 'cicd-agent' }),
});
if (res.statusCode >= 200 && res.statusCode < 300) {
console.log(`Deploy triggered for ${repo}`);
}
}Vercel setup: Go to your Vercel project settings, then Git, then Deploy Hooks. Create a hook for the main branch and set DEPLOY_WEBHOOK_URL to the generated URL.
The Poll Loop
The main loop ties everything together. On each cycle, for each repo:
- Check for new open PRs — run the wallet test suite and dispatch CI
- Check for recently merged PRs — trigger deployment
- Check for failed CI runs — post failure comments
async function tick(address) {
for (const repo of GITHUB_REPOS) {
// 1. New open PRs — wallet tests + CI
const openPRs = await pollOpenPRs(repo);
for (const pr of openPRs) {
const key = `${repo}#${pr.number}`;
if (seenPRs.has(key)) continue;
seenPRs.add(key);
await triggerCI(repo, pr.head.ref, pr.number);
const results = await runWalletTestSuite(address, pr.number, repo);
await postTestResultsComment(repo, pr.number, results);
}
// 2. Merged PRs — deploy
const mergedPRs = await pollMergedPRs(repo);
for (const pr of mergedPRs) {
const key = `${repo}#${pr.number}`;
if (seenMerges.has(key)) continue;
seenMerges.add(key);
if (pr.base.ref === 'main' || pr.base.ref === 'master') {
await triggerDeploy(repo, pr.head.sha, `merge-pr-${pr.number}`);
}
}
// 3. Failed CI runs — failure comments
const failedRuns = await pollFailedRuns(repo);
for (const run of failedRuns) {
const key = `${repo}:${run.id}`;
if (seenFailures.has(key)) continue;
seenFailures.add(key);
for (const pr of run.pull_requests) {
await postFailureComment(repo, pr.number, run);
}
}
}
}
// Entrypoint — seed state, then poll forever
async function main() {
const me = await whoami();
const address = me.evmWalletAddress;
// Seed seen sets so existing PRs are not re-tested on first launch
for (const repo of GITHUB_REPOS) {
const openPRs = await pollOpenPRs(repo);
for (const pr of openPRs) seenPRs.add(`${repo}#${pr.number}`);
// ... same for merges and failures
}
while (true) {
await tick(address);
await new Promise((r) => setTimeout(r, POLL_MS));
}
}
main();Run the agent:
node agent.jsSafety: Privileges and Spend Limits
WaaP supports Privileges — pre-approved spending scopes that restrict what the agent’s wallet can do. For a test agent that sends real transactions on testnet:
# Only allow transactions to the agent's own address, the test contract, and the test token
waap-cli policy set --allowed-recipients 0xAgentAddress,0xTestContract,0xTestToken
# Set a daily spend limit to cap gas costs
waap-cli policy set --daily-spend-limit 0.01
# Require human approval for any single transaction above a threshold
waap-cli policy set --approval-threshold 0.005These constraints ensure the agent can only interact with the addresses you approve, and cannot exceed the daily budget — even if the agent code is compromised or a bug triggers excessive test runs.
For production use, configure the allowed recipients to match only your staging contracts. The agent should never need to send to arbitrary addresses.
Next Steps
Now that your agent tests wallet integrations on every PR:
- Preview URL integration: Replace the static
STAGING_URLwith the PR’s preview deployment URL (e.g., from Vercel’s deployment status API). - Expand the test suite: Add tests for multi-step flows like swap-then-stake, or test error handling by sending invalid calldata.
- Slack or Telegram alerts: Notify your team channel when wallet tests fail on a PR.
- Multi-chain testing: Run the same test suite on multiple chains by looping over chain IDs.
- GitHub webhook mode: Replace polling with GitHub webhooks for near-instant test runs on PR creation.
- Connect to the AEX dashboard: The Agent Exchange dashboard provides a live view of agent activity and test results.
Related
- WaaP CLI and Skills — CLI command reference and agent workflows
- Privileges — Pre-approved spending scopes for automated flows
- Send Transactions — Transaction lifecycle, async flow, and events