Skip to content

Conversation

@0xBigBoss
Copy link
Member

Summary

  • Add @my/send-earn CLI package for automating Send Earn vault revenue collection
  • Support harvest (Merkl rewards), sweep (vault → safe), and fee distribution (affiliate contracts)
  • Add TVL command for total value locked reporting
  • Refactor @my/workflows to delegate activities to @my/send-earn library functions

Features

CLI Commands

  • send-earn dry-run - Preview harvestable revenue and vault balances
  • send-earn harvest - Claim MORPHO/WELL rewards from Merkl distributor
  • send-earn sweep - Transfer tokens from vaults to revenue safe
  • send-earn distribute-fees - Process affiliate contract fee distributions
  • send-earn tvl - Display total value locked across all vaults

Output Formats

All commands support --format flag: table (default), json, csv, markdown

Technical Changes

  • New package: packages/send-earn/ with full library + CLI
  • Merkl API v4 integration for reward claiming
  • ERC4626 vault type detection (Morpho vs Moonwell)
  • Database queries for active vaults and affiliate contracts
  • Comprehensive test coverage (unit + Anvil fork tests)

Test plan

  • yarn typecheck passes
  • yarn biome:check passes
  • Unit tests pass (yarn workspace @my/send-earn test)
  • Anvil integration tests pass (yarn workspace @my/send-earn test:anvil)
  • CLI help displays all commands (bun packages/send-earn/bin/send-earn.ts --help)

🤖 Generated with Claude Code

@0xBigBoss 0xBigBoss force-pushed the bb/send-earn-automation branch from cb648ba to 2b346a2 Compare January 9, 2026 23:41
0xBigBoss and others added 26 commits January 13, 2026 20:23
Implement automated claiming of MORPHO and WELL token rewards from Merkl
distribution system for Send Earn vaults (SEND-172).

- Add MerklDistributor contract to wagmi config with claim function ABI
- Add send_earn_reward_claims table schema with indexes and RLS policies
- Create Temporal workflow with read (retryable) and write (non-retryable) activities:
  - getActiveVaultsActivity: query vaults from send_earn_create table
  - fetchClaimableRewardsActivity: call Merkl API with rate limiting/retry
  - executeClaimActivity: batch claim with individual fallback on failure
  - recordClaimsActivity: insert claim records to database
- Add configurable minimum claim thresholds for gas efficiency
- Include design documentation and unit tests

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
Redesign architecture based on clarification that MORPHO/WELL tokens are
protocol revenue for Send Foundation, not yield for depositors.

Key changes:
- Two-step flow: harvest (Merkl→vault) + sweep (vault→revenue safe)
- Terminology: claim/rewards → harvest/sweep/revenue throughout
- Safety check: verify vault.collections() == REVENUE_SAFE before sweep
- Track both steps in separate DB tables (harvest + sweep)
- Monthly schedule + manual trigger
- Dry run mode for simulation

Destination: Send Foundation revenue safe (0x65049C4B8e970F5bcCDAE8E141AA06346833CeC4)

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
…sweep

Refactor rewards claiming workflow to revenue collection with two-step flow:
1. Harvest: Claim MORPHO/WELL rewards from Merkl to vault addresses
2. Sweep: Call SendEarn.collect() to transfer tokens to revenue safe

Key changes:
- Rename rewards-claim-workflow → revenue-collection-workflow
- Add sweepToRevenueActivity with safety check (verify collections == REVENUE_SAFE)
- Add getVaultBalancesActivity for dry run mode
- Add DryRunData type with harvestableFromMerkl, currentVaultBalances, expectedRevenue
- Add RevenueError with step discriminator ('harvest' | 'sweep')
- Add Temporal schedule (monthly 1st at 6 AM UTC)
- Add manual API trigger at /api/cron/send-earn-revenue
- Add database tables: send_earn_revenue_harvest, send_earn_revenue_sweep
- Bump workflows version to 0.0.9

Safety: sweepToRevenueActivity verifies vault.collections() matches REVENUE_SAFE
before executing sweep to prevent sending tokens to wrong address.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
Add integration test infrastructure using @morpho-org/test with Vitest:
- Setup forked Base mainnet using Anvil for realistic contract interactions
- Test token balance reading (MORPHO, WELL)
- Test ERC20 storage manipulation for test setup
- Test Merkl distributor contract accessibility
- Test account impersonation for collector

New scripts:
- yarn test:anvil - Run fork integration tests
- yarn test:anvil:watch - Watch mode for development

Dependencies added:
- @morpho-org/test ^2.6.6 - Vitest fixtures for Anvil fork tests
- vitest ^2.1.8 - Test runner (Jest kept for existing unit tests)

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Replace Jest with Vitest for faster ESM-native testing
- Add separate configs for unit tests (vitest.config.ts) and anvil fork tests (vitest.anvil.config.ts)
- Update test imports from @jest/globals to vitest (jest.* → vi.*)
- Remove Jest dependencies (@jest/globals, jest, ts-jest, nyc)
- Add @vitest/coverage-v8 for coverage support
- Add vitest.setup.ts with nock configuration for unit tests
- Anvil tests run without nock to allow real network access

Test commands:
- yarn test: unit tests
- yarn test:anvil: anvil fork integration tests
- yarn test:all: all tests

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Update README.md to document Vitest test configurations
- Remove jest.config.ts from tsconfig.json includes
- Delete obsolete docs/todo-fix-workflows-jest-paths.md

Addresses review ISSUE-3.

Co-Authored-By: Claude <noreply@anthropic.com>
- Add SEND_EARN_VAULT address and MERKL_DISTRIBUTOR_ABI to test setup
- Add mock SendEarn vault bytecode for deploying test contracts
- Add tests that deploy mock vault and call collections() on-chain
- Add tests for Merkl claim flow (claimed amounts, revert behavior)
- Verify safety check correctly passes/fails based on collections address

These tests exercise the actual on-chain call paths used by
sweepToRevenueActivity, catching regressions that pure logic
tests would miss.

Addresses review ISSUE-1, ISSUE-2, ISSUE-4.

Co-Authored-By: Claude <noreply@anthropic.com>
Add AbortController with configurable timeout to prevent fetch calls
from hanging indefinitely. Timeout is controlled via MERKL_API_TIMEOUT_MS
environment variable (default: 30 seconds).

- Add merklApiTimeoutMs to RevenueConfig interface
- Wrap fetch calls with AbortController signal
- Properly cleanup timeout in finally block

Addresses review ISSUE-5.

Co-Authored-By: Claude <noreply@anthropic.com>
…lection

Implements the @my/send-earn package with:
- CLI commands: dry-run, harvest, sweep
- Library exports for workflows integration
- Merkl API client with rate limiting and retries
- Vault operations (balance reading, harvest, sweep)
- Output formatters (table, json, csv, markdown)
- Database queries using pg (node-postgres)

The package provides a reusable foundation for the revenue collection
workflow and can be used as a standalone CLI tool.

Includes specification document defining the package API and success criteria.

Co-Authored-By: Claude <noreply@anthropic.com>
…bility

Address review feedback from iteration 1:

- Add @my/send-earn as workspace dependency in @my/workflows
- Import shared types (REVENUE_ADDRESSES, VaultRevenue, etc.) from @my/send-earn
- Fix harvest fail-fast: batch failures now throw instead of falling back to
  individual claims (stale proofs require fresh data, not retry)
- Fix Anvil test port collision: add pkill cleanup and maxConcurrency=1

All verifications pass: typecheck, lint, unit tests, anvil tests.

Co-Authored-By: Claude <noreply@anthropic.com>
…ctions

Complete the refactor to use @my/send-earn library:

- Activities now delegate to library functions:
  - getActiveVaultsActivity → getActiveVaults
  - fetchHarvestableRevenueActivity → fetchHarvestableRevenue
  - getVaultBalancesActivity → getVaultBalances
  - harvestRevenueActivity → executeHarvest
  - sweepToRevenueActivity → executeSweep

- Config updated to use createConfig from @my/send-earn with
  SUPABASE_DB_URL and BASE_RPC_URL env vars

- Types re-exported from @my/send-earn, keeping only workflow-specific
  HarvestResult and SweepResult types local

- Tests updated to mock @my/send-earn functions instead of local impls

All verifications pass: typecheck, lint, 35 unit tests, 15 anvil tests.

Co-Authored-By: Claude <noreply@anthropic.com>
- Remove execSync pkill that could kill unrelated Anvil processes
- @morpho-org/test manages Anvil lifecycle; added comment for manual cleanup
- Document Foundry and native build tool prerequisites in README

Co-Authored-By: Claude <noreply@anthropic.com>
- Align zx version in @my/send-earn to ^8.1.2 (matches monorepo)
- Update vitest in @0xsend/webauthn-authenticator to ^2.1.8 (fixes check-deps)
- Move REVENUE_ADDRESSES to types.ts to prevent pg module import in workflow
  bundle (Temporal requires deterministic code - pg uses Node.js modules)
- Document Python setuptools requirement for Python 3.12+ in README

Co-Authored-By: Claude <noreply@anthropic.com>
- Add yarn resolution for better-sqlite3@11.5.0 in root package.json
- @snaplet/snapshot pins 8.5.0 which has C++ compilation issues with Node.js 22
- The newer version includes prebuilt binaries and fixes for Node.js 22

Co-Authored-By: Claude <noreply@anthropic.com>
When --vault flag is specified, use the provided vault addresses directly
instead of fetching from database. This enables CLI testing without a
database connection.

Tested against local Anvil fork:
- dry-run with all output formats (table, json, csv, markdown)
- harvest and sweep commands
- Error handling for missing env vars

Co-Authored-By: Claude <noreply@anthropic.com>
Addresses review feedback ISSUE-1:
- Normalize vault addresses to lowercase for consistent comparison
- Deduplicate using Set to prevent double-counting in dry-run or
  duplicate claims/sweeps from repeated --vault flags

Co-Authored-By: Claude <noreply@anthropic.com>
The Merkl v4 API returns an array structure with rewards nested inside
chain data, not an object keyed by token address. This caused the CLI
to show 0 rewards when querying production vaults.

- Update MerklRewardsResponse type to match v4 API structure
- Add MerklTokenInfo and MerklV4Reward types for proper typing
- Iterate rewards array and match by token.address field
- Tested against production: Platform Vault shows 74.54 MORPHO + 21,176.40 WELL

Co-Authored-By: Claude <noreply@anthropic.com>
Add CLI commands and functions to distribute performance fee shares
from SendEarnAffiliate contracts:

- `fees-dry-run`: Display pending fee shares for affiliate contracts
- `distribute-fees`: Execute fee distribution via pay() on affiliates

Key changes:
- New types for fee distribution tracking (shares, not assets)
- `getFeeRecipientInfo()` identifies affiliate vs direct recipients
- `executeFeeDistribution()` calls pay() on affiliate contracts
- All output formats (table/json/csv/markdown) include fee share data
- `dry-run` now shows fee shares alongside Merkl rewards

Per spec: non-Revenue Safe contracts are classified as affiliates;
affiliate metadata (platformVault, payVault) is optional.

Co-Authored-By: Claude <noreply@anthropic.com>
Add TVL (Total Value Locked) tracking specification:
- VaultTVL and TVLResult interfaces
- getVaultsTVL() implementation details
- CLI command example (send-earn tvl)
- Output format examples
- Use cases: analytics, revenue projections, monitoring, reporting

Co-Authored-By: Claude <noreply@anthropic.com>
- Add VaultTVL and TVLResult interfaces to types.ts
- Implement getVaultsTVL() in vaults.ts using ERC4626 totalAssets(), totalSupply(), and VAULT() getter
- Add tvl() high-level function to revenue.ts
- Export new types and functions from index.ts
- Add send-earn tvl CLI command with all output formats (table, json, csv, markdown)
- Format USDC amounts with commas and 2 decimal places

Co-Authored-By: Claude <noreply@anthropic.com>
- Fix table output to use box-drawing characters matching spec format
- Add UnderlyingVaultType discriminator (Morpho, Moonwell, Unknown)
- Add detectVaultType() to identify Morpho vaults via MORPHO() function
- Update all output formats (table, json, csv, markdown) to show vault type
- Table now displays: Vault | TVL (USDC) | Underlying (Morpho/Moonwell)

Co-Authored-By: Claude <noreply@anthropic.com>
Title row was 66 chars (pad to 64 + 2 borders) but borders are 67 chars.
Fixed by padding title content to 65 chars.

Co-Authored-By: Claude <noreply@anthropic.com>
Spec shows continuous top border for title spanning full width, with
column separators only appearing on the row below the title.

Co-Authored-By: Claude <noreply@anthropic.com>
Type was renamed to MerklV4Reward in @my/send-earn but wasn't used.

Co-Authored-By: Claude <noreply@anthropic.com>
The Dockerfile explicitly lists all package.json files for yarn install.

Co-Authored-By: Claude <noreply@anthropic.com>
Add tables to track the two-step revenue collection process:
- send_earn_reward_claims: legacy reward tracking
- send_earn_revenue_harvest: Merkl → vault claims
- send_earn_revenue_sweep: vault → revenue safe transfers

Includes summary views for analytics and appropriate RLS policies.

Co-Authored-By: Claude <noreply@anthropic.com>
@0xBigBoss 0xBigBoss force-pushed the bb/send-earn-automation branch from 2b346a2 to 7cdecbd Compare January 13, 2026 20:24
Replace @jest/globals imports with vitest for consistency with the
rest of the workflows package which migrated to Vitest.

Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions
Copy link

Vercel Unique URL: https://sendapp-4xytjcwl9-0xsend.vercel.app
Vercel Preview URL: sendapp-bb-send-earn-automation-0xsend.vercel.app
Last Commit: ede7cfe

@github-actions
Copy link

Playwright Report

Artifacts: View all artifacts | JSON Report: json-report--attempt-1 | HTML Report: html-report--attempt-1

Summary

Passed Skipped Failed Flaky Duration Pass Rate
54 2 34 1 1467.12s 60.7%

Suites

account-sendtag-add.onboarded.spec.ts (5/5 passed)

can visit add sendtags page

  • chromium: ✅ passed

can add a pending tag

  • chromium: ✅ passed

cannot add an invalid tag name

  • chromium: ✅ passed

cannot add more than 5 tags

  • chromium: ✅ passed

cannot confirm a tag without paying

  • chromium: ✅ passed
account-sendtag-checkout.onboarded.spec.ts (1/3 passed)

can confirm a tag

  • chromium: ✅ passed

can refer a tag

  • chromium: ❌ failed
    Error: �[31mTimed out 5000ms waiting for �[39m�[2mexpect(�[22m�[31mlocator�[39m�[2m).�[22mtoHaveValue�[2m(�[22m�[32mexpected�[39m�[2m)�[22m
    

can refer multiple tags in separate transactions

  • chromium: ❌ failed
    Error: �[31mTimed out 5000ms waiting for �[39m�[2mexpect(�[22m�[31mlocator�[39m�[2m).�[22mtoHaveValue�[2m(�[22m�[32mexpected�[39m�[2m)�[22m
    
account-settings-backup.onboarded.spec.ts (2/2 passed)

can backup account

  • chromium: ✅ passed

can remove a signer

  • chromium: ✅ passed
account.logged-in.spec.ts (2/2 passed)

can visit account page

  • chromium: ✅ passed

can update profile

  • chromium: ✅ passed
activity.onboarded.spec.ts (0/1 passed)

can visit activity page and see correct activity feed

  • chromium: ❌ failed
    Error: �[31mTimed out 5000ms waiting for �[39m�[2mexpect(�[22m�[31mlocator�[39m�[2m).�[22mtoContainText�[2m(�[22m�[32mexpected�[39m�[2m)�[22m
    
contacts.onboarded.spec.ts (0/0 passed)
earn.onboarded.spec.ts (0/0 passed)
home.onboarded.spec.ts (0/1 passed)

can visit token detail page

  • chromium: ❌ failed
    TimeoutError: page.waitForResponse: Timeout 30000ms exceeded while waiting for event "response"
    
leaderboard.logged-in.spec.ts (0/0 passed)

can visit leaderboard page

  • chromium: ⏭️ skipped
onboarding.logged-in.spec.ts (0/1 passed)

can visit onboarding page

  • chromium: ❌ failed
    �[31mTest timeout of 30000ms exceeded.�[39m
    
profile-external-address.anon.spec.ts (0/0 passed)
profile-external-address.onboarded.spec.ts (0/0 passed)
profile.anon.spec.ts (1/2 passed)

anon user can visit public profile

  • chromium: ❌ failed
    Error: expect.toBeVisible: Error: strict mode violation: getByText('Peggie Stanton') resolved to 2 elements:
    

anon user cannot visit private profile

  • chromium: ✅ passed
profile.logged-in.spec.ts (0/1 passed)

logged in user needs onboarding before visiting profile

  • chromium: ❌ failed
    Error: �[31mTimed out 5000ms waiting for �[39m�[2mexpect(�[22m�[31mlocator�[39m�[2m).�[22mtoBeVisible�[2m()�[22m
    
profile.onboarded.spec.ts (4/4 passed)

can visit other user profile and send by tag

  • chromium: ✅ passed

can visit my own profile

  • chromium: ✅ passed

can visit private profile

  • chromium: ✅ passed

can view activities between another profile

  • chromium: ✅ passed
send-token-upgrade.onboarded.spec.ts (0/1 passed)

can upgrade their Send Token V0 to Send Token V1

  • chromium: ❌ failed
    Error: �[2mexpect(�[22m�[31mreceived�[39m�[2m).�[22mtoEqual�[2m(�[22m�[32mexpected�[39m�[2m) // deep equality�[22m
    
send.onboarded.spec.ts (0/13 passed)

can send USDC starting from profile page

  • chromium: ❌ failed
    Error: �[31mTimed out 5000ms waiting for �[39m�[2mexpect(�[22m�[31mlocator�[39m�[2m).�[22mtoBeVisible�[2m()�[22m
    

can send USDC using tag starting from home page

  • chromium: ❌ failed
    Error: �[31mTimed out 10000ms waiting for �[39m�[2mexpect(�[22m�[31mlocator�[39m�[2m).�[22mtoBeVisible�[2m()�[22m
    

can send USDC using sendid starting from home page

  • chromium: ❌ failed
    Error: �[31mTimed out 10000ms waiting for �[39m�[2mexpect(�[22m�[31mlocator�[39m�[2m).�[22mtoBeVisible�[2m()�[22m
    

can send USDC using address starting from home page

  • chromium: ❌ failed
    Error: expect.toBeVisible: Error: strict mode violation: getByText('0x3FE...Bc83') resolved to 2 elements:
    

can send SEND starting from profile page

  • chromium: ❌ failed
    Error: Send button still visible
    

can send SEND using tag starting from home page

  • chromium: ❌ failed
    Error: �[31mTimed out 10000ms waiting for �[39m�[2mexpect(�[22m�[31mlocator�[39m�[2m).�[22mtoBeVisible�[2m()�[22m
    

can send SEND using sendid starting from home page

  • chromium: ❌ failed
    Error: �[31mTimed out 10000ms waiting for �[39m�[2mexpect(�[22m�[31mlocator�[39m�[2m).�[22mtoBeVisible�[2m()�[22m
    

can send SEND using address starting from home page

  • chromium: ❌ failed
    Error: expect.toBeVisible: Error: strict mode violation: getByText('0x121...936F') resolved to 2 elements:
    

can send ETH starting from profile page

  • chromium: ❌ failed
    Error: Send button still visible
    

can send ETH using tag starting from home page

  • chromium: ❌ failed
    Error: �[31mTimed out 10000ms waiting for �[39m�[2mexpect(�[22m�[31mlocator�[39m�[2m).�[22mtoBeVisible�[2m()�[22m
    

can send ETH using sendid starting from home page

  • chromium: ❌ failed
    Error: �[31mTimed out 10000ms waiting for �[39m�[2mexpect(�[22m�[31mlocator�[39m�[2m).�[22mtoBeVisible�[2m()�[22m
    

can send ETH using address starting from home page

  • chromium: ❌ failed
    Error: expect.toBeVisible: Error: strict mode violation: getByText('0x649...8FE6') resolved to 2 elements:
    

cannot send below minimum amount for SEND token

  • chromium: ❌ failed
    Error: �[31mTimed out 10000ms waiting for �[39m�[2mexpect(�[22m�[31mlocator�[39m�[2m).�[22mtoBeVisible�[2m()�[22m
    
sendtag-happy-path.onboarded.spec.ts (0/1 passed)

sendtag complete happy path - create, confirm, and change main tag

  • chromium: ❌ failed
    Error: �[31mTimed out 5000ms waiting for �[39m�[2mexpect(�[22m�[31mlocator�[39m�[2m).�[22mtoBeVisible�[2m()�[22m
    

sendtag main tag succession - auto-assigns new main when current is deleted

  • chromium: ⏭️ skipped
sign-in.anon.spec.ts (3/3 passed)

redirect on sign-in

  • chromium: ✅ passed

redirect to send page on sign-in with recipient params

  • chromium: ✅ passed

old user can login using phone number

  • chromium: ✅ passed
sign-up.anon.spec.ts (2/2 passed)

can sign up

  • chromium: ✅ passed

country code is selected based on geoip

  • chromium: ✅ passed
swap.onboarded.spec.ts (2/5 passed)

can swap USDC for SEND

  • chromium: ❌ failed
    Error: Timeout 3000ms exceeded while waiting on the predicate
    

can swap USDC for ETH

  • chromium: ❌ failed
    Error: Timeout 3000ms exceeded while waiting on the predicate
    

can refresh swap form and preserve filled data

  • chromium: ❌ failed
    Error: Timeout 3000ms exceeded while waiting on the predicate
    

can't access form page without accepting risk dialog

  • chromium: ✅ passed

can't access summary page without filling swap form

  • chromium: ✅ passed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants