How Akash Handles Escrow Payments and Failure Scenarios

Akash Network uses a dual-object escrow system where Accounts hold user deposits and Payments stream funds to providers block-by-block, with automatic state transitions to Overdrawn when balances are insufficient and explicit error returns for all failure modes.

The x/escrow module in the akash-network/node repository implements a deterministic payment system that protects both deployment owners and providers. This module manages Akash escrow payments through two primary objects—Accounts and Payments—which track funds and state transitions across the lifecycle of a deployment.

Core Escrow Architecture

The escrow subsystem guarantees that providers are paid only when a deployment runs correctly and that users cannot lose funds if a provider fails to deliver. The logic lives in x/escrow/keeper/keeper.go and is driven by a keeper that manages two core objects persisted in the Cosmos SDK KV-store.

The Account Object

An Account holds the total funds a user deposits for a deployment or bid. It tracks the owner, deposited funds, and settlement state.

  • State machine: Open → Overdrawn → Closed
  • Key fields: Funds (available balance), Transferred (total moved in), SettledAt (last block height processed)

The Payment Object

A Payment tracks the rate-based payment stream for a specific provider. It links to an Account and accrues funds block-by-block based on a predefined rate.

  • State machine: Open → Overdrawn → Closed
  • Key fields: Rate (per-block payment amount), Balance (accrued but unwithdrawn funds), Owner (provider address)

The Escrow Lifecycle

Both Accounts and Payments are settled on every state-changing operation (deposit, withdraw, close). Settlement attempts to apply any accrued payment based on block height; if the account lacks sufficient balance, the state moves to Overdrawn.

Account Creation and Deposits

AccountCreate initializes an Account with zeroed transferred and settled balances, registers the owner, and stores the deposits. If the account ID already exists, it returns ErrAccountExists (see implementation around line 113 in keeper.go).

AccountDeposit moves funds from the depositor’s bank account to the escrow module, updates Funds, then triggers settlement. Attempting to deposit into a closed account returns ErrAccountClosed (guarded around line 43).

Payment Creation and Streaming

PaymentCreate validates that the parent account is not overdrawn and ensures the rate is non-zero. It stores a new Payment linked to the account. Key failure modes include:

  • ErrAccountOverdrawn – if the account lacks funds (checked around line 518)
  • ErrPaymentRateZero – if the rate is zero (checked around line 534)
  • ErrPaymentExists – if the payment ID is duplicate

Settlement and Withdrawal Logic

accountSettle computes how many full blocks have elapsed since the last settlement (SettledAt). It then:

  1. Calculates the owed amount (rate * heightDelta)
  2. Deducts the owed amount from the account’s Funds
  3. If funds go negative, the account (and all its payments) become Overdrawn and further payment creation is blocked

See the implementation around accountSettle (line 50) and accountSettleFullBlocks (line 82) in x/escrow/keeper/keeper.go.

PaymentWithdraw settles the account, picks the target payment, transfers any accrued balance to the provider, and updates state. It returns ErrPaymentClosed if the payment is not open (guarded around line 82).

PaymentClose ensures any remaining balance is withdrawn and forces the payment’s state to Closed. It returns ErrPaymentClosed if already closed (guarded around line 637).

AccountClose settles any outstanding payments, withdraws all payment balances, then marks the account Closed. It returns ErrAccountClosed if the account is already closed (guarded around line 300).

Failure Scenarios and Error Handling

When an account becomes Overdrawn, the system still permits payment withdrawals (to recover any accrued balance) but blocks new payment creation and account closure until the overdrawn condition is resolved—typically by the provider returning funds or manual admin intervention.

Scenario Why it happens Resulting state Error returned
Insufficient balance for a payment The escrow account cannot cover rate * heightDelta. Account → Overdrawn; all linked payments → Overdrawn. ErrAccountOverdrawn
Attempt to create a duplicate account AccountCreate finds an existing KV entry. No state change. ErrAccountExists
Closing an already closed account AccountClose sees StateClosed. No state change. ErrAccountClosed
Depositing into a closed account AccountDeposit checks StateClosed. No state change. ErrAccountClosed
Creating a payment with zero rate PaymentCreate validates rate.IsZero(). No state change. ErrPaymentRateZero
Duplicate payment creation PaymentCreate finds existing payment ID. No state change. ErrPaymentExists
Closing a payment that is already closed PaymentClose encounters StateClosed. No state change. ErrPaymentClosed
Withdrawing from a closed payment PaymentWithdraw checks StateOpen. No state change. ErrPaymentClosed

Practical Implementation Examples

The following snippets illustrate the typical workflow using the keeper (the handler layer wraps these calls for gRPC/REST endpoints).

1. Create an escrow account for a bid

// ctx – sdk.Context, keeper – escrow.Keeper, owner – sdk.AccAddress
id := escrowid.Account{
    Scope:  escrowid.ScopeBid,
    Owner:  owner.Bytes(),
    DSeq:   dseq,
    GSeq:   gseq,
    OSeq:   oseq,
}
deposits := []etypes.Depositor{
    {
        Owner:   owner.String(),
        Height:  ctx.BlockHeight(),
        Source:  deposit.SourceBalance,
        Balance: sdk.NewDecCoinFromCoin(sdk.NewCoin("uakt", sdk.NewInt(1_000_000))),
    },
}
err := keeper.AccountCreate(ctx, id, owner, deposits)

Creates a fresh account; fails with ErrAccountExists if the same id already exists.
Source: x/escrow/keeper/keeper.goAccountCreate

2. Deposit additional funds

more := []etypes.Depositor{{
    Owner:   owner.String(),
    Height:  ctx.BlockHeight(),
    Source:  deposit.SourceBalance,
    Balance: sdk.NewDecCoinFromCoin(sdk.NewCoin("uakt", sdk.NewInt(500_000))),
}}
err = keeper.AccountDeposit(ctx, id, more)

Moves tokens from the user’s bank account into escrow; may trigger settlement and cause an overdrawn state.
Source: x/escrow/keeper/keeper.goAccountDeposit

3. Create a payment stream for a provider

payID := escrowid.Payment{
    Account: id,
    Provider: provAddr.Bytes(),
}
rate := sdk.NewDecCoinFromCoin(sdk.NewCoin("uakt", sdk.NewInt(10))) // 10 uakt per block
err = keeper.PaymentCreate(ctx, payID, provAddr, rate)

Registers a payment; will error with ErrAccountOverdrawn or ErrPaymentRateZero as appropriate.
Source: x/escrow/keeper/keeper.goPaymentCreate

4. Withdraw accrued payment (usually called each block)

err = keeper.PaymentWithdraw(ctx, payID)

Calculates owed amount, transfers it to the provider, and updates payment state.
Source: x/escrow/keeper/keeper.goPaymentWithdraw

5. Close a payment (e.g., when lease ends)

err = keeper.PaymentClose(ctx, payID)

Ensures any remaining balance is withdrawn and marks the payment Closed.
Source: x/escrow/keeper/keeper.goPaymentClose

6. Close the escrow account (when deployment is fully terminated)

err = keeper.AccountClose(ctx, id)

Settles all payments, withdraws any leftover funds, and finally puts the account in Closed state.
Source: x/escrow/keeper/keeper.goAccountClose

Key Source Files

File Role Direct link
x/escrow/keeper/keeper.go Core escrow logic – account/payment lifecycle, settlement, error handling. keeper.go
x/escrow/module.go Module wiring – registers handlers, querier, and genesis logic. module.go
x/escrow/genesis.go Genesis validation & initialization of escrow state. genesis.go
x/market/handler/keepers.go Higher‑level market module that calls escrow keeper for bids/deployments. market/keepers.go
x/market/handler/handler_test.go Comprehensive tests demonstrating overdrawn, settlement, and failure scenarios. handler_test.go

Summary

  • Dual-object model: Accounts hold user deposits while Payments stream funds to providers based on per-block rates.
  • Deterministic settlement: The accountSettle function calculates owed amounts using block height deltas (rate * heightDelta), automatically transitioning states to Overdrawn when funds are insufficient.
  • Explicit failure modes: Every error condition—duplicate accounts, closed states, zero rates, insufficient balances—returns a specific typed error (e.g., ErrAccountOverdrawn, ErrPaymentRateZero) rather than panicking.
  • Recovery paths: Overdrawn accounts block new payment creation but allow withdrawals, ensuring providers can recover accrued funds while protecting users from further charges.

Frequently Asked Questions

What happens when an Akash escrow account runs out of funds?

When an account's balance cannot cover the calculated settlement amount (rate * blocks_elapsed), the accountSettle function transitions the account state to Overdrawn and propagates this state to all linked payments. While new payment creation is blocked with ErrAccountOverdrawn, providers can still withdraw any previously accrued balances.

Can a provider withdraw funds if the deployment is still active?

Yes. The PaymentWithdraw method can be called at any time while the payment is in StateOpen. It triggers settlement for the account, calculates the accrued amount based on blocks elapsed since the last settlement, and transfers those funds to the provider's wallet. This is typically invoked periodically (e.g., every block) by the market module.

What is the difference between an Account and a Payment in Akash escrow?

An Account represents the total escrowed funds for a specific deployment or bid scope, tracking the owner's deposited balance and overall state. A Payment represents a specific rate-based payment stream from that account to a single provider. While the Account holds the aggregate funds, Payments calculate per-block accrual rates and manage the actual transfer of value to providers.

How does Akash prevent duplicate escrow entries?

The keeper methods AccountCreate and PaymentCreate both perform existence checks against the KV-store before initialization. AccountCreate returns ErrAccountExists if the composite ID (scope, owner, deployment sequence) is already present, while PaymentCreate returns ErrPaymentExists if a payment for that provider and account combination already exists.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →