Fees API

Configure fees, preview them before sending, and pull historical fee analytics.

Authentication: Dual-auth (JWT or HMAC) on every route. Base path: /api/v1/fees


Fee resolution chain

When HasaPay calculates a fee, it walks a five-tier lookup and uses the first match:

  1. Address override — per-(child address) row in address_fee_overrides
  2. Org × chain × network × token — exact match in fee_configurations
  3. Org × chain × network — wildcard across tokens
  4. Org × chain — wildcard across networks
  5. Platform defaults — per-chain defaults table (deposit-side only; see below)

Platform deposit defaults

These apply only when none of the four upstream tiers matches. Note that withdrawal-side platform defaults are zero — to charge a withdrawal fee, configure a fee_configurations row at the org or chain level.

Chain Network Deposit fee Min deposit
Ethereum mainnet 1% $10
Ethereum sepolia 0% $0
Polygon mainnet 0.5% $1
Polygon amoy 0% $0
Tron mainnet flat $5 $20
Tron shasta 0% $0
Solana mainnet 1% $10
Solana devnet 0% $0
Bitcoin mainnet 1% $10
Bitcoin testnet 0% $0

Rate convention. Percentage rates are stored as decimals — 0.02 = 2%. Multiply by 100 for display. The one exception is effective_fee_rate in some response payloads, which is pre-multiplied.

min_deposit_usd is "accept-and-defer", not reject. A deposit below the threshold is still credited to the ledger; the sweep is just deferred until accumulated balance crosses the threshold.


Get fee config

GET /api/v1/fees/config

Returns the org's resolved fee configuration — platform defaults, org overrides, gasless flags, rev-share settings. Use it to render a "current fees" panel.

Example response

{
  "data": {
    "deposit_fee": { "enabled": true, "type": "percentage", "rate": 0.005 },
    "withdrawal_fee": { "enabled": true, "type": "percentage", "rate": 0.01 },
    "org_fee": {
      "enabled": false,
      "deposit_fee_type": "percentage",
      "deposit_fee_rate": 0,
      "withdrawal_fee_type": "percentage",
      "withdrawal_fee_rate": 0
    },
    "rev_share": { "enabled": false, "percentage": 0 },
    "gasless": { "enabled": true }
  }
}

Address fee overrides

Per-(child address) overrides. Useful when one customer's payment terms differ from your org default.

List overrides

GET /api/v1/fees/addresses
{
  "data": {
    "overrides": [ /* AddressFeeOverride[] */ ],
    "total": 3
  }
}

Get one override

GET /api/v1/fees/addresses/:addressId

Set / upsert an override

PUT /api/v1/fees/addresses/:addressId

Request body — all fields optional, send only what you want to change

Field Type Description
fees_enabled bool Master switch — false disables all fees on this address
deposit_fee_override bool Set true to use the address-level deposit fee fields instead of org defaults
deposit_fee_type string percentage or flat
deposit_fee_rate float Decimal rate (0.02 = 2%) or flat amount
deposit_fee_min string Min absolute fee (token base units)
deposit_fee_max string Max absolute fee cap
withdrawal_fee_override bool Same idea for withdrawals
withdrawal_fee_type string percentage or flat
withdrawal_fee_rate float Rate or flat amount
withdrawal_fee_min / max string Same as deposit
gasless_enabled bool Address-level gasless toggle
notes string Free-text label

Delete an override (revert to org default)

DELETE /api/v1/fees/addresses/:addressId

Withdrawal fee estimate

GET /api/v1/fees/estimate?token=USDC&chain=ethereum&network=sepolia&amount=100

Server-side fee quote for a withdrawal. Walks the full resolution chain and returns gas + protocol + total broken out.

Query params

Param Required Description
token Yes Token symbol (e.g. USDC)
chain Yes Chain
network Yes Network
amount Yes Human-readable amount (e.g. 100 for 100 USDC)
address_id No Child address UUID — applies any per-address override

Response

{
  "data": {
    "token": "USDC",
    "chain": "ethereum",
    "network": "sepolia",
    "send_amount": {
      "amount": "100",
      "amount_raw": "100000000",
      "token": "USDC"
    },
    "protocol_fee": {
      "amount": "1.00",
      "amount_raw": "1000000",
      "token": "USDC",
      "breakdown": {
        "platform_fee": { "amount": "1.00", "rate": 0.01 },
        "org_fee": { "amount": "0", "rate": 0 }
      }
    },
    "gas_fee": {
      "native_symbol": "ETH",
      "native_amount": "0.0008",
      "usd": 2.10,
      "token_equivalent": "2.10",
      "token": "USDC",
      "buffer_pct": 30
    },
    "total_fee": {
      "amount": "3.10",
      "amount_usd": 3.10,
      "token": "USDC"
    },
    "total_deducted": {
      "amount": "103.10",
      "amount_raw": "103100000",
      "token": "USDC"
    },
    "quote_valid_secs": 60
  }
}
  • send_amount = what the recipient receives
  • total_deducted = what comes out of your balance
  • gas_fee.buffer_pct = the safety buffer added to live gas (30% by default)
  • quote_valid_secs = how long this estimate is reliably accurate before gas drifts

Deposit fee estimate

GET /api/v1/fees/deposit/estimate?token=USDC&chain=ethereum&network=sepolia&amount=100

Mirror of the withdrawal estimate, but for incoming deposits. Used to show "you will receive X" before a customer sends.

Query params

Same as the withdrawal estimate.

Response

{
  "data": {
    "token": "USDC",
    "chain": "ethereum",
    "network": "sepolia",
    "amount": {
      "amount": "100",
      "amount_raw": "100000000",
      "amount_usd": 100.00,
      "token": "USDC"
    },
    "protocol_fee": {
      "amount": "1.00",
      "amount_raw": "1000000",
      "amount_usd": 1.00,
      "token": "USDC",
      "breakdown": {
        "platform_fee": { "amount": "1.00", "amount_raw": "1000000", "amount_usd": 1.00, "rate": 0.01 },
        "org_fee": { "amount": "0", "amount_raw": "0", "amount_usd": 0, "rate": 0 }
      }
    },
    "rev_share_earned": { "amount": "0", "amount_raw": "0", "token": "USDC" },
    "net_received": { "amount": "99.00", "amount_raw": "99000000", "token": "USDC" },
    "fee_source": "platform_default",
    "min_deposit_usd": 10,
    "below_min_deposit": false,
    "quote_valid_secs": 60
  }
}
Field Meaning
net_received Amount credited to the customer's wallet after fees
fee_source Which tier resolved (address_override, org_chain_token, chain_default, platform_default, etc.)
min_deposit_usd The applicable minimum deposit threshold
below_min_deposit Hint flag — if true, the deposit will be accepted but the sweep deferred until accumulated balance crosses the threshold

Fee analytics

Summary

GET /api/v1/fees/summary?period=current_month&chain=ethereum

Aggregate fees over a period, broken down by chain, token, and sub-period.

Query params

Param Default Description
period current_month current_month, last_month, current_quarter, last_quarter, last_30d, last_90d, ytd
from_date / to_date — Override period with explicit ISO dates
chain — Filter by chain
token — Filter by token symbol
network_type testnet testnet or mainnet

Response

{
  "data": {
    "period": { "label": "June 2026", "from": "2026-06-01", "to": "2026-06-30" },
    "totals": {
      "deposit_count": 412,
      "deposit_volume_usd": 234500.00,
      "platform_fees_paid_usd": 2345.00,
      "org_fees_earned_usd": 0,
      "rev_share_earned_usd": 0,
      "net_cost_usd": 2345.00
    },
    "by_chain": [
      { "chain": "ethereum", "deposit_volume_usd": 145000.00, "platform_fees_paid_usd": 1450.00 }
    ],
    "by_token": [
      { "token": "USDC", "deposit_volume_usd": 200000.00, "platform_fees_paid_usd": 2000.00 }
    ],
    "by_period": [
      { "label": "2026-06-01", "deposit_volume_usd": 7500.00, "platform_fees_paid_usd": 75.00 }
    ]
  }
}

Sub-cent fee precision. USD aggregates are recomputed inline from raw token amounts for stablecoins (USDC, USDT, DAI, BUSD, TUSD, USDP, PYUSD, FDUSD) so fees smaller than one cent are not rounded away. Non-stablecoins fall back to the stored amount_usd value at fee-charge time.

History

GET /api/v1/fees/history?page=1&limit=50

Per-deposit rows with attached fees + running period totals. Use this for an "activity log" view rather than aggregate summaries.

Sources (per-wallet ranking)

GET /api/v1/fees/sources?sort_by=volume&period=current_month

Ranks child addresses + master wallets by activity — useful for spotting "this one customer is generating 90% of my fees" patterns.

Query params

Param Default Description
sort_by volume volume, fees_paid, tx_count, last_active
period current_month Same period values as summary
chain / token / network_type — Standard filters
limit / offset 50 / 0 Pagination

Source-level rev-share. Rev-share is earned at the org level, not per source. Source rows pro-rate the org's rev-share by each source's share of fees paid, so net_cost_usd = fees_paid_usd − rev_share_earned_usd is meaningful per source while still reflecting how rev-share is actually earned.

Single source detail

GET /api/v1/fees/sources/:source_id

:source_id is either a child address ID or a master wallet ID — the handler resolves which. Returns 403 if the source belongs to a different org.

Response includes the same per-chain / per-token / per-period breakdowns as the summary, scoped to that one source, plus a recent-activity log.

Fees for one transaction

GET /api/v1/fees/transactions/:id

Returns the fee roll-up for a single transaction — platform_fee, org_fee, rev_share, and total_fee, each with raw + formatted + USD amounts. Same source-of-truth as the data.fees block on deposit.* / withdrawal.* webhooks, so the numbers reconcile exactly.

Gas spent on sweeps

GET /api/v1/fees/gas?period=current_month

Reports the native gas/energy/bandwidth your sweeps consumed (paid from the per-chain fee wallet), broken down by chain/network. This is your cost of moving funds, separate from the platform/org fees above.


Fee fields reference

Field name Format Notes
*_rate float (decimal) 0.005 = 0.5%. Multiply by 100 to display
effective_fee_rate float (percentage) Pre-multiplied — already in % form. Don't multiply again
*_fee_type string percentage or flat
*_min, *_max string Absolute amounts in token base units
*_fees_paid_usd float Aggregated in USD using current prices
net_cost_usd float fees_paid_usd − rev_share_earned_usd

Relationship to webhook payloads

The fee figures surfaced here use the same source-of-truth (ledger_entries) as the data.fees block on deposit.* and withdrawal.* webhooks. Reconciling a single deposit from a webhook plus calling GET /fees/history for the same period should produce identical numbers — webhooks remove the need for the second call when you only need per-transaction reconciliation.


Errors

Code Cause
validation_error Bad query params, invalid fee type string, token not supported on (chain, network)
not_found Address override doesn't exist (only for explicit GET / DELETE — PUT auto-creates)
forbidden Address doesn't belong to your organization
internal_error Underlying service or price-feed failure — retryable