A transaction is a plain, visible JSON object. Nothing in it is encrypted, the memo included.
| Field | Required | Description |
|---|---|---|
from | yes | Sender address (twelve dot-separated words) |
pubkey | yes | Sender's FALCON-512 public key, hex. Its derived address must equal from. |
outputs | yes | Non-empty list of {"to": address, "amount": ticks}, at most 500. Amounts are positive integers. You cannot pay your own address. |
nonce | yes | Integer, exactly the sender's last used nonce plus one (the first transaction uses 1) |
fee | yes | Non-negative integer in ticks, chosen by the sender |
memo | no | Plaintext string, at most 200 bytes, no null bytes. Omit the key entirely when there is no memo. |
signature | yes | FALCON-512 signature over the canonical JSON of every other field, hex |
Unknown fields are rejected. The sender's balance must cover the sum of all outputs plus the fee. 1 LAPSE = 100,000,000 ticks.
- Build the transaction object without
signature. - Serialize it as canonical JSON: keys sorted, no whitespace. The node uses
orjsonwith sorted keys. For integers and strings, Python'sjson.dumps(tx, sort_keys=True, separators=(",", ":"), ensure_ascii=False)produces the same bytes. - Sign those bytes with FALCON-512 (liboqs, algorithm name
"Falcon-512"). - Add the signature as a hex string in
signature. - POST the whole object to
/api/tx/send.
FALCON signing uses fresh randomness, so re-signing the same content gives a different valid signature. The transaction hash excludes the signature, so the hash stays the same.
The fee is an absolute amount in ticks. Builders prefer higher fee per byte, where the size is the canonical JSON length of the transaction excluding the signature (and including the fee field itself). GET /api/fees reports the current next_block rate. Board posts (memo starting with [board] ) have an additional protocol-enforced fee floor.
Nonces are sequential per sender, starting from 0 for an address that has never sent. A transaction is valid only if its nonce is exactly the last used nonce plus one, counting the sender's pending mempool transactions. GET /api/state?addr=... reports the highest nonce used, confirmed or pending; the next transaction uses that plus one. See the API page.
The board is made of ordinary transactions whose memo follows a few conventions. A ref is the first six hex characters of a post's transaction hash.
| Memo | Meaning |
|---|---|
[board] + text | A post. May begin with a profile header [p:icon:nick] and then a reply header [r:ref]. Pays the board fee floor and burns 1 tick. |
[vote+] ref, [vote-] ref | A vote on a post. Not a board post: no fee floor, 1 tick burned. |
[board] [e:ref:pos:ndel]text | An edit. In the sender's post with that ref, replace ndel characters at character position pos with text. It is a board post, so it pays the fee floor, burns 1 tick and counts toward the floor's staircase. |
[del] ref | Hides the sender's post with that ref. Not a board post: no fee floor, 1 tick burned. |
Edits and deletes change nothing on the chain, and nothing could: the original text stays in its block. They are rules that readers apply the same way. An edit or delete acts only on a post by its own sender that came before it, and an edit is applied to the post as earlier edits left it, in chain order. The result must be non-empty and fit in a post (200 bytes of memo less the [board] tag). An edit that cannot be applied is not hidden: it shows as the ordinary post it also is. The node's validation does not check any of this, so a node will relay an edit or delete of someone else's post, and readers will ignore it.
- Take the SHA-256 of the raw FALCON-512 public key bytes.
- Keep the first 132 bits and split them into twelve 11-bit indices, most significant first.
- Look each index up in the BIP39 English wordlist (2048 words, shipped as
src/bip39_english.txt) and join with dots.
Addresses are a one-way hash of the public key, so any syntactically valid word sequence passes validation. Validity does not prove that anyone holds the key.
Requires liboqs-python (module oqs) and requests. Keys come from oqs.Signature("Falcon-512").generate_keypair() and export_secret_key().
import hashlib, json, requests, oqs
NODE = "https://lapsenode.vicnas.me"
WORDLIST = open("bip39_english.txt").read().split() # src/bip39_english.txt in the repo
def address_from_pubkey(pk: bytes) -> str:
bits = int.from_bytes(hashlib.sha256(pk).digest()[:17], "big") >> 4 # top 132 bits
return ".".join(WORDLIST[(bits >> (11 * i)) & 0x7FF] for i in range(11, -1, -1))
def canonical(obj) -> bytes:
return json.dumps(obj, sort_keys=True, separators=(",", ":"),
ensure_ascii=False).encode()
def next_nonce(addr: str) -> int:
hist = requests.get(f"{NODE}/api/address/{addr}/history").json()
sent = sum(1 for r in hist if r["direction"] == "sent")
pool = requests.get(f"{NODE}/api/mempool").json()["transactions"]
pending = sum(1 for t in pool if t["from"] == addr)
return sent + pending + 1
def send(secret_key: bytes, public_key: bytes, to_addr: str,
amount_ticks: int, fee_ticks: int, memo: str = ""):
addr = address_from_pubkey(public_key)
tx = {
"from": addr,
"pubkey": public_key.hex(),
"outputs": [{"to": to_addr, "amount": amount_ticks}],
"nonce": next_nonce(addr),
"fee": fee_ticks,
}
if memo:
tx["memo"] = memo
signature = oqs.Signature("Falcon-512", secret_key).sign(canonical(tx))
tx["signature"] = signature.hex()
return requests.post(f"{NODE}/api/tx/send", json=tx).json()
# {"ok": true, "tx_hash": "..."} or {"ok": false, "error": "..."}
import requests
def confirmed_deposits(node, addr):
rows = requests.get(f"{node}/api/address/{addr}/history").json()
for r in rows:
if r["direction"] != "received":
continue
tx = r["tx"]
ticks = sum(o["amount"] for o in tx["outputs"] if o["to"] == addr)
yield r["tx_hash"], r["height"], ticks, tx.get("memo", "")
def confirmations(node, tx_hash):
t = requests.get(f"{node}/api/tx/{tx_hash}").json()
return t.get("confirmations", 0)
Deduplicate by tx_hash, sum every output paying your address (one transaction can pay you more than once), and wait for the confirmation depth you are comfortable with before crediting.
- Amounts are integer ticks. Never use floating point for balances.
- The memo is optional, public and permanent.
- The burn address (the first twelve words of the BIP39 list) can never be a sender.
- Wallet keys held by a node are stored encrypted with Argon2id and a mandatory passphrase.