Near Learn

NEP-199: NFT royalties and payouts

NEP-199 NFT royalties and payouts on NEAR: nft_payout and nft_transfer_payout, the Payout map, max_len_payout, and how it compares to ERC-2981.

Advanced7 min read3-question check

NEP-199 lets an NFT contract tell a marketplace who should get paid, and how much, when a token sells. The NFT contract owns the royalty logic; the marketplace just asks for a payout split for a given sale price and then sends NEAR (or tokens) accordingly.

The answer is a Payout: a map from account ID to amount (as U128 strings). It typically contains the seller plus one or more royalty receivers (artist, collaborators, a DAO), and its amounts must add up to no more than the sale price.

Methods#

MethodKindWhat it doesDeposit
nft_payout(token_id, balance, max_len_payout?)viewFor a sale of balance, return { payout: { account: amount, ... } }. Must not exceed max_len_payout entries if given.—
nft_transfer_payout(receiver_id, token_id, approval_id?, memo?, balance, max_len_payout?)changeTransfer the token (like nft_transfer) and return the payout for this sale in one call — computed for the owner before the transfer.exactly 1 yoctoNEAR
ERC-2981 royaltyInfo vs NEP-199 payout
Solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

// ERC-2981: one receiver, the marketplace pays the seller the rest
interface IERC2981 {
    function royaltyInfo(uint256 tokenId, uint256 salePrice)
        external view
        returns (address receiver, uint256 royaltyAmount);
}
Rust
use std::collections::HashMap;
use near_contract_standards::non_fungible_token::core::NonFungibleTokenCore;
use near_contract_standards::non_fungible_token::TokenId;
use near_sdk::json_types::U128;
use near_sdk::{assert_one_yocto, near, require, AccountId};

#[near(serializers = [json])]
pub struct Payout {
    pub payout: HashMap<AccountId, U128>,
}

#[near]
impl Contract {
    // royalties: LookupMap<TokenId, HashMap<AccountId, u32>> in basis points
    pub fn nft_payout(&self, token_id: TokenId, balance: U128, max_len_payout: Option<u32>) -> Payout {
        let owner_id = self.tokens.nft_token(token_id.clone()).expect("no token").owner_id;
        let royalty = self.royalties.get(&token_id).cloned().unwrap_or_default();
        if let Some(max) = max_len_payout {
            require!(royalty.len() as u32 + 1 <= max, "payout too long");
        }
        let mut amounts: HashMap<AccountId, u128> = HashMap::new();
        let mut paid = 0u128;
        for (account, bps) in royalty {
            let amount = balance.0 * bps as u128 / 10_000;
            *amounts.entry(account).or_default() += amount;
            paid += amount;
        }
        // the seller gets the remainder, so the total never exceeds balance
        *amounts.entry(owner_id).or_default() += balance.0 - paid;
        Payout { payout: amounts.into_iter().map(|(a, v)| (a, U128(v))).collect() }
    }

    #[payable]
    pub fn nft_transfer_payout(
        &mut self,
        receiver_id: AccountId,
        token_id: TokenId,
        approval_id: Option<u64>,
        memo: Option<String>,
        balance: U128,
        max_len_payout: Option<u32>,
    ) -> Payout {
        assert_one_yocto();
        // compute first: the payout belongs to the CURRENT owner
        let payout = self.nft_payout(token_id.clone(), balance, max_len_payout);
        self.tokens.nft_transfer(receiver_id, token_id, approval_id, memo);
        payout
    }
}
Coming from EVM

ERC-2981 returns a single receiver and amount; the marketplace works out the seller’s share. NEP-199 returns the whole split, seller included, for any number of receivers.

Neither standard can force a marketplace to honour royalties — but on NEAR, nft_transfer_payout makes the transfer and the split one call, so marketplaces that use it get the split as part of the sale.

How a marketplace uses it#

  1. The buyer pays the marketplace (attached NEAR or an ft_transfer_call).
  2. The marketplace calls nft.nft_transfer_payout(receiver_id: buyer, token_id, approval_id: <stored from NEP-178>, balance: price, max_len_payout: 10) with 1 yoctoNEAR.
  3. In its callback it reads the returned Payout, checks it (sum ≤ price, length ≤ max), and sends each account its share.
  4. If the NFT call failed, the token did not move: refund the buyer. If the payout is malformed, a common choice is to pay the seller in full rather than lose the sale.

Check yourself

3 questions · progress saved in this browser

  1. 1.Why does nft_transfer_payout compute the payout before calling the transfer?
  2. 2.What constraint must a valid Payout satisfy?
  3. 3.How does NEP-199 differ most from ERC-2981?