Developer Reference
This page is for wallets, apps and integrators building deposits and burns for frSOL, frADA and frXRP. For frFB, see the Fractal developer guide.
frSOL (4:50050), frXRP (4:50051) and frADA (4:50052) are live on mainnet. For bots and AI agents, see Agents and automation.
The alkanes
frSOL, frADA and frXRP are three builds of one contract design, forked from frFB's. They share every opcode:
| opcode | name | who | behaviour |
|---|---|---|---|
| 0 | Initialize {auth_token_units, sigil_units} | deployer, once | emits AUTH and SIGIL to the protostone pointer. Mints no tokens |
| 77 | Mint {token_units} | AUTH | mints within the window's mint cap. Trailing words (the signer group's deposit-sequence marker) are ignored by the contract |
| 88 | Burn | anyone | burns and records a redemption; arguments differ per asset (below) |
| 21 | GetBurnsByHeight {height} | view | the burn records at a Bitcoin height, concatenated |
| 22 | GetLimits | view | five u128 LE words: mint_cap, window_blocks, window_start, minted_in_window, max_burn |
| 23 | GetPendingLimits | view | empty, or four u128 LE words: mint_cap, window_blocks, max_burn, eta |
| 79 | Upgrade {block, tx} | SIGIL | records an upgrade target; does not change the running code |
| 80 | SetLimits {mint_cap, window_blocks, max_burn} | SIGIL | tightening applies at once and drops any queued raise; any raise (including a shorter window) is queued for 144 blocks |
| 81 | ExecuteLimits | SIGIL | applies a queued raise at or after its eta, until 1,008 blocks after it |
| 82 | CancelLimits | SIGIL | drops a queued raise |
| 99 / 100 / 101 / 1000 | name / symbol / total supply / data | view |
| frSOL | frADA | frXRP | |
|---|---|---|---|
| alkane id | 4:50050 | 4:50052 | 4:50051 |
| AUTH / SIGIL | 2:102983 / 2:102984 | 2:103101 / 2:103102 | 2:103066 / 2:103067 |
| unit | lamport (9 decimals) | lovelace (6 decimals) | drop (6 decimals) |
window_blocks allowed | 144–4,032 | 144–4,032 | 144–4,032 |
| hard ceiling: mint cap per window | 10,000 SOL | 400,000 ADA | 2,000,000 XRP |
| hard ceiling: max burn | 4,000 SOL | 400,000 ADA | 2,000,000 XRP |
Windows are anchored. A window opens at the height of the first mint after the previous window closed, and runs window_blocks blocks from there. GetLimits word 2 is that start height; a window is open while minted_in_window > 0 and height < window_start + window_blocks.
Reading a view
Views are free, read-only calls against SUBFROST's alkanes index. With alkanes-cli (see the CLI reference):
alkanes-cli -p mainnet --jsonrpc-url https://mainnet.subfrost.io/v4/jsonrpc \
alkanes simulate 4:50050:22 # GetLimits
For opcode 21, pass the height as the next cellpack word ([4, 50050, 21, height]). Over JSON-RPC, the same calls go through alkanes_simulate or metashrew_view (see the JSON-RPC reference).
Deposit intents
A deposit intent says where the alkane goes on Bitcoin. It is a different thing from the release intent a burn carries (see Release intents), and the two have different size limits: a deposit intent is at most 512 bytes, a release intent at most 256 bytes.
Each asset reads deposit intents a little differently:
| asset | deposit intent accepted | recipient script | what else is read |
|---|---|---|---|
| frSOL | none: the vault takes the recipient script directly, as deposit's btc_script argument | standard script, at most 34 bytes | nothing |
| frADA | frFB's gateway intent version 1 or 2, in the deposit's inline datum | 1–75 bytes | only the recipient script and the action. The refund key, CSV and (v2) K are skipped; the Cardano refund is the datum's refund address |
| frXRP | its own version 1 (no refund) or version 2 (with an XRP Ledger refund account), see frXRP deposit intent. frFB's version 2 is refused | 1–75 bytes | the recipient script, the action and (v2) the refund account and tag |
frFB's gateway intent format is specified on the Fractal developer guide. Version 2, as frADA accepts it:
| field | size |
|---|---|
version 0x02 | 1 |
| recipient script length n (1–75) | 1 |
recipient scriptPubKey on Bitcoin: receives the alkane | n |
| refund x-only key, or 32 zero bytes (ignored by frADA) | 32 |
| refund CSV, u16 LE (ignored by frADA) | 2 |
sweep fee K, u32 LE (ignored by frADA) | 4 |
| action length m, u16 LE | 2 |
| action, empty = plain | m |
Version 1 is the same without the 4-byte K field. (frXRP's own version 2 is a different layout; see below.) The first version of frADA and frXRP delivers every action as a plain mint; the action format is frFB's intent route actions.
Recipient scripts
The recipient is a raw Bitcoin scriptPubKey, not address text. Decode the address and build the script:
| address | script |
|---|---|
P2TR bc1p… (bech32m, witness v1) | 51 20 <32-byte program> |
P2WPKH bc1q…, 20-byte program | 00 14 <20 bytes> |
P2WSH bc1q…, 32-byte program | 00 20 <32 bytes> |
P2PKH 1… | 76 a9 14 <20 bytes> 88 ac |
P2SH 3… | a9 14 <20 bytes> 87 |
Example (BIP86's first mainnet address): bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr →
5120a60869f0dbcf1dc659c9cecbaf8050135ea9e8cdc487053f1dc6880949dc684c
Use an address from a wallet that can hold alkanes; taproot is the usual choice.
frSOL: the deposit instruction
Program 25oyhHXj19hFvtut2Vd9fq3u3E2YJrHwQXoY7vRFpfBb (Anchor).
Data: the Anchor discriminator, then the Borsh-encoded arguments:
| bytes | field |
|---|---|
| 8 | discriminator f2 23 c6 89 52 e1 f2 b6 |
| 8 | amount, u64 LE, lamports |
| 4 | btc_script length, u32 LE |
| n | btc_script, a standard script of at most 34 bytes |
Accounts, in order:
| # | account | flags | address |
|---|---|---|---|
| 0 | depositor | signer, writable | your wallet: pays the SOL and the record's rent |
| 1 | vault | writable | H9DgYrKMeQfUH4qrC1yFZ2QxZXThVrrCs7adtUyCGZqx (PDA, seed "vault") |
| 2 | treasury | writable | CanG1DgFswnD1wNMbUAgVPR4bh6mrhhmVNaez9Cg1EmV (PDA, seed "treasury") |
| 3 | deposit_record | writable | PDA, seeds "deposit" ‖ next_deposit_seq as u64 LE |
| 4 | system_program | 11111111111111111111111111111111 |
Read next_deposit_seq from the vault account (offset 40, below) right before building the transaction. If another deposit lands first, yours fails on the address check and nothing moves: read it again and resubmit.
Errors you may see: Paused (6000), BelowMinimum (6001), NonStandardScript (6002), ZeroAmount (6003), AboveMaximum (6010).
Reading the vault account
Vault (368 bytes), at H9DgYrKMeQfUH4qrC1yFZ2QxZXThVrrCs7adtUyCGZqx. All integers little-endian:
| offset | size | field |
|---|---|---|
| 0 | 8 | Anchor discriminator |
| 8 | 32 | authority: the signer group's Ed25519 key |
| 40 | 8 | next_deposit_seq |
| 48 | 8 | rotate_nonce |
| 56 | 8 | min_deposit (lamports) |
| 64 | 8 | max_deposit (lamports) |
| 72 | 1 | paused |
| 73 | 1 | bump |
| 74 | 1 | treasury_bump |
| 75 | 8 | withdraw_cap_per_window (lamports) |
| 83 | 8 | window_slots |
| 91 | 8 | window_start (slot) |
| 99 | 8 | window_spent (lamports) |
| 107 | 32 | operator: the offline operator key |
| 139 | 12 | burn_cursor (height u64 ‖ index u32) |
| 151 | 8 | intent_seq |
| 159 | 8 | timelock_slots |
| 167 | 1 | upgrade_bump |
| 168 | 5 × 40 | pending: {hash [32], executable_at u64} for limits, rotate, upgrade, timelock, evict. executable_at = 0 means empty |
Deposit records (PDA "deposit" ‖ seq u64 LE, 102 bytes): discriminator, seq u64, depositor (32), amount u64, btc_script (u32 length + bytes), slot u64.
Payout receipts (UsedBurn, PDA "used" ‖ the 12-byte burn id, 94 bytes): discriminator, burn_id (12), recipient (32), amount u64 (the burn's total payout), slot u64, voided u8, parts u8, paid_mask u64, paid u64, split_base u64. A burn is settled when it is voided or every one of its parts bits is set in paid_mask. A burn with no receipt has not been paid or voided.
Burning
Every burn is opcode 88 on the alkane, called with exactly one incoming alkane: the asset itself. The contract burns everything the burn protostone receives. One burn per protostone; a burn that reverts doesn't use up the slot, and the tokens go to the burn protostone's refund output.
Getting the amount right
Two rules of how alkanes move inside a transaction decide what the burn protostone receives:
- Every alkane on the transaction's inputs goes to the first alkanes protostone in the transaction.
- Edicts in a protostone that carries a call run after the call. An edict placed in the burn protostone itself can't limit the burn: by the time it runs, the call has already received, and burned, the whole input balance (or reverted, if that is above the max burn).
So a wallet must use one of these two shapes.
Shape A: spend a UTXO that holds exactly the amount. The transaction's inputs carry the asset to burn, exactly the amount, and no other alkane. Prepare such a UTXO beforehand with an ordinary transfer if you need to. The transaction has one protostone, the burn, with no edicts. Its pointer and refund are outputs you own. The first frSOL burn on mainnet (e27df8dc…7b34) was built this way.
Shape B: split first, in the same transaction. Two protostones, the split first:
| protostone | message | edicts | pointer | refund |
|---|---|---|---|---|
p0 | none | one edict: {asset id, amount, output = N + 1 + 1} | your change output | — |
p1 | the burn cellpack | none | an output you own | an output you own |
N is the number of outputs in the transaction, OP_RETURN included. Protostone i is addressed as the virtual output N + 1 + i, so the edict's output N + 2 sends exactly amount to p1. p0 receives every alkane on the inputs; everything except the edict's amount goes to p0's pointer, your change. Shape B lets you burn part of a UTXO's balance, and keeps any other alkanes on the inputs safe.
In alkanes-cli's protostone syntax an edict can target a protostone as pN; check what your tool actually emits, and the transaction, before signing.
Relay. A burn's OP_RETURN is larger than the legacy 83-byte relay limit in most cases: the first frSOL burn (Shape A) is 97 bytes, an frSOL burn protostone alone is about 81, and with Shape B's split protostone about 100. A base-address frADA burn alone is over it. Bitcoin Core 30 and later relay these by default.
Worked examples
All three use Shape B, with outputs v0 = your taproot address (alkane change and refunds), v1 = your BTC change, v2 = the OP_RETURN. So N = 3 and the edict's output is 3 + 1 + 1 = 5.
frSOL: burn 0.5 frSOL (500,000,000 units) to the example Solana key 4gJPamk7fDjNkY1rLVxrECa6jXMTpqgmLs16qEbwD1Vp (encoding):
p0 edicts [ {4:50050, 500000000, 5} ] pointer v0
p1 cellpack [4, 50050, 88, 254191324530969493053561943663094768694, 94454996492546882319798964218521386430]
pointer v0 refund v0
frADA (4:50052): burn 250 ADA (250,000,000 units) to the CIP-19 base address in Building a frADA burn:
p0 edicts [ {4:50052, 250000000, 5} ] pointer v0
p1 cellpack [4, 50052, 88, 59,
243785115696890623016683073811140131073,
68528828464926516401702606259487992545,
79755110632395976016866799648638984827,
98464968974830816898711145]
pointer v0 refund v0
frXRP (4:50051): burn 100 XRP (100,000,000 units) to rGWrZyQqhTp9Xu7G5Pkayo7bXjH4k4QYpf with destination tag 1 (encoding):
p0 edicts [ {4:50051, 100000000, 5} ] pointer v0
p1 cellpack [4, 50051, 88, 26, 165048989300338120600441722749357304322, 282618147814843]
pointer v0 refund v0
For Shape A, drop p0, make sure the inputs hold exactly the amount and no other alkane, and use the same p1.
Burn ids
Every burn record has an id derived from Bitcoin alone: (height, index), the burn's block height and its position in that height's queue (the order GetBurnsByHeight returns). As 12 bytes it is height u64 LE ‖ index u32 LE. Payouts are made in (height, index) order, and the foreign-chain side records each burn under this id:
- frSOL: the vault's
UsedBurnseed, and the payout memofrsol:burn:<height>:<index>. A payout made in parts adds:<part>/<parts>to the memo, so match on the prefix. The memo is for explorers; the vault's receipt is the record; - frADA: the vault's burn cursor;
- frXRP: the
frxrp/payoutmemo of the payment from the group account (orfrxrp/voidfor a voided burn), whose data is the 12-byte id.
frSOL burn
[4, 50050, 88, recipient0, recipient1]
recipient0 = bytes 0–15 of the 32-byte Solana public key read as a little-endian u128; recipient1 = bytes 16–31, the same way.
Worked example, with the example key 4gJPamk7fDjNkY1rLVxrECa6jXMTpqgmLs16qEbwD1Vp:
base58 decode 36a45fe165ee6c059fcbb8e6e5773bbfbefd121c2ce253b6b9027792e05e0f47
bytes 0–15 36a45fe165ee6c059fcbb8e6e5773bbf → LE u128 254191324530969493053561943663094768694
bytes 16–31 befd121c2ce253b6b9027792e05e0f47 → LE u128 94454996492546882319798964218521386430
cellpack [4, 50050, 88, 254191324530969493053561943663094768694, 94454996492546882319798964218521386430]
Record (48 bytes): amount u128 LE ‖ recipient [32]. GetBurnsByHeight returns the records concatenated, so a height's queue is a multiple of 48 bytes.
Reverts: more or other than one incoming alkane, the incoming alkane is not frSOL, amount zero or above max_burn, the all-zero recipient, a second burn in the protostone. See frSOL for burns that are accepted but not paid.
Release intents (frADA, frXRP)
frADA and frXRP burns carry a variable-length release intent, packed into the cellpack after its byte length:
[4, <id>, 88, len, w0, w1, …, w(n-1)]
lenis the release intent's length in bytes, at most 256. The decoders accept frADA 1–256 and frXRP 22–256, but the shortest valid frADA intent is 31 bytes (an enterprise address), so frADA's usable range is 31–256;n = ⌈len / 16⌉;w_iis bytes16i … 16i+15of the intent read as a little-endianu128(byte16iis the lowest byte); the unused high bytes of the last word must be zero;- any words after
w(n-1)must be zero. The protostone encoding pads the message to whole chunks, which decode as trailing zeros, so zeros are tolerated and anything else reverts.
The first byte of a release intent is its version, which also identifies the chain, so one peg's intent never parses on another:
| chain | version | layout |
|---|---|---|
| Cardano | 0x01 | 0x01 ‖ addr_len u8 ‖ raw Shelley address ‖ action |
| XRP Ledger | 0x02 | 0x02 ‖ AccountID [20] ‖ has_tag u8 ‖ tag u32 LE (only if has_tag = 1) ‖ action |
Record (frADA and frXRP): amount u128 LE ‖ intent_len u16 LE ‖ intent, 18 + len bytes, self-delimiting. GetBurnsByHeight returns them concatenated; walk the queue by reading each record's length.
Building a frADA burn
The release intent carries the address's raw bytes: bech32-decode the addr1… string (it is longer than bech32's usual 90-character limit, so use a decoder that allows that) and take the payload. Its first byte is the header: the high nibble is the address type, the low nibble the network, which must be 1 (mainnet).
| header type | address | raw length |
|---|---|---|
| 0–3 | base (payment + stake) | 57 bytes |
| 4–5 | pointer | 29 bytes + three variable-length naturals (32–59 bytes) |
| 6–7 | enterprise (payment only) | 29 bytes |
Byron addresses, reward addresses (types 14, 15), reserved types and testnet addresses revert.
Worked example, the CIP-19 mainnet base address test vector addr1qx2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3n0d3vllmyqwsx5wktcd8cc3sq835lu7drv2xwl2wywfgse35a3x:
raw address (57 bytes)
019493315cd92eb5d8c4304e67b7e16ae36d61d34502694657811a2c8e337b62cfff6403a06a3acbc34f8c46003c69fe79a3628cefa9c47251
release intent (59 bytes) = 01 39 ‖ raw address
0139019493315cd92eb5d8c4304e67b7e16ae36d61d34502694657811a2c8e337b62cfff6403a06a3acbc34f8c46003c69fe79a3628cefa9c47251
cellpack
[4, 50052, 88, 59,
243785115696890623016683073811140131073,
68528828464926516401702606259487992545,
79755110632395976016866799648638984827,
98464968974830816898711145]
The enterprise address of the same payment key, addr1vx2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzers66hrl8, is 29 bytes (61…8e), a 31-byte intent 011d61…8e, and the cellpack [4, 50052, 88, 31, 243785115696890623016683073811146415361, 738200679895806883608446185190419169].
A base-address burn carries four full words, so its OP_RETURN is over the legacy 83-byte relay limit on its own (see Relay).
Building a frXRP burn
The release intent carries the raw 20-byte AccountID, never address text. Both XRP address forms are base58check with the ripple alphabet (rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz; the checksum is the first 4 bytes of SHA-256(SHA-256(payload))). Decode, verify the checksum, then:
- Classic address (
r…): the payload is 21 bytes,0x00 ‖ AccountID. If the destination needs a tag, take it from a separate destination-tag field:has_tag = 1and the u32; otherwisehas_tag = 0. - X-address (
X…): the payload is 31 bytes,0x05 0x44 ‖ AccountID ‖ flag ‖ tag u64 LE.flag = 0means no tag (the 8 tag bytes must be zero);flag = 1means the tag is the low 4 bytes (the high 4 must be zero). Refuse any other flag (64-bit tags), and testnet X-addresses (T…, prefix0x04 0x93). If the user also typed a separate tag, it must match.
Worked example, from the ripple-address-codec test vectors: XVLhHMPHU98es4dbozjVtdWzVrDjtV8xvjGQTYPiAx6gwDC, which is rGWrZyQqhTp9Xu7G5Pkayo7bXjH4k4QYpf with tag 1:
AccountID aa066c988c712815cc37af71472b7cbbbd4e2a0a
release intent (26) 02 aa066c988c712815cc37af71472b7cbbbd4e2a0a 01 01000000
cellpack [4, 50051, 88, 26, 165048989300338120600441722749357304322, 282618147814843]
same account, no tag:
release intent (22) 02 aa066c988c712815cc37af71472b7cbbbd4e2a0a 00
cellpack [4, 50051, 88, 22, 165048989300338120600441722749357304322, 43659476411]
Before building it, check the destination at a validated ledger (account_info, server_state): not the group account; at least the base reserve if the account doesn't exist; a tag if it requires one; no deposit authorization. See frXRP.
frXRP deposit intent and address
The intent
frXRP accepts two deposit intent versions. Both are canonical (no trailing bytes), at most 512 bytes, with a recipient script of 1–75 bytes:
v1 0x01 ‖ n u8 ‖ recipient_spk [n] ‖ 32 zero bytes ‖ csv u16 LE = 0 ‖ m u16 LE ‖ action [m]
v2 0x02 ‖ n u8 ‖ recipient_spk [n] ‖ refund AccountID [20] ‖ has_tag u8 (0|1) ‖ tag u32 LE (only if has_tag = 1) ‖ m u16 LE ‖ action [m]
- v1 has no refund path. It is frFB's version-1 gateway intent in its no-refund form: the refund key must be 32 zero bytes and the CSV 0.
- v2 always carries an XRP Ledger refund account, with an optional destination tag. It is where the deposit is paid back if its deposit address can never be swept (below). No refund is written as v1, so each meaning has exactly one encoding. frXRP's v2 is not frFB's v2 (frFB's carries a Fractal sweep fee); frFB v2 bytes are refused.
- A tag of 0 (
has_tag = 1, tag 0) is a realDestinationTag: 0, distinct from no tag, and gives a different deposit address. - The refund account may not be ACCOUNT_ZERO or ACCOUNT_ONE. The gateway also refuses the group account, and refund addresses for the wrong network.
- Use v2. The refund account is part of the bytes the deposit address commits to, so nobody can change where a refund goes without changing the address.
Any other bytes are refused, and XRP sent to an address derived from a refused intent is never swept or minted. Both versions mint plain frXRP to the recipient script; any action is delivered plain.
The deposit address D(intent)
t = SHA-512( "FROST-ED25519-SHA512-v1" ‖ "id" ‖ "frxrp-intent-v1" ‖ 0x00 ‖ G_pk ‖ intent ) mod ℓ
D_pk = G_pk + t·B
D = classic address of AccountID = RIPEMD-160( SHA-256( 0xED ‖ D_pk ) )
- The 64-byte SHA-512 digest is read as a little-endian integer and reduced modulo the Ed25519 group order ℓ (the usual "wide" reduction). The strings are ASCII bytes, with no length prefixes;
0x00is one zero byte.intentis the whole intent, v1 or v2. frxrp-intent-v1names the derivation, not the intent version: v2 intents use the same domain.G_pkis the signer group's 32-byte Ed25519 public key,Bthe Ed25519 base point, andD_pkthe compressed point. This is the FROST(Ed25519, SHA-512) tweak the signer group signs with, so only the group can sign forD.
On mainnet, the group's key and account are:
G_pk 7a06eebf8e040b065e9b7022f2c5acbe4441ed65cea9d7e3ed7eae881287a095
AccountID RIPEMD-160(SHA-256(0xED ‖ G_pk)) = 8c4612af82f72c2094c1cee150a3cb6a8f9a748e
G rD86YcZ35wxoTJEhrtmascwW932h2Qa7yE
Test vectors. None of these is a real group: G_pk is the RFC 8032 key of the seed 00 01 … 1f, 03a107bff3ce10be1d70dd18e74bc09967e4d6309ba50d5f1ddc8664125531b8 (r31tMdHuY2hzyxqFkyMof7WQxD9ZVRMrXH). The recipient script in each is the P2TR script 5120 followed by 32 bytes of ab, written <spk> below.
v2, refund to rGMTQpyhaDwWTqmw4dcYHj5NPJhtWNhtRW (AccountID a8683dc013f856accdd629e0730e9fbb707d0d1b) with tag 12345:
intent 02 22 <spk> a8683dc013f856accdd629e0730e9fbb707d0d1b 01 39300000 0000
t 6a0374fffd3ae4576ad5ef3b477126a1f29fd985d50dd02061ca15c03c67b70e
D_pk b1f15df44268f23a82b245ab8853458f41b43ad14d54e564932e444780943881
D rh2yHobysCuFhLwnVGjpaE6wc2MXBpE35k (X-address X7CNVAuvvZzVp1qQtEq5WUhY98o9s19q5gMpSd9nJEvNwdh)
v2, the same refund account with no tag:
intent 02 22 <spk> a8683dc013f856accdd629e0730e9fbb707d0d1b 00 0000
D rK3BcMYrbMPP9sag3VKChxU3PakwffqN31
v1 (no refund):
intent 01 22 <spk> 00…00 (32 bytes) 0000 0000
t d0326b7c61080698b83471cc64988962bafe18c9a6d5b3a0dd727bc434668e05
D_pk 77e3e4b659921ef3d49e26395b02f640ed82141bedf22ab3c260cf685f294995
D rpRjsCPYT7mztrAPcgd2mNUeYUD5XZs1Rh (X-address X7YUPnbh4Fba6L82tY1itzy3We6y7iA8SgbwTM5ZfkBtcMt)
The gateway API
A SUBFROST gateway service stores intents so the signer group can find them. The mainnet gateway is https://fr1q4mgjev2fnzp2z2p0hwv89q59pxnq8r07px97rcrfkptdhlw6vyqcwkl7e.fr.style (also on .subdns.to and .asilos.ltd), served over SUBFROST's subtun0 overlay. It holds no keys, and every seat re-derives D from the stored bytes, so a gateway can't redirect a deposit.
-
POST /v1/intenttakes either:intent_hex, a full v1 or v2 intent; or- the fields
recipient_spk(orrecipient_address), an optionalaction, and an optional refund:refund_address, either a classicr…address plus an optionalrefund_tag, or an X-address carrying its tag. A refund makes the intent v2.
Pass
expected_address, theDyou derived yourself. If it doesn't match, the gateway answers 409 and stores nothing. This catches, for example, a client that derived the v1 address while asking for a refund.The response includes
deposit_address,x_address,min_deposit_drops,intent_hex,intent_version,refund({address, tag, x_address}, or null for v1),group_pubkeyandgroup_address. -
GET /v1/intent/{address}(classic or X-address) returns the stored intent, including itsrefund. -
GET /v1/infolists the acceptedintent_versions.
Always derive D yourself and compare before you send XRP to it.
What the sweep requires
The group sweeps a deposit address (an AccountDelete of D into the group account, with the intent as an frxrp/intent memo) only when, in a validated ledger:
Dholds at least the minimum deposit, 2 XRP by default. The signers never set it below the base reserve plus the owner reserve, so a sweep always delivers something. A deposit below the minimum waits; you can top the address up;Dis at least 256 ledgers old (Sequence + 256 ≤the ledger), the XRP Ledger's rule for deleting an account;Dhas no deletion blockers (below);- the sweep simulates successfully on every provider.
The sweep's fee is the owner reserve (0.2 XRP at the time of writing), taken from the deposit. The mint is the delivered amount, less the mint fee, to the intent's recipient script. Only these sweeps are ever minted. A delivery whose memo does not re-derive to the account it came from, or whose recipient script can't be paid, is quarantined: nothing is minted, and its XRP stays in the group account for the operator.
Blocked deposit addresses
Anyone can attach an object to an XRP Ledger account's directory: an incoming Check, Escrow or payment channel, or a trust line they open to it. While one exists, D can't be deleted. The group treats D as blocked when:
- a validated read lists a deletion blocker; or
- the sweep's simulation answers
tecHAS_OBLIGATIONS; or - the sweep's simulation answers
tecTOO_SOONa full 256 ledgers past the age rule.
A blocked D holding at least the minimum deposit gets one Payment from D, signed by the group:
amount = balance − (base reserve + owner reserve × D's owned objects) − 12 drops
- Refund. If the intent is v2 and its refund account can receive that amount, the payment goes to the refund account with the intent's tag, with memo
frxrp/refund(data: SHA-256 of the intent). Nothing is minted. - Held. Otherwise the payment goes to the group account with destination tag 0, with memo
frxrp/held(data: the intent bytes, so anyone can re-deriveD). This covers a v1 intent, and a refund account that can't receive it: missing and the amount below the base reserve, requires a tag the intent lacks, has deposit authorization, or is the group account orDitself. A held deposit is never minted. It stays a refundable liability, recorded in the group account's own history. SUBFROST's operator settles it with an approved payment from the group account, memofrxrp/refund-held(data: the held payment's transaction id), at most once per held payment.
In both cases the reserve stays on D: at least the base reserve (1 XRP today) plus the owner reserve for each object on it can't be recovered.