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#
| Method | Kind | What it does | Deposit |
|---|---|---|---|
nft_payout(token_id, balance, max_len_payout?) | view | For 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?) | change | Transfer 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 |
// 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);
}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
}
}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#
- The buyer pays the marketplace (attached NEAR or an
ft_transfer_call). - 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. - In its callback it reads the returned
Payout, checks it (sum ≤ price, length ≤ max), and sends each account its share. - 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