NIP: 2
Layer: Applications
Title: Payment URI Scheme
Authors: codewordneptune
Status: Draft
Discussion: Neptune-Crypto/neptune-core#959 (RFC), Neptune-Crypto/nips#2 (this proposal's pull request).
There is currently no documented standard for expressing "pay this address this amount" as a link or QR code. Every wallet and site that wants payments must invent its own format, and none of them would interoperate. This NIP (Neptune Improvement Proposal), Neptune Cash's equivalent of Bitcoin's BIP 21, unblocks several things at once:
- QR-based receive/send flows in wallets
- Click-to-pay links on explorers, donation pages, and merchant sites
- Deep-linking (
neptunecash:opens the wallet with a pre-filled send screen)
The two existing wallets already use an undocumented NPT: prefix in QR codes, but only for bare addresses, with no additional metadata:
- neptune-wallet-app generates receive QR payloads as
NPT:<ADDRESS>(uppercased). - npt-mobile-wallet displays the same format on the receive screen, and its scanner strips a leading
npt:case-insensitively and treats the remainder as the address.
No amount, label, or other parameters exist today, and nothing registers an OS-level URI handler. This proposal documents that practice and defines its successor, without breaking anything already deployed: existing address-only NPT: payloads keep working in the wallets that already accept them, and parameterized URIs are not accepted by the legacy NPT: scanners described above: the query string makes the scanner's address decoding fail, producing an invalid-address error rather than a payment with wrong details.
The key words MUST, MUST NOT, SHOULD, SHOULD NOT, and MAY in this document are to be interpreted as described in RFC 2119 and RFC 8174 when, and only when, they appear in all capitals.
neptunecash:<address>[?amount=<amount>][&label=<label>][&message=<message>]
- The scheme is
neptunecash:, matching the project's full name; see the rationale section for the naming discussion and its resolution. The address-onlyNPT:payload described under existing practice is a wallet-specific convention that was never standardized; wallets MAY keep accepting it for compatibility with deployed QR codes, but this spec does not require it. Migration: wallets that scan QR payloads SHOULD accept both this scheme and the legacy address-only payload. Wallets that generate address-only receive QR codes SHOULD keep emitting the legacy payload while they target scanners that predate this NIP (a deployed scanner that only stripsnpt:cannot read aneptunecash:payload), switching to this scheme once the wallets expected to scan their codes accept it; parameterized URIs have no legacy form and always use this scheme. - Each query parameter is optional and may appear in any order; the query string, if present, is introduced by
?and parameters are separated by&, as in ordinary URIs. A URI ending in a bare?with no parameters is equivalent to one with no query string. A URI containing a fragment (#) MUST be rejected: nothing in this scheme uses fragments, and rejecting is safer than silently parsing a different string than the user supplied. Parsers SHOULD trim surrounding whitespace before parsing, since QR scanners and chat clients sometimes introduce it. - A parameter name appearing more than once MUST cause the entire URI to be rejected. First-wins and last-wins parsers disagree silently, and a disagreement about which
amountcounts is a payment bug. This applies to every parameter name, known or unknown: duplicate detection runs over the raw component names before unknown parameters are discarded. <address>is a bech32m receiving address as produced by neptune-core, appearing literally, never percent-encoded. The network is already encoded in the address's human-readable part (HRP), the prefix before the1separator, so the URI needs no separate network field; wallets MUST reject a URI whose address network does not match the active network.- neptune-core currently defines four receiving-address types. Their disclosure properties differ sharply, and not all of them belong in a payment URI:
| Type | Mainnet HRP | Length | Status in this spec |
|---|---|---|---|
| Generation | nolgam |
3482 chars | Allowed. The only type safe to publish or to reuse across payers. |
| EcHybrid | nechm |
168 chars | Allowed for a request to a single counterparty; see security considerations. |
| ViewingAddress | nviewm |
116 chars | Allowed for a request to a single counterparty; see security considerations. |
| Symmetric | nsymkm |
n/a | MUST NOT appear in a URI; see below. |
- Each HRP is a type base (
nolga,nech,nview,nsymk) followed by a single network character:mmainnet,ttestnet,ztestnet-mock,rregtest, per neptune-core'snetwork_hrp_char. The table lists the mainnet forms. Rejection classification runs in this order: an HRP with basensymkMUST be rejected under the symmetric-address rule regardless of its network character; a known base whose network character does not match the active network MUST be rejected as a network mismatch; an HRP matching no known base MUST be rejected as an unknown address type. - Symmetric-key addresses MUST NOT be used in a payment URI. A raw symmetric-key bech32m encoding contains the secret key itself, so exposing it grants spending authority to anyone who obtains it; and because neptune-core uses the same
nsymkHRP family for digest-only display strings (SymmetricKey::to_display_bech32m), wallets MUST reject the entire family rather than attempt to distinguish secret-bearing from display-only forms. Wallets MUST NOT generate such a URI and MUST reject one on parse, before any payment UI is shown, with an error distinct from a generic invalid-address error; the error SHOULD say that the URI may contain a secret key, not merely that the address type is unsupported. - The receiving-address enums in neptune-core are marked non-exhaustive, so further types will appear. An address type not listed in the table above MUST be rejected; this spec is to be amended to classify each new type's disclosure properties before wallets accept it, keeping the failure mode closed rather than open.
- Scheme matching is case-insensitive. The address may be all-uppercase or all-lowercase (an all-uppercase scheme and address let the scheme and address portion of the URI use QR alphanumeric mode; see the QR code guidance section); mixed case is invalid per bech32m. Implementation note: neptune-core's generic
ReceivingAddress::from_bech32mselects the type-specific decoder with a case-sensitive lowercase prefix match and therefore rejects an all-uppercase address; parsers that delegate to it must lowercase the address first (the type-specific decoders themselves accept uppercase). - Parameter names are lowercase and case-sensitive; the scheme and the address may each appear in uppercase, but parameter names MUST NOT be uppercased (values legitimately contain uppercase, as in
label=Dev%20Fund, so this rule is about names, not the query string wholesale). QR alphanumeric mode has no?,=, or&in its character set, so uppercasing parameter names gains no QR benefit, and it changes their semantics:amountwould become the unknown parameterAMOUNTand be silently ignored. (Percent-encoded triplets survive uppercasing, since their hex digits are case-insensitive per RFC 3986.) amountis optional: a decimal string in whole NPT units (e.g.amount=1.25). The grammar is deliberately stricter than neptune-core's owncoins_from_str, which also accepts negative, fraction-only, trailing-dot, and even empty forms, and which silently rounds input finer than 1 nau instead of rejecting it, so a URI parser MUST NOT delegate to it unvalidated:- digits with at most one
.as decimal separator; at least one digit before the separator and, when the separator is present, at least one digit after it; no leading zeros (the integer part is0or begins with a nonzero digit); no sign, no exponent notation, no grouping separators, no whitespace; an empty value is invalid. The leading-zero rule removes padding spellings and bounds the amount's length at 39 characters. Trailing zeros in the fraction remain valid (10.00parses as 10 NPT), since fixed-decimal formatting is common; wallets comparing amounts must therefore compare numeric values, not strings - at most 30 decimal places: every conforming amount converts to a whole number of nau exactly, with no rounding. The grammar's step is 10^-30 NPT, which is 4 nau (1 NPT is 4 * 10^30 nau, and 1 nau is 2.5 * 10^-31 NPT); on-chain amounts whose nau count is not a multiple of 4, which neptune-core can represent, are deliberately not expressible in a URI
- greater than zero and at most 42000000, the maximum supply
- implementations MUST convert the amount to an integer number of nau using exact decimal arithmetic; binary floating point MUST NOT be used at any stage, since even 1 NPT in nau is far beyond the 2^53 exact-integer range of an IEEE 754 double, which silently rewrites conforming amounts (see the rationale)
- digits with at most one
labelis optional: a human-readable name supplied by the URI creator; it is unauthenticated. It is intended for display in the payer's wallet and is not sent to the recipient. The value is percent-encoded UTF-8 and MUST be at most 255 bytes after percent-decoding (a longer value makes the entire URI invalid); an empty value is treated as absent.messageis optional: a payment description supplied by the URI creator; it is unauthenticated. It is intended for display to the payer and is not sent to the recipient. The value is percent-encoded UTF-8 and MUST be at most 255 bytes after percent-decoding (a longer value makes the entire URI invalid); an empty value is treated as absent.- Both
labelandmessageare unauthenticated metadata supplied by the URI creator. Neither is transmitted on-chain nor sent to the recipient in any form. Wallets MUST NOT treat either field as authenticated information about the recipient or payment. - Unknown query parameters MUST be ignored (forward compatibility). The exception is the reserved prefix
req-(for "required"): a parameter prefixedreq-that the wallet does not understand MUST cause the wallet to treat the entire URI as invalid, same convention as BIP 21. This gives future extensions a way to mark parameters that are unsafe to ignore, such as a hypotheticalreq-expires. - The security considerations section is normative and imposes further requirements on wallets: user confirmation before any send, inert rendering of
labelandmessage, a generation-time warning when a reuse-sensitive address type is placed in a URI, and a bar on presenting such types as public or reusable requests.
Parsing proceeds in this order, and percent-decoding happens exactly once, as the final step, on parameter values only:
- Trim surrounding whitespace and match the scheme case-insensitively; a URI containing a
#is invalid. - Split the remainder into address and query string at the first
?. The address begins immediately after the scheme's colon; a URI with//after the colon is invalid, since these URIs have no authority component, and composers MUST NOT emit that form. Generic URL libraries parse//as an authority and case-normalize it, silently accepting and rewriting the invalid form, so parsers SHOULD apply the rules here rather than a generic URL parser. - An empty query string (a bare trailing
?) is valid and equivalent to no query string. Otherwise, split the query string into components on&, then split each component into name and value at its first=; literal=characters after the first belong to the value. An empty component (from a leading&, a trailing&, or&&), a component with no=, or an empty name makes the URI invalid. - Match parameter names byte-for-byte against the literal, undecoded text; a
%in a parameter name makes the URI invalid, so%72eq-can never decode intoreq-and evade the required-parameter rule. After duplicate detection, each name is classified as a known parameter, an unknown ordinary parameter, or an unknownreq-parameter; unknown ordinary parameters are ignored, while an unknownreq-parameter invalidates the URI. - Validate the
amountvalue against its grammar literally, with no percent-decoding; a%in the amount makes the URI invalid. - Percent-decode
labelandmessagevalues exactly once. A+is a literal plus sign, never a space: this is RFC 3986 percent-encoding, not form encoding, and composers MUST NOT use form serialization, which encodes spaces as+. Every%must be followed by exactly two hexadecimal digits, and the decoded bytes must be valid UTF-8; a malformed escape or invalid UTF-8 makes the URI invalid. A decoded value MUST NOT be decoded again.
Example:
neptunecash:nolgam1abc...xyz?amount=10&label=Dev%20Fund&message=August%20pledge
These vectors are hand-derived; they will be re-verified against the reference parser implementation, and extended, before this NIP leaves Draft status. In the vectors, <ADDR> stands for the following known-valid mainnet generation address, taken from neptune-core's test suite:
nolgam1cvqtx45kkfqhkmzt74ec98laulywqevxyyqhhchs5v5g07t76uqjnp4la3htxa5jev004wsmtt0w0ktxc08qz03ztpx80t652qgvh44k7s3esxg6flkdnhmljlx9vwz4zt3awn2lj0y3pga5yj7rusp8mtp7alkzkt0fvufxwmqwqyxzg5z8d54a8zj7l0az237yc6wjxr6z04d2skdql82p5qaks7crqwafgu7nm3yjkwjnetrhlqp8u0y22vgavdltgdhc6xql0q7krh6f8rn9snuyr5whufy5pk2ja5mhhkal6v2mt0f4wj9y27fhep60cxgv4gnlyu9j36yava459dr9zzm9ctq32juar4dmpsgt98sq0twnzhvfkmyfvvjtjtdfm9t9yfkexyf5und9xfj3ll9qxy4qdvzz3mft0kj5kw0cggacy7p4f0zuag3pxexh20378k2gr0kt7nkmchaum8ud80a4t697hfj67p6mamu00d8g2erx4fhuu9xfuzv8sjgwqzkfjdkaayrfqy5kvrkmpf27z2r5pyd783ghwzgqk8z96ek0xjxxawem849nn24r80u42m3s63y6zqlsfu3jyy3ddq2rzjn0pj7fyt3f7k2smum9mzhwzw03vfh8lcetrcgtdjc83ctcre4ajmhlkt8s3r2wgnf43d4lndkml20yu7z3xua3ev3nudtq0mt0mx4rvakml08qy8nln08t6dew6v9q46xp94e3s83sf6as8v5w27uvyqvckymqg7mpuhht2gmxrug0tj4uz6hsymatwhaq0m25p4hkptfft4jcgly9l26ufcu23f3knj53u5tfv3gkm3rdzruhjra0k9d6g3xhmvpsu3n780y65m5vx22a3pfyvzenv0zf8te55f4gmhhtqwy57zhazsd3wqkcf0qrd7vpndeprssgu4hc9373ytapkersgufemdvselrkf7fdl3xh7sx4ph42pjxcgtx6vyqqjedeqgtsfev57eefpc9uje6lkvwnzxh5dvmwk8w69ynkmnza0qppnkgzd8m7fnthxrml20v2uce8hl655tfppsgfgnyc80q2mnpmx6f5jkvlmyhdsvd56ywzahr68qs37s8edegwad9u90fw44774yshp9k8kyj9am8hmuqczg0capa38gh3jupsgu3heaxkrlfhwcghl3k8s72dg9ljf76c6f79v5x4jpykvacdkhc7wp02g5je6su8nyqr7zef9z9sqlwwazc8u3cu8s2usum77qmj47eh8w424sued8x8lc7mxj398f6gt8yph0zgrslserjdpmvf877sum79k0nq39haw904eruls6ed0zkpkmw7twj7p67wz6ewhc9nu6dnackmjsugmxsphvqgxwll54fr5v4cy23xsyadtnd5u5p62qwyqz6sv8clgjcyhaekf4sul8zlxmc4p7h0uf75s9929343wn769z7vy0vaj4sfq4544s9zmnxxlyuquxamq94qnwl5nvsqxumfwl3jdef8lxfgreacsv78k89vqfllx7sknx5lr3jj4upynjf5cxlmjdcn8e6rlryrc8ecju70dg98tsuj0h7reqt0j7uagdqk2sqe77eftlrwqmap9ceuwe60n7akkntc2r5pttshv9mpsykhy44mam6k09azgma3m78ktkqr8zm35l5z9vhn0mczfu9sfpsztdehl2wlt4h3dy0lrjvrqf6r69xe05r9ut8x9n87fuvy7ny6crpfdvzfxekzye3pefhr4k8v6th2hjkz3ekxy6r3h4dvzqq7hns0qz0xr5w27xrejvjgaw08m9pmrlzns9uk4tt9ykus34mf6nxfdvgw5pyklnvzj3xnzjessely3u3v6gp2ncfmct47yu507pc5r7uear28llzz8krd8e0zcrlmhvjj69q75ttm9kn6emkqxt2acux63ezjc66hs6pnynrffgffdl6hdns0e0nkz58t5j7h6wltje7hfvzwzxzmshsxvpsgj3flzlptzfwv34udx5g3g4kpcpu04p85xvdq0m0qmkmpg88uv6jsts6u4m2al45gr2tzdg6fuaedwlvhkf9neav0r264eadtvgnk8ahgshfaluqwqz604s73hzp9qj8en397h3lhl6d5a2xz5w2jujak2l62p35c55h07plx4jxz5dwwed6v8ym682dl00eqml7zxewma6dhhl9ufp0rw83ckuw9qft92y7l7t4fqzfumglhcqpkrgl0vjpzkzhda734mauxv6lr2glcjee0xp96ls3rnsl0l5e2qexx4aj2rhllezphmqzka047pafa6l7k2lr75xgyc4p84dvskldmcljm67jgjw4nxawnqternspgph4793fthad5yp3zvt5vtcmcr36sygjkw89zu0vewcnu9gfx3u7wpwvwz585d0dl5ycg0dq4l2z74cyznn5cmz43qj8745duqgrpmx23vhjzhmzmck8mq37h65duxx547z6ml52mmtnkc0x2hh2tm83t6s37l0aau8v2kw88v4gx3jfmez2re626lgydhz5je47zh8rlc74qxk83pr87nwcvhtlh2jjn8ksv38hc8g5sh4gtx9emj7q8kzsgjqvsn0l8rnhzx0ztseqr3usuztva7ejkj7degxgyur2w92n4m7mudufslz9d5puuzd6yzk3g4rjyphr6sun0v28qffumdupx737udpeextntdnkrl8u8h7kgntwj7eh8ynvwqcuer5xne36nmhmr3awv7kyfc7xh9g4sl4xj9zg5fzmehjda6d9zwcnkwfhcar2gh84jmew47e69uak2s329x6mg7sn23dkswyla7xjfn5r23a7uj7r3r5jt4djg8m7en5p6vh290jajs5fpm4hkfza4uwp29nqtn9wf05hlvgjnx9fjh3pzlhd97s3ge3xczchl306ayqunlu2kyasklt8lu0mt6dy734wdtgxpu2s5jw3m8jrkm5epz6s9mf03z6xsf4efaahzamxpdvvx68ykqjpeu3y0mps2sj4z86ucxd2tnwtgks00ep5hmrnvcjwmfqkgdfp3x7x3zx9r3tc2fz70mzpal64hd5ewalxvkxmv8uz6rt48vjqfghfe49d5derqdjege6g477esdkgnmpmpj5uhx9vxcm48lqqpucnn7d2xap00twgje3927m80jnuvagw2mfa4j9vcsl9mauzjn7ps9naa5kkg6d2l4yv4f0f6u88er0st25sq65p4zppukfrjknl8nkr4ujgrgk8tulatqh93ydd7hexrlsh9zje0eskp8656zwx25fs53ee6atrknffj95a85mfwtdsd5djafr3rs2c4wvurfeslj6s8869wvlj0yw7a730npk2dhgwgmq4p9ag0eugcuu5ucsycul295xcwprr
<ADDR-UPPER> stands for the same address converted to all-uppercase.
<ADDR-ECH> stands for the following known-valid mainnet EcHybrid address (168 characters), also from neptune-core's test suite:
nechm1qtmcrts70kl4hzjej5te74pk67aja8wmz6aawx995c0cnzl2sxuxdv37p70sgxl7z4dqnmrvqcy2lgsy3xu8gc6xpgaps8x6ppxulpzqj8vqnm5p3n4cyjrzpr2pjr6zwn49x0ejs2p3vyausep5pcftn2dq200dz7
<ADDR-VIEW> stands for the following known-valid mainnet viewing address (116 characters), likewise from neptune-core's test suite:
nviewm10243hqsugd3nvmck0f3f49y75dku9lq4ym37lxx6vzl0p2l4kvthjcyzkkpy6f9v9gh8w6hxhaa4fexl4zhnh2qq7ulyrdmttjwukzsn87z88
Valid, a conforming wallet accepts these:
neptunecash:<ADDR>parses as the bare address, no parameters.neptunecash:<ADDR>?parses identically; a bare trailing?is equivalent to no query string.neptunecash:<ADDR>?amount=1.25parses with an amount of 1.25 NPT.NEPTUNECASH:<ADDR>?amount=10&label=Dev%20Fundparses with an amount of 10 NPT and the labelDev Fund; scheme matching is case-insensitive.neptunecash:<ADDR-UPPER>?amount=1.25parses; an all-uppercase address is accepted.neptunecash:<ADDR-ECH>?amount=1.25&label=Caf%C3%A9parses; EcHybrid addresses are accepted, and the label percent-decodes toCafé.neptunecash:<ADDR-VIEW>parses as a bare viewing address.neptunecash:<ADDR>?futuredata=abc&amount=2parses with an amount of 2 NPT; the unknown parameterfuturedatais ignored, and parameter order does not matter.neptunecash:<ADDR>?AMOUNT=10parses as the bare address with no amount: parameter names are case-sensitive, soAMOUNTis an unknown parameter and is ignored.neptunecash:<ADDR>?amount=10&Amount=20parses with an amount of 10 NPT:Amountis a different raw name fromamount, so it is neither a duplicate nor recognized, and is ignored.neptunecash:<ADDR>?label=%2520parses with the label%20, not a space: values are percent-decoded exactly once, never twice.neptunecash:<ADDR>?amount=42000000parses; the maximum supply is the largest permitted amount.neptunecash:<ADDR>?amount=0.000000000000000000000000000001parses; 30 decimal places is the permitted maximum, and this amount is exactly 4 nau.neptunecash:<ADDR>?label=a+bparses with the labela+b, nota b; a plus is a literal plus.neptunecash:<ADDR>?label=a%26amount%3D9parses with the labela&amount=9and no amount: values are decoded only after the query string has been split.
Invalid, a conforming wallet rejects these:
neptunecash:<ADDR>?req-expires=2027-01-01, because the wallet does not recognize thereq-prefixed parameter.neptunecash:<ADDR>?amount=1&amount=2, because a duplicated parameter name is rejected outright rather than resolved first-wins or last-wins.neptunecash:<ADDR>?amount=, becauseamountmay not be empty.neptunecash:<ADDR>?amount=1,25, because the decimal separator must be..neptunecash:<ADDR>?amount=1e3, because exponent notation is not allowed.neptunecash:<ADDR>?amount=.5, because at least one digit must precede the decimal separator.neptunecash:<ADDR>?amount=-1, because signed amounts are not allowed, even though neptune-core's own amount parser accepts them.neptunecash:<ADDR>?amount=0, because the amount must be greater than zero.neptunecash:<ADDR>?amount=42000001, because the amount exceeds the maximum supply.neptunecash:<ADDR>?amount=0.0000000000000000000000000000001, because 31 decimal places is finer than the grammar's step.neptunecash:<ADDR>?amount=1., because a decimal separator must be followed by at least one digit.neptunecash:<ADDR>?amount=007, because leading zeros are not allowed: the rule removes padding spellings and bounds the amount's length.neptunecash:<ADDR>?amount=1%2E25, because the amount is validated literally and may not contain percent-encoding.neptunecash:<ADDR>?futuredata=1&futuredata=2, because the duplicate rule covers unknown parameter names too.neptunecash:<ADDR>?%61mount=2, because a%in a parameter name is invalid: names are matched literally and never percent-decoded, so%61mountcan neither becomeamountnor slip past as ignorable.neptunecash:<ADDR>#donate, because fragments are not part of this scheme and are rejected rather than stripped.neptunecash://<ADDR>, because these URIs have no authority component;//after the colon is invalid.neptunecash:<ADDR>?amount, because every component must contain an=, andneptunecash:<ADDR>?&amount=1, because an empty component is invalid.neptunecash:<ADDR>?label=%GG,neptunecash:<ADDR>?label=abc%2, andneptunecash:<ADDR>?label=%FF, because every%must be followed by exactly two hexadecimal digits and the decoded bytes must be valid UTF-8.- A URI whose
labelormessagepercent-decodes to more than 255 bytes. neptunecash:<ADDR>, with the final character changed fromrtoqso the address ends...ucsycul295xcwprq, because the bech32m checksum fails.neptunecash:<ADDR>, with the first character of the address uppercased (Nolgam1...), because mixed case is invalid per bech32m.- The first valid vector, presented to a wallet whose active network is not mainnet, because the address HRP
nolgamdoes not match the active network. neptunecash:with a testnet generation address (HRPnolgat) presented to a mainnet wallet is rejected as a network mismatch, not as an unknown address type; the classification order is specified above.- A URI whose address is a symmetric-key address (HRP
nsymkm), because that address type is banned. The concrete vector will be generated from a throwaway seed alongside the reference parser, since even an example symmetric address is a spending key. - A URI carrying a syntactically valid bech32m string whose HRP is outside the table above, for example an address of the XNT fork (
xntnwplus a network character), because unknown address types are rejected.
Two independent limits govern QR encoding of these URIs: symbol capacity and scannability.
Capacity. The QR alphanumeric character set contains : but not ?, =, or &, so the uppercase scheme and all-uppercase address portion can be encoded using QR alphanumeric mode while any query string cannot.
- An address-only generation payload (
NEPTUNECASH:plus the uppercased 3482-character address, 3494 characters in total) requires error correction level L: no level M symbol of any version can hold it, since alphanumeric capacity at level M peaks at 3391 characters. At level L it fits from version 36 (capacity 3537 characters, 161x161 modules) through version 40 (capacity 4296 characters, 177x177 modules). - A parameterized generation URI still fits at version 40 level L, but only with mixed-mode segmentation: an alphanumeric segment for the scheme and uppercased address (19,234 of the 23,648 available data bits) and a byte-mode segment for the query string, leaving at most 549 bytes for parameters. A single-segment encoder, one that does not perform explicit mixed-mode segmentation, drops the whole string to byte mode the moment it sees
?and fails, because version 40 byte capacity at level L is 2953 bytes. This is the most likely implementation mistake: an encoder has to segment explicitly or fall back to offering the URI as a copyable link. - EcHybrid (168 characters) and viewing (116 characters) addresses never need segmentation: even the worst case the grammar permits (an amount at its maximum length of 39 characters, 8 integer digits plus the separator plus 30 decimal places, and
labelandmessageeach at 255 bytes, fully percent-encoded to 765 characters) is a 1773-byte URI. Version 31 at level L (141x141 modules) is the first symbol whose single-segment byte-mode capacity holds it: version 30 at level L holds 1732 bytes, version 31 holds 1840. For a concrete point of reference, the EcHybrid test vector above,neptunecash:<ADDR-ECH>?amount=1.25&label=Caf%C3%A9(208 bytes), fits a version 9 symbol at level L (53x53 modules).
Scannability. A version 36 to 40 symbol is 161x161 to 177x177 modules and demands both physical size and camera quality. Wallets SHOULD prefer EcHybrid or viewing addresses for QR codes when the request goes to a single counterparty (subject to the reuse rules under security considerations), SHOULD render generation-address QR codes large, and MAY offer a copyable link instead of a QR code for parameterized generation URIs.
For a published or reusable request, a generation address is the appropriate type, since wallets MUST NOT present the other allowed types as public or reusable requests (see security considerations); the choice there is between a large dense QR code and a plain link.
Wallets that support opening links or deep links SHOULD register as the OS-level handler for the neptunecash: scheme, if no other handler is already registered, so that such links open a pre-filled send screen:
- Windows: a registry key
HKEY_CLASSES_ROOT\neptunecashwith an emptyURL Protocolvalue and ashell\open\commandentry. - macOS:
CFBundleURLTypesin the app bundle'sInfo.plist. - Linux: a
.desktopfile declaringMimeType=x-scheme-handler/neptunecash;. - Android: an
<intent-filter>with<data android:scheme="neptunecash" />. - iOS:
CFBundleURLSchemesinInfo.plist.
No allowlist exists at the OS level; any scheme string can be registered, which is how bitcoin: and monero: links work today. Two browser caveats do apply:
- Browsers gate navigation to unknown schemes behind user activation and usually a confirmation prompt, and block it entirely from some contexts such as sandboxed iframes. This affects every cryptocurrency scheme equally.
- The JavaScript API
navigator.registerProtocolHandler, which lets a website rather than a native app claim a scheme, is restricted to a safelist plus schemes prefixedweb+. The safelist includesbitcoinbut notmoneroorzcash. A browser-hosted Neptune Cash wallet would therefore have to useweb+neptunecash:, or the scheme would need to be added to the safelist by a spec change, asbitcoinwas. This NIP notes the constraint but does not reserveweb+neptunecash:; no browser-hosted wallet exists today.
Scheme registration itself is unauthenticated: another installed application, including one impersonating a wallet, can claim neptunecash: and receive the URIs the user opens. Nothing in this spec therefore treats delivery of a URI as trusted, and this is a second reason a URI must never carry a secret (see the symmetric-address rule). A wallet that finds another application already registered SHOULD alert the user once rather than silently seizing the registration, the same etiquette Stellar's SEP-0007 specifies for this threat.
A payment URI is untrusted input: it arrives from web pages, chat messages, and scanned QR codes, none of which the wallet controls.
- Confirmation. The wallet MUST display the decoded address and, if present, the amount, and require explicit user confirmation before signing or broadcasting a transaction. Opening a URI MUST NOT by itself authorize or send a payment, however the URI arrived.
- Untrusted text.
labelandmessageare attacker-chosen strings and MUST be rendered inert: no markup interpretation, control characters stripped or escaped, and bidirectional override characters such as U+202E neutralized (these are Unicode format characters rather than controls, and can visually reverse displayed text), and truncation made visible rather than silent. Homoglyphs makelabelunsuitable as the sole identifier of a payee; the address is the identity, the label is a hint. Wallets MUST NOT treatlabelormessageas authenticated information about the recipient, and MUST NOT use either parameter to determine the destination address. - Network mismatch. As specified above, an address whose HRP does not match the active network MUST be rejected as a network mismatch. Addresses of the XNT fork are rejected as unknown address types, since their HRP family is not defined by this NIP.
- The symmetric address family. A raw symmetric-key encoding is the secret key; a digest-only display string shares the same HRP, which is why the whole family is rejected rather than told apart. Beyond the MUST NOT in the specification, implementers should know why this is easy to get wrong: neptune-core's generic
ReceivingAddress::to_bech32mreturns the raw key material for the symmetric variant, and its own documentation warns that it must never be used for display. A wallet that builds URIs by calling the generic encoder over an arbitrary receiving address will leak a spending key the moment a symmetric address reaches it; match on the address type explicitly. On the parsing side, a URI carrying annsymk-family address MUST be rejected before it is displayed or used for payment, and once the family is recognized the wallet MUST NOT intentionally persist or log the raw URI or address, including in wallet-controlled clipboard or history features, telemetry, crash reports, or diagnostic output. - Reuse-sensitive address types. Wallets MUST NOT present an EcHybrid or viewing address as a public or reusable payment request; these types MAY be used for a request intended for a single counterparty. Anyone who knows a viewing address can read that address's entire on-chain transaction history (when on-chain announcements are used), and an EcHybrid address gives the same power to an attacker with a quantum computer. No parser can detect wider sharing after the fact, since the hazard lies in how widely the string is shared, not in the string itself; the enforceable levers sit with the wallet that creates the URI, which MUST warn the user at generation time that the address is intended for a single payer.
The wallets' existing QR payloads use NPT:, and extending that payload was an option, but a fully spelled-out scheme name is preferable: other cryptocurrencies consistently use their project name (bitcoin:, zcash:, monero:), not their ticker.
A bare neptune: was the initial proposal, but the project name is "Neptune Cash", not "Neptune", and the name "Neptune" is contested: Neptune Privacy (XNT) is a live fork that also presents itself as Neptune. This is not a funds-safety concern, because the two chains use distinct bech32m HRPs (nolga plus a network character in neptune-core, xntnw plus a network character in xnt-core), so an XNT address presented to a Neptune wallet is rejected as an unknown address type rather than being interpreted as a Neptune address. The collision is narrower but real: OS-level URI handler registration is a single global namespace per scheme, so if both projects ship wallets registering neptune:, which application receives a click-to-pay link becomes contested and platform-dependent, and a user with both installed can get the wrong app.
neptunecash: is therefore the more faithful application of the project-name convention, not a departure from it, and the comparison table at the end of this document already records the precedent: Bitcoin Cash uses bitcoincash:, not bitcoin:. The cost is 4 extra characters in the QR payload, negligible next to a generation address of roughly 3500 characters and irrelevant for links.
Resolution: a poll in the development channel (7 votes) preferred neptunecash: (43%) over neptune: (28%) and npt: (28%); five of the seven votes went to a spelled-out project name rather than the ticker, and a maintainer gave the go-ahead to proceed in the development channel the same day (2026-08-20). neptunecash: is primary for links and deep-linking; the legacy address-only npt: payload remains an optional compatibility choice for the wallets that already ship it, since it is a wallet-specific convention this spec does not standardize.
Generation addresses are long enough to strain practical QR code density. Options considered: recommend the shorter address types for QR use where acceptable; accept large QR codes; or additionally spec an out-of-band form where the URI carries a URL from which the full payment request is fetched (BIP72-style).
Resolution, from the RFC discussion: recommend the shorter address types for QR use, and also accept the large QR codes needed to encode generation addresses; the QR code guidance section above is the normative outcome. Fetchable payment requests stay out of scope for v1 and can be revisited in a later NIP.
Whole NPT with a decimal fraction, rather than the smallest unit. This matches BIP 21 practice and human expectations, and the comparison table at the end of this document records the cost of the alternative: smallest-unit amounts, as in Ethereum's wei and Nano's raw, are a recurring source of decimal-shift bugs. Every conforming amount converts to a whole number of nau exactly; what the grammar cannot express are on-chain amounts whose nau count is not a multiple of 4 (1 nau equals 2.5 * 10^-31 NPT), and requesting payment at that precision is deliberately out of scope.
The exact-arithmetic requirement has no counterpart in BIP 21 or ZIP 321 because those chains never needed one: Bitcoin's and Zcash's maximum supplies are about 2.1 * 10^15 atomic units, inside the 2^53 range an IEEE 754 double represents exactly, while Neptune Cash's maximum supply is 1.68 * 10^38 nau. Float parsing can silently change conforming amounts in Neptune Cash while remaining harmless for those chains, which is why this specification must state explicitly what its ancestors could leave unsaid.
BIP 21 and ZIP 321 set no maximum length for label or message, so each wallet invents its own limit and they diverge. The cap here exists so that validity is deterministic across wallets and implementations can bound what they store and render; the value 255 bytes is chosen for familiarity, not necessity, and can be revisited while this NIP is a Draft.
Excluded deliberately, not by omission. A raw symmetric-key encoding is the secret key itself, so a payment URI carrying one would hand spending power to everyone who sees the URI; the whole nsymk HRP family is rejected because digest-only display strings share it, and no parser should be in the business of telling the two apart. This is recorded here so that a future implementer does not add symmetric support back in the belief that it was overlooked.
| Coin | Scheme | Spec | Parameters | Notes |
|---|---|---|---|---|
| Bitcoin | bitcoin: |
BIP 21 | amount, label, message, req-* |
The template most others copy. BIP 72 added ?r=<url> for fetchable payment requests (BIP 70, now abandoned). |
| Zcash | zcash: |
ZIP 321 | amount, label, message, memo |
Closest precedent for Neptune Cash: a privacy coin with long/multiple address types. Supports multiple recipients in one URI via indexed params (address.1=, amount.1=). |
| Monero | monero: |
de facto wallet convention | tx_amount, recipient_name, tx_description |
Same idea as BIP 21 but with its own parameter names: the fragmentation a written spec avoids. |
| Ethereum | ethereum: |
EIP-681 | value, gas, chain id, function calls |
Extends into contract invocation; value is in wei (smallest unit). |
| Litecoin / Dogecoin / Dash | litecoin: etc. |
BIP 21 clones | same as Bitcoin | Fork keeps the convention, changes the scheme name. |
| Bitcoin Cash | bitcoincash: |
CashAddr + BIP 21 params | amount, label, message |
The address format itself carries the prefix, so bare address and URI are the same string. Precedent for the scheme-name rationale: the fork spells out the full project name rather than claiming the parent chain's bitcoin:. |
| Solana | solana: |
Solana Pay | amount, spl-token, reference, label, message, memo |
reference provides a mechanism for identifying a payment on-chain; neptune-wallet-app#66 requests a related tracking capability for Neptune. |
| Stellar | web+stellar: |
SEP-0007 | destination, amount, memo, callback |
Uses the web+ prefix convention for browser handler registration; see the handler registration section for why native schemes do not need it. |
| Nano | nano: |
de facto | amount (in raw, smallest unit), label, message |
Smallest-unit amounts are a frequent source of decimal-shift bugs. |