Skip to content

Commit 4e9f007

Browse files
chrisli30Chris Li
andauthored
feat: value-capture fee estimation and billing with tiered pricing (#506)
Co-authored-by: Chris Li <chris@avaprotocol.org>
1 parent 690bbc0 commit 4e9f007

30 files changed

Lines changed: 3144 additions & 1797 deletions

.github/workflows/publish-dev-docker.yml

Lines changed: 49 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
name: Publish Dev Docker Image
22

33
on:
4+
# Auto-build on PR merge to staging when protobuf changes — SDK CI pulls :latest to test against.
5+
# Non-proto changes don't affect the wire format, so no need to rebuild the image.
6+
push:
7+
branches:
8+
- staging
9+
paths:
10+
- 'protobuf/avs.proto'
11+
12+
# Manual dispatch for ad-hoc builds (specific tag, commit, or branch)
413
workflow_dispatch:
514
inputs:
615
branch_name:
@@ -22,13 +31,15 @@ on:
2231
type: boolean
2332
default: false
2433

34+
# Cancel in-flight builds when a newer commit lands on staging
35+
concurrency:
36+
group: dev-docker-${{ github.ref }}
37+
cancel-in-progress: true
38+
2539
jobs:
2640
publish-staging-build:
27-
# If you want to keep the merged check for manual dispatch, you might need to adjust context
28-
# For now, assuming manual dispatch doesn't need this specific PR context check.
29-
# if: github.event.pull_request.merged == true
3041
name: Build and Publish Dev Docker Image to Dockerhub
31-
runs-on: 'blacksmith-4vcpu-ubuntu-2404'
42+
runs-on: blacksmith-4vcpu-ubuntu-2404
3243
steps:
3344
- name: Login to Docker Hub
3445
uses: docker/login-action@v3
@@ -40,19 +51,27 @@ jobs:
4051
id: checkout_code
4152
uses: actions/checkout@v4
4253
with:
43-
fetch-depth: 0
44-
# Priority: git_tag, then commit_hash, then branch_name
45-
ref: ${{ github.event.inputs.git_tag && format('refs/tags/{0}', github.event.inputs.git_tag) || github.event.inputs.commit_hash || github.event.inputs.branch_name }}
54+
fetch-depth: 0
55+
# push event: github.ref is already correct (staging branch)
56+
# workflow_dispatch: priority is git_tag > commit_hash > branch_name
57+
ref: ${{ github.event_name == 'push' && github.ref || (github.event.inputs.git_tag && format('refs/tags/{0}', github.event.inputs.git_tag) || github.event.inputs.commit_hash || github.event.inputs.branch_name) }}
4658

4759
- name: Determine Tags and Build Args
4860
id: vars
4961
run: |
5062
SHORT_SHA=$(git rev-parse --short HEAD)
5163
PRIMARY_DOCKER_TAG=""
52-
BRANCH_FOR_CONTEXT="" # Sanitized branch name used for context with commits
64+
BRANCH_FOR_CONTEXT=""
5365
RELEASE_TAG_ARG=""
5466
55-
if [[ -n "${{ github.event.inputs.git_tag }}" ]]; then
67+
if [[ "${{ github.event_name }}" == "push" ]]; then
68+
# Automatic build from staging push (PR merge)
69+
BRANCH_NAME="${{ github.ref_name }}"
70+
echo "Source: Push to ${BRANCH_NAME} (auto-build)"
71+
PRIMARY_DOCKER_TAG="${BRANCH_NAME}-${SHORT_SHA}"
72+
BRANCH_FOR_CONTEXT="$BRANCH_NAME"
73+
RELEASE_TAG_ARG="${BRANCH_NAME}-${SHORT_SHA}"
74+
elif [[ -n "${{ github.event.inputs.git_tag }}" ]]; then
5675
echo "Source: Git Tag (${{ github.event.inputs.git_tag }})"
5776
PRIMARY_DOCKER_TAG="${{ github.event.inputs.git_tag }}"
5877
RELEASE_TAG_ARG="${{ github.event.inputs.git_tag }}"
@@ -65,18 +84,18 @@ jobs:
6584
BRANCH_FOR_CONTEXT="$SANITIZED_BRANCH_CTX"
6685
RELEASE_TAG_ARG="$SANITIZED_BRANCH_CTX-$SHORT_SHA"
6786
else
68-
PRIMARY_DOCKER_TAG="$SHORT_SHA" # Fallback if branch_name was empty
87+
PRIMARY_DOCKER_TAG="$SHORT_SHA"
6988
RELEASE_TAG_ARG="$SHORT_SHA"
7089
fi
7190
else
7291
# Source: Branch Name (no git_tag, no commit_hash)
73-
RAW_BRANCH_NAME="${{ github.event.inputs.branch_name }}" # Default 'staging'
92+
RAW_BRANCH_NAME="${{ github.event.inputs.branch_name }}"
7493
echo "Source: Branch ($RAW_BRANCH_NAME)"
7594
SANITIZED_BRANCH_NAME=$(echo "$RAW_BRANCH_NAME" | sed 's|/|-|g' | tr -cs 'a-zA-Z0-9.-_' '-' | sed 's/--\+/-/g' | sed 's/^-*//;s/-*$//')
7695
if [[ -n "$SANITIZED_BRANCH_NAME" ]]; then
77-
PRIMARY_DOCKER_TAG="$SANITIZED_BRANCH_NAME-$SHORT_SHA" # Always include SHA for branch builds
96+
PRIMARY_DOCKER_TAG="$SANITIZED_BRANCH_NAME-$SHORT_SHA"
7897
BRANCH_FOR_CONTEXT="$SANITIZED_BRANCH_NAME"
79-
RELEASE_TAG_ARG="$SANITIZED_BRANCH_NAME-$SHORT_SHA" # Embed branch-sha as version
98+
RELEASE_TAG_ARG="$SANITIZED_BRANCH_NAME-$SHORT_SHA"
8099
else
81100
PRIMARY_DOCKER_TAG="unknown-$SHORT_SHA"
82101
RELEASE_TAG_ARG="unknown-$SHORT_SHA"
@@ -104,26 +123,27 @@ jobs:
104123
echo "---- End Debug ----"
105124
shell: bash
106125

107-
- name: Show Build Configuration
126+
# Automatic push builds use amd64-only (fast) since SDK CI runs on ubuntu.
127+
# Manual dispatch respects the fast_build input.
128+
- name: Resolve build platforms
129+
id: platforms
108130
run: |
109-
echo "---- Build Configuration ----"
110-
if [[ "${{ inputs.fast_build }}" == "true" ]]; then
111-
echo "🚀 Fast Build Mode: Building for linux/amd64 only (~50% faster)"
112-
echo "Platforms: linux/amd64"
113-
echo "QEMU Setup: Skipped"
131+
if [[ "${{ github.event_name }}" == "push" || "${{ inputs.fast_build }}" == "true" ]]; then
132+
echo "value=linux/amd64" >> $GITHUB_OUTPUT
133+
echo "fast=true" >> $GITHUB_OUTPUT
134+
echo "Build mode: fast (linux/amd64 only)"
114135
else
115-
echo "🏗️ Full Build Mode: Building for linux/amd64,linux/arm64"
116-
echo "Platforms: linux/amd64,linux/arm64"
117-
echo "QEMU Setup: Enabled for ARM64"
136+
echo "value=linux/amd64,linux/arm64" >> $GITHUB_OUTPUT
137+
echo "fast=false" >> $GITHUB_OUTPUT
138+
echo "Build mode: full (linux/amd64,linux/arm64)"
118139
fi
119-
echo "---- End Configuration ----"
120140
shell: bash
121141

122142
- name: Set up QEMU
123143
uses: docker/setup-qemu-action@v3
124144
with:
125-
platforms: 'linux/arm64' # Use specific platform and default image
126-
if: ${{ !inputs.fast_build }}
145+
platforms: 'linux/arm64'
146+
if: steps.platforms.outputs.fast != 'true'
127147

128148
- name: Setup Blacksmith Builder
129149
uses: useblacksmith/setup-docker-builder@v1
@@ -134,21 +154,21 @@ jobs:
134154
with:
135155
images: avaprotocol/avs-dev
136156
tags: |
137-
# Primary tag (e.g., v1.6.0 or main-<sha>)
157+
# Primary tag (e.g., v1.6.0 or staging-abc1234)
138158
type=raw,value=${{ steps.vars.outputs.PRIMARY_DOCKER_TAG }}
139159
140160
# Always set latest tag for any build
141161
type=raw,value=latest
142-
162+
143163
- name: Build and push avs-dev Docker image
144164
uses: useblacksmith/build-push-action@v2
145165
with:
146166
build-args: |
147167
RELEASE_TAG=${{ steps.vars.outputs.RELEASE_TAG_ARG }}
148168
COMMIT_SHA=${{ steps.vars.outputs.SHORT_SHA }}
149-
platforms: ${{ inputs.fast_build && 'linux/amd64' || 'linux/amd64,linux/arm64' }}
169+
platforms: ${{ steps.platforms.outputs.value }}
150170
context: .
151171
file: dockerfiles/operator.Dockerfile
152-
push: true
172+
push: true
153173
tags: ${{ steps.meta.outputs.tags }}
154174
labels: ${{ steps.meta.outputs.labels }}

README.md

Lines changed: 2 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -32,121 +32,9 @@ ap-avs aggregator
3232

3333
Note: The Ava Protocol team currently manages the aggregator, and the communication IP address between the operator and the aggregator is hardcoded in the operator.
3434

35-
### Configurable Fee Rates
36-
37-
The aggregator supports configurable fee rates for workflow execution and monitoring. This allows for dynamic pricing adjustments without code deployments, environment-specific pricing, and A/B testing of pricing models.
38-
39-
#### Configuration Overview
40-
41-
Fee rates are completely **optional** and **backward compatible**. The aggregator will work exactly as before without any configuration changes. When fee rates are configured, they override the hardcoded defaults.
42-
43-
#### Adding Fee Configuration
44-
45-
To configure custom fee rates, add a `fee_rates:` section to your existing aggregator YAML configuration file:
46-
47-
```yaml
48-
# Your existing aggregator config (unchanged)
49-
environment: development
50-
db_path: /tmp/ap-avs/db
51-
ecdsa_private_key: your_private_key_here
52-
# ... other existing config fields ...
53-
54-
# 🆕 Optional fee rates configuration
55-
fee_rates:
56-
# Base fees (one-time per workflow)
57-
base_fee_usd: 0.0
58-
59-
# Monitoring fees (per minute) - cost to monitor triggers
60-
manual_monitoring_fee_usd_per_minute: 0.0 # Manual triggers free
61-
fixed_time_monitoring_fee_usd_per_minute: 0.000017 # ~$0.01/day
62-
cron_monitoring_fee_usd_per_minute: 0.000033 # ~$0.02/day
63-
block_monitoring_fee_usd_per_minute: 0.000033 # ~$0.02/day
64-
event_monitoring_fee_usd_per_minute: 0.000083 # ~$0.05/day base
65-
66-
# Per-execution fees - cost for each trigger activation
67-
manual_execution_fee_usd: 0.0 # Manual executions free
68-
scheduled_execution_fee_usd: 0.005 # $0.005 per scheduled execution
69-
block_execution_fee_usd: 0.01 # $0.01 per block trigger
70-
event_execution_fee_usd: 0.01 # $0.01 per event trigger
71-
72-
# Event monitoring scaling factors
73-
event_address_fee_usd_per_minute: 0.000008 # ~$0.005/day per address
74-
event_topic_fee_usd_per_minute: 0.000003 # ~$0.002/day per topic
75-
```
76-
77-
#### Configuration Files
78-
79-
The aggregator uses configuration files from the `config/` directory:
80-
81-
- **Default**: `./config/aggregator.yaml`
82-
- **Custom**: Specify with `--config` flag
83-
84-
**Available config files:**
85-
- `config/aggregator.yaml` - Default configuration
86-
- `config/aggregator-sepolia.yaml` - Sepolia testnet
87-
- `config/aggregator-base.yaml` - Base network
88-
- `config/aggregator-ethereum.yaml` - Ethereum mainnet
89-
90-
#### Starting with Custom Config
91-
92-
```bash
93-
# Use default config (./config/aggregator.yaml)
94-
./ap-avs aggregator
95-
96-
# Use specific config file
97-
./ap-avs aggregator --config ./config/aggregator-sepolia.yaml
98-
./ap-avs aggregator -c ./path/to/your/config.yaml
99-
```
100-
101-
#### Pricing Strategies
102-
103-
**All fields are optional** - only specify rates you want to override:
104-
105-
**Beta Testing (Free Everything):**
106-
```yaml
107-
fee_rates:
108-
scheduled_execution_fee_usd: 0.0
109-
block_execution_fee_usd: 0.0
110-
event_execution_fee_usd: 0.0
111-
# All monitoring fees default to existing values
112-
```
113-
114-
**Premium Pricing (Selective Overrides):**
115-
```yaml
116-
fee_rates:
117-
# Only override specific rates
118-
block_execution_fee_usd: 0.02 # 2x default rate
119-
event_execution_fee_usd: 0.015 # 1.5x default rate
120-
# All other fees use hardcoded defaults
121-
```
122-
123-
**Environment-Specific Pricing:**
124-
```yaml
125-
# Production - Higher rates
126-
fee_rates:
127-
scheduled_execution_fee_usd: 0.01 # 2x default
128-
129-
# Development - Free rates
130-
fee_rates:
131-
scheduled_execution_fee_usd: 0.0 # Free for development
132-
```
35+
### Fee Estimation
13336

134-
#### Default Values
135-
136-
When no `fee_rates` section is provided, these hardcoded defaults are used:
137-
138-
- **Base Fee**: $0.00 (one-time per workflow)
139-
- **Manual Monitoring**: $0.00/minute (free)
140-
- **Fixed Time Monitoring**: $0.000017/minute (~$0.01/day)
141-
- **Cron Monitoring**: $0.000033/minute (~$0.02/day)
142-
- **Block Monitoring**: $0.000033/minute (~$0.02/day)
143-
- **Event Monitoring**: $0.000083/minute (~$0.05/day base)
144-
- **Manual Execution**: $0.00 (free)
145-
- **Scheduled Execution**: $0.005 per execution
146-
- **Block Execution**: $0.01 per execution
147-
- **Event Execution**: $0.01 per execution
148-
- **Event Address Fee**: $0.000008/minute per monitored address
149-
- **Event Topic Fee**: $0.000003/minute per topic filter
37+
See [docs/FEE_ESTIMATION.md](docs/FEE_ESTIMATION.md) for the fee pricing model, configuration, and implementation details.
15038

15139
#### Benefits
15240

aggregator/rpc_server.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -548,6 +548,7 @@ func (r *RpcServer) sendUserOpWithGlobalWs(
548548
smartWalletAddress,
549549
nil, // saltOverride - not needed for already-deployed wallets
550550
r.smartWalletWsRpc, // Use global WebSocket client
551+
nil, // executionFeeWei - no platform fee for direct RPC calls (e.g., WithdrawFunds)
551552
r.config.Logger, // Pass logger for debug/verbose logging
552553
)
553554
} else {
@@ -560,6 +561,7 @@ func (r *RpcServer) sendUserOpWithGlobalWs(
560561
paymasterReq, // Use provided paymaster request
561562
smartWalletAddress,
562563
nil, // saltOverride - not needed for already-deployed wallets
564+
nil, // executionFeeWei - no platform fee for direct RPC calls
563565
r.config.Logger, // Pass logger for debug/verbose logging
564566
)
565567
}
@@ -1255,8 +1257,7 @@ func (r *RpcServer) EstimateFees(ctx context.Context, req *avsproto.EstimateFees
12551257

12561258
r.config.Logger.Info("✅ fee estimation completed successfully",
12571259
"user", user.Address.String(),
1258-
"final_total_usd", resp.FinalTotal.UsdAmount,
1259-
"estimation_method", resp.GasFees.EstimationMethod)
1260+
"pricing_model", resp.PricingModel)
12601261

12611262
return resp, nil
12621263
}

aggregator/task_engine.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import (
4545
"time"
4646

4747
"github.com/AvaProtocol/EigenLayer-AVS/core/apqueue"
48+
"github.com/AvaProtocol/EigenLayer-AVS/core/services"
4849
"github.com/AvaProtocol/EigenLayer-AVS/core/taskengine"
4950
"github.com/AvaProtocol/EigenLayer-AVS/core/taskengine/macros"
5051
sdklogging "github.com/Layr-Labs/eigensdk-go/logging"
@@ -96,8 +97,19 @@ func (agg *Aggregator) startTaskEngine(ctx context.Context) {
9697
agg.logger,
9798
)
9899

100+
// Create price service for fee conversion (USD → ETH)
101+
var priceService taskengine.PriceService
102+
if agg.config.MoralisApiKey != "" {
103+
priceService = services.GetMoralisService(agg.config.MoralisApiKey, agg.logger)
104+
} else {
105+
priceService = newFallbackPriceService()
106+
}
107+
108+
// Store price service on engine for use in simulation path
109+
agg.engine.SetPriceService(priceService)
110+
99111
// Create executor with engine reference for atomic execution indexing
100-
taskExecutor := taskengine.NewExecutor(agg.config.SmartWallet, agg.db, agg.logger, agg.engine)
112+
taskExecutor := taskengine.NewExecutor(agg.config.SmartWallet, agg.db, agg.logger, agg.engine, priceService)
101113
taskengine.SetCache(agg.cache)
102114
macros.SetRpc(agg.config.SmartWallet.EthRpcUrl)
103115

config/aggregator.example.yaml

Lines changed: 11 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -76,28 +76,18 @@ approved_operators:
7676
- "0xc6b87cc9e85b07365b6abefff061f237f7cf7dc3" # Operator 2
7777
- "0xa026265a0f01a6e1a19b04655519429df0a57c4e" # Operator 3 (newly added)
7878

79-
# 🆕 CONFIGURABLE FEE RATES
80-
# All fields are optional - only specify ones you want to override
79+
# FEE STRUCTURE: execution_fee + COGS + value_fee
80+
# - execution_fee: flat per-run platform fee
81+
# - COGS: per-node operational costs (gas, external APIs) — estimated automatically
82+
# - value_fee: workflow-level % of tx value, classified by urgency/importance
83+
# All fields optional — unset values use defaults shown below.
8184
fee_rates:
82-
# Base fees (one-time per workflow) - defaults to 0.0
83-
base_fee_usd: 0.0
84-
85-
# Monitoring fees (per minute) - cost to monitor triggers
86-
manual_monitoring_fee_usd_per_minute: 0.0 # Manual triggers free (default)
87-
fixed_time_monitoring_fee_usd_per_minute: 0.000017 # ~$0.01/day (default)
88-
cron_monitoring_fee_usd_per_minute: 0.000033 # ~$0.02/day (default)
89-
block_monitoring_fee_usd_per_minute: 0.000033 # ~$0.02/day (default)
90-
event_monitoring_fee_usd_per_minute: 0.000083 # ~$0.05/day (default)
91-
92-
# Per-execution fees - cost for each trigger activation
93-
manual_execution_fee_usd: 0.0 # Manual executions free (default)
94-
scheduled_execution_fee_usd: 0.005 # $0.005 per scheduled execution (default)
95-
block_execution_fee_usd: 0.01 # $0.01 per block trigger (default)
96-
event_execution_fee_usd: 0.01 # $0.01 per event trigger (default)
97-
98-
# Event monitoring scaling factors - additional costs for complex event monitoring
99-
event_address_fee_usd_per_minute: 0.000008 # ~$0.005/day per address (default)
100-
event_topic_fee_usd_per_minute: 0.000003 # ~$0.002/day per topic (default)
85+
execution_fee_usd: 0.02 # Flat per-execution platform fee ($0.02 default)
86+
credit_limit_usd: 0.0 # Max outstanding value fees before blocking (0 = block on any outstanding)
87+
tiers:
88+
tier_1: 0.03 # Value-capture group 1 (0.03% of tx value)
89+
tier_2: 0.09 # Value-capture group 2 (0.09% of tx value)
90+
tier_3: 0.18 # Value-capture group 3 (0.18% of tx value)
10191

10292
# Macro variables and secrets - Available globally to ALL workflows at runtime
10393
# Access in workflows via template syntax: {{apContext.configVars.SECRET_NAME}}

0 commit comments

Comments
 (0)