Skip to main content
What you’ll be able to do
  • Create a MIP20 hold.
  • Make a payment from held funds.
  • Release any unused funds.
A MIP20 hold sets funds aside until an authorized spender pays a recipient or releases them. The holder - named owner in the hold API - approves that spender first. Held funds leave the holder’s spendable balance and cannot pay gas, while the spender controls capture and release. heldTotalOf(owner) reports the funds still attributed to that holder across all of its holds. The token records one aggregate hold for each holder and spender. The application records which orders share that hold and how much remains assigned to each. MIP20 supplies no order ID, automatic expiry or release initiated by the holder.

Configure the holder and spender

Use ownerWallet for the holder’s signing account and spenderWallet for the spender’s. Each pays gas for its own transactions. For a contract spender, its own application entry point must make the token calls below with appropriate access controls. Install @metalntwx/metal-ts@0.3.2 and viem@2.57.7 with pnpm. Resolve token and its decimals using Read metadata. Validate the order’s amount and convert it to base-unit reserved using Convert a payment amount. Read the holder’s spendable balance and retain enough outside the hold for gas costs.

Approve and place the hold

Use the holder’s wallet client, ownerWallet, to grant the spender an allowance of reserved using Approve a spender. If an allowance already exists, reconcile its intended use first. approve replaces the allowance, including a nonzero allowance. Coordinate with the spender so outstanding transactions cannot spend an old allowance while it is being replaced. The spender places the hold. Estimate and simulate this exact call using Estimate and record fees, with the spender’s signing account. Its balance in the gas token pays the transaction fee. Set gasLimit from that estimate and use the quoted price as maxFeePerGas.
Persist the signed transaction hash and bytes with the order’s hold placement record before await signed.send(). Use the same sign, save, submit and transaction receipt procedure for each capture and release, with a new transaction attempt and an estimate for its actual arguments. Require successful execution and canonical finality. The transaction receipt must contain HoldPlaced(owner, spender, reserved) and the token’s Transfer(owner, token, reserved). Read heldBalanceOf(owner, spender) and require heldBefore + reserved when no other transaction changed the hold concurrently. Verify that the finite allowance decreased by reserved and the holder’s spendable balance decreased accordingly. Account separately for gas if the holder also submits a transaction.

Allocate the held amount to orders

Store the chain ID, token, holder and spender with each allocation. Track reserved, captured and released amounts as base-unit integers, and retain each transaction attempt and verified event identity. The remaining allocation is reserved - captured - released. A capture that enters quarantine still consumes its allocation. Record delivery separately as paid or quarantined, with the quarantine receipt for quarantined funds. Leaving those funds available for another capture would spend the order’s allocation twice. Serialize changes to a shared aggregate hold and update allocations atomically with their verified results. The sum of outstanding allocations must never exceed the onchain hold. Refuse a capture larger than the order’s remaining allocation even when the aggregate hold has enough funds from other orders. Use the storage and concurrency procedure in Verify and reconcile payments.

Capture part of the hold

Set recipient from the immutable order and choose captured such that 0 < captured < reserved for a partial payment. The spender assigned to the hold calls captureHold, which releases that amount from escrow internally and then transfers it from the holder to the recipient.
Sign capture with spenderWallet, save its separate attempt, and submit it using the placement procedure. Verify these token events in the same successful, finalized transaction, in this order:
  1. Transfer(token, owner, captured) returns escrowed funds internally.
  2. HoldReleased(owner, spender, captured) records the hold reduction.
  3. Transfer(owner, recipient, captured) records the payment.
The hold decreases by captured. The holder’s spendable balance has no net increase from capture. Credit the recipient only for the transfer from the holder to the recipient, using the finalized transfer checks. If receive policy refuses the recipient credit, capture forces quarantine even when the recipient uses the default REVERT mode. The first two events still occur, followed by a ReceivePolicy Quarantined event and the token’s Transfer(owner, ReceivePolicy, captured). The hold and heldTotalOf(owner) decrease, but the intended recipient receives no tokens. Verify and save the quarantine receipt, including its token, originator, intended recipient (receiver), amount and recovery authority. Mark the allocation captured and quarantined, leave the order unpaid, and recover through that receipt. Do not repeat the capture to try to pay the same order.

Release the remainder

Only the spender assigned to the hold can release it. The holder sends a cancellation request to the application, which checks the order and instructs that spender. For this partial-payment flow, release reserved - captured.
Sign and persist a separate release attempt. Verify Transfer(token, owner, remainder), HoldReleased(owner, spender, remainder) and finality before closing the allocation. With no other holds, heldBalanceOf is now zero. For an existing aggregate hold, it returns to heldBefore. Revoke any unused allowance separately. Placement and release bypass receive screening. Release remains available even if the holder’s receive policy would refuse an ordinary credit. They emit transfers into and out of the token contract, which must not be credited as customer payments.

Check rejection and interruption

Before relying on holds, test excess capture and a different spender’s capture with simulation. Both must fail. A reverted capture preserves the hold, token balances and application allocations, including the internal release. With a controlled refusing receive policy, verify that capture instead succeeds into quarantine, reduces the hold and allocation, saves its quarantine receipt and leaves the order unpaid. These are different outcomes. Use fixtures for these checks when the live deployment lacks the required policy code, and report the live behavior as unverified. A lost response leaves the allocation pending. Recover using the saved transaction hash and inspect its transaction receipt before signing another placement, capture or release. A hold remains until the spender acts, so retain the spender’s signing and recovery access for the complete order lifecycle.