Skip to content

[CE-039] No /api/v1/pool/fee endpoint — fee only discoverable via quote response #79

Description

@Jaydbrown

Problem

The Core Engine has no /api/v1/pool/fee endpoint. While the backend proxies pool data from the Core Engine and pool.stats() returns fee_bps, there is no dedicated endpoint that returns JUST the pool fee configuration. This matters for:

  1. Frontend components that need to display the current fee without fetching full pool stats
  2. Clients that want to monitor fee changes over time (if fee is governance-updatable)
  3. Clients that need fee_bps for client-side calculation of output amounts

Current situation: Fee is buried inside GET /pool/stats:

{
  "price_token0_in_token1": 0.0001,
  "price_token1_in_token0": 10000,
  "k_invariant": "10000000000000",
  "fee_bps": 30,    ← fee is here but requires fetching all stats
  "reserves": {...}
}

Root Cause

No dedicated fee endpoint was designed. The fee is derivable from stats but fetching stats requires a Soroban RPC call to the contract, which is heavier than querying just the fee.

Impact

  • Fee-only queries fetch the entire pool state (expensive Soroban call)
  • No way to subscribe to fee changes specifically (webhook/polling would need to diff full stats)
  • Slight inefficiency for mobile app displaying just the fee percentage

Fix

Add GET /pool/fee to the Core Engine pool handler:

// In the Axum pool router:
pub async fn get_fee(State(state): State<Arc<AppState>>) -> impl IntoResponse {
    match state.pool_adapter.get_fee().await {
        Ok(fee_bps) => (StatusCode::OK, Json(json!({
            "fee_bps": fee_bps,
            "fee_percent": fee_bps as f64 / 100.0,
            "fee_description": format!("{:.2}% per swap", fee_bps as f64 / 100.0),
        }))).into_response(),
        Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({
            "error": e.to_string()
        }))).into_response(),
    }
}

Wire into router:

let pool_router = Router::new()
    .route("/pool/reserves", get(get_reserves))
    .route("/pool/stats", get(get_stats))
    .route("/pool/fee", get(get_fee))   // ← add this
    // ...

Response format:

GET /api/v1/pool/fee

{
  "fee_bps": 30,
  "fee_percent": 0.30,
  "fee_description": "0.30% per swap"
}

Also add to Backend pool/client.go:

func (c *Client) GetFee(ctx context.Context) (*feeConfig, error) {
    resp, err := c.get(ctx, "/pool/fee")
    // ...
}

And Backend pool handler:

// GET /api/v1/pool/fee
func (h *Handler) GetFee(c *gin.Context) {
    fee, err := h.svc.GetFee(c.Request.Context())
    if err != nil {
        utils.InternalServerError(c, "fee failed: "+err.Error())
        return
    }
    utils.OK(c, "fee retrieved", fee)
}

Steps to Verify

  1. curl http://localhost:8080/api/v1/pool/fee{"fee_bps": 30, "fee_percent": 0.30, ...}
  2. Response is cached (same 30-second TTL as other pool endpoints)
  3. Frontend pool.fee() API call returns fee_bps without fetching full stats
  4. Mobile app bpsToPercent(await poolService.getFee()) shows "0.30%"

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestpriority: mediumEdge cases, error handling, validation gaps

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions