diff --git a/modules/ecash-lib/src/index.ts b/modules/ecash-lib/src/index.ts
--- a/modules/ecash-lib/src/index.ts
+++ b/modules/ecash-lib/src/index.ts
@@ -28,5 +28,6 @@
 export * from './token/common.js';
 export * from './token/empp.js';
 export * from './token/slp.js';
+export * from './token/slp.parse.js';
 
 export * as payment from './payment';
diff --git a/modules/ecash-lib/src/token/common.ts b/modules/ecash-lib/src/token/common.ts
--- a/modules/ecash-lib/src/token/common.ts
+++ b/modules/ecash-lib/src/token/common.ts
@@ -4,10 +4,34 @@
 
 import { strToBytes } from '../io/str.js';
 
-export const GENESIS = strToBytes('GENESIS');
-export const MINT = strToBytes('MINT');
-export const SEND = strToBytes('SEND');
-export const BURN = strToBytes('BURN');
+/** GENESIS tx type: Creates a new token ID */
+export const GENESIS_STR = 'GENESIS';
+export const GENESIS = strToBytes(GENESIS_STR);
+
+/** MINT tx type: Mints more of a token ID */
+export const MINT_STR = 'MINT';
+export const MINT = strToBytes(MINT_STR);
+
+/** SEND tx type: Moves existing tokens to different outputs */
+export const SEND_STR = 'SEND';
+export const SEND = strToBytes(SEND_STR);
+
+/** BURN tx type: Destroys existing tokens */
+export const BURN_STR = 'BURN';
+export const BURN = strToBytes(BURN_STR);
+
+/**
+ * UNKNOWN: Placeholder for unknown token types.
+ * Note: These may hold valuable tokens, but which aren't recognized.
+ * They should be excluded from UTXO selection.
+ **/
+export const UNKNOWN_STR = 'UNKNOWN';
+
+/** Number of bytes in a token ID */
+export const TOKEN_ID_NUM_BYTES = 32;
+
+/** How many decimals a token can have at most (SLP/ALP) */
+export const MAX_DECIMALS = 9;
 
 /** Genesis info found in GENESIS txs of tokens */
 export interface GenesisInfo {
diff --git a/modules/ecash-lib/src/token/slp.parse.ts b/modules/ecash-lib/src/token/slp.parse.ts
new file mode 100644
--- /dev/null
+++ b/modules/ecash-lib/src/token/slp.parse.ts
@@ -0,0 +1,382 @@
+// Copyright (c) 2025 The Bitcoin developers
+// Distributed under the MIT software license, see the accompanying
+// file COPYING or http://www.opensource.org/licenses/mit-license.php.
+
+import { bytesToStr } from '../io/str.js';
+import { toHex } from '../io/hex.js';
+import { OP_RETURN } from '../opcode.js';
+import { isPushOp } from '../op.js';
+import { Script, ScriptOpIter } from '../script.js';
+import {
+    BURN_STR,
+    GENESIS_STR,
+    GenesisInfo,
+    MAX_DECIMALS,
+    MINT_STR,
+    SEND_STR,
+    TOKEN_ID_NUM_BYTES,
+    UNKNOWN_STR,
+} from './common.js';
+import {
+    SLP_ATOMS_NUM_BYTES,
+    SLP_FUNGIBLE,
+    SLP_GENESIS_HASH_NUM_BYTES,
+    SLP_LOKAD_ID_STR,
+    SLP_MAX_SEND_OUTPUTS,
+    SLP_MINT_VAULT,
+    SLP_MINT_VAULT_SCRIPTHASH_NUM_BYTES,
+    SLP_NFT1_CHILD,
+    SLP_NFT1_GROUP,
+    SlpTokenType,
+} from './slp.js';
+
+/** Parsed SLP GENESIS OP_RETURN Script */
+export interface SlpGenesis {
+    /** "GENESIS" */
+    txType: typeof GENESIS_STR;
+    /** Token type of the token to create */
+    tokenType: SlpTokenType;
+    /** Info about the token */
+    genesisInfo: GenesisInfo;
+    /** Number of token atoms to initially mint to out_idx=1 */
+    initialAtoms: bigint;
+    /** Output index to send the mint baton to, or undefined if none */
+    mintBatonOutIdx?: number;
+}
+
+/**
+ * Parsed SLP MINT (token type 0x01 and 0x81) OP_RETURN Script.
+ * Note: Token type 0x41 has no mint batons.
+ **/
+export interface SlpMintClassic {
+    /** "MINT" */
+    txType: typeof MINT_STR;
+    /** Token type of the token to mint */
+    tokenType: typeof SLP_FUNGIBLE | typeof SLP_NFT1_GROUP;
+    /** Token ID of the token to mint */
+    tokenId: string;
+    /** Number of token atoms to mint to out_idx=1 */
+    additionalAtoms: bigint;
+    /** Output index to send the mint baton to, or undefined to destroy it */
+    mintBatonOutIdx?: number;
+}
+
+/** Parsed SLP MINT (token type 0x02) OP_RETURN Script */
+export interface SlpMintVault {
+    /** "MINT" */
+    txType: typeof MINT_STR;
+    /** Token type of the token to mint (0x02) */
+    tokenType: typeof SLP_MINT_VAULT;
+    /** Token ID of the token to mint */
+    tokenId: string;
+    /** Array of the number of token atoms to mint to the outputs at 1 to N */
+    additionalAtomsArray: bigint[];
+}
+
+/** Parsed SLP MINT OP_RETURN Script */
+export type SlpMint = SlpMintClassic | SlpMintVault;
+
+/** Parsed SLP SEND OP_RETURN Script */
+export interface SlpSend {
+    /** "SEND" */
+    txType: typeof SEND_STR;
+    /** Token type of the token to send */
+    tokenType: SlpTokenType;
+    /** Token ID of the token to send */
+    tokenId: string;
+    /** Array of the number of token atoms to send to the outputs at 1 to N */
+    sendAtomsArray: bigint[];
+}
+
+/** Parsed SLP BURN OP_RETURN Script */
+export interface SlpBurn {
+    /** "BURN" */
+    txType: typeof BURN_STR;
+    /** Token type of the token to burn */
+    tokenType: SlpTokenType;
+    /** Token ID of the token to burn */
+    tokenId: string;
+    /** How many tokens should be burned */
+    burnAtoms: bigint;
+}
+
+/** New unknown SLP token type or tx type */
+export interface SlpUnknown {
+    /** Placeholder for unknown token type or tx type */
+    txType: typeof UNKNOWN_STR;
+    /** Token type number */
+    tokenType: number;
+}
+
+/** Parsed SLP OP_RETURN Script */
+export type SlpData = SlpGenesis | SlpMint | SlpSend | SlpBurn | SlpUnknown;
+
+/**
+ * Parse the given SLP OP_RETURN Script.
+ *
+ * For data that's clearly not SLP it will return `undefined`.
+ * For example, if the OP_RETURN or LOKAD ID is missing.
+ *
+ * For an unknown token type, it'll return SlpUnknown.
+ *
+ * For a known token type, it'll parse the remaining data, or throw an error if
+ * the format is invalid or if there's an unknown tx type.
+ *
+ * This behavior mirrors that of Chronik for consistency.
+ **/
+export function parseSlp(opreturnScript: Script): SlpData | undefined {
+    const ops = opreturnScript.ops();
+    const opreturnOp = ops.next();
+
+    // Return undefined if not OP_RETURN
+    if (
+        opreturnOp === undefined ||
+        isPushOp(opreturnOp) ||
+        opreturnOp !== OP_RETURN
+    ) {
+        return undefined;
+    }
+
+    // Return undefined if LOKAD ID is not "SLP\0"
+    const lokadId = ops.next();
+    if (lokadId === undefined || !isPushOp(lokadId)) {
+        return undefined;
+    }
+    if (bytesToStr(lokadId.data) !== SLP_LOKAD_ID_STR) {
+        return undefined;
+    }
+
+    // Parse token type
+    const tokenTypeBytes = nextBytes(ops);
+    if (tokenTypeBytes === undefined) {
+        throw new Error('Missing tokenType');
+    }
+    if (tokenTypeBytes.length !== 1) {
+        throw new Error('tokenType must be exactly 1 byte');
+    }
+    const tokenType = tokenTypeBytes[0];
+    if (
+        tokenType !== SLP_FUNGIBLE &&
+        tokenType !== SLP_MINT_VAULT &&
+        tokenType !== SLP_NFT1_GROUP &&
+        tokenType !== SLP_NFT1_CHILD
+    ) {
+        return {
+            txType: UNKNOWN_STR,
+            tokenType,
+        };
+    }
+
+    // Parse tx type (GENESIS, MINT, SEND, BURN)
+    const txTypeBytes = nextBytes(ops);
+    if (txTypeBytes === undefined) {
+        throw new Error('Missing txType');
+    }
+    const txType = bytesToStr(txTypeBytes);
+
+    // Handle tx type specific parsing.
+    // Advances the `ops` Script iterator
+    switch (txType) {
+        case GENESIS_STR:
+            return nextGenesis(ops, tokenType);
+        case MINT_STR:
+            return nextMint(ops, tokenType);
+        case SEND_STR:
+            return nextSend(ops, tokenType);
+        case BURN_STR:
+            return nextBurn(ops, tokenType);
+        default:
+            throw new Error('Unknown txType');
+    }
+}
+
+function nextGenesis(ops: ScriptOpIter, tokenType: SlpTokenType): SlpGenesis {
+    // Parse genesis info
+    const tokenTicker = bytesToStr(nextBytesRequired(ops, 'tokenTicker'));
+    const tokenName = bytesToStr(nextBytesRequired(ops, 'tokenName'));
+    const url = bytesToStr(nextBytesRequired(ops, 'url'));
+    const hash = nextBytesRequired(ops, 'hash');
+    if (hash.length !== 0 && hash.length !== SLP_GENESIS_HASH_NUM_BYTES) {
+        throw new Error(
+            `hash must be either 0 or ${SLP_GENESIS_HASH_NUM_BYTES} bytes`,
+        );
+    }
+    const decimalsBytes = nextBytesRequired(ops, 'decimals');
+    if (decimalsBytes.length !== 1) {
+        throw new Error('decimals must be exactly 1 byte');
+    }
+    const decimals = decimalsBytes[0];
+    if (decimals > MAX_DECIMALS) {
+        throw new Error(`decimals must be at most ${MAX_DECIMALS}`);
+    }
+
+    // Parse mint data
+    let mintVaultScripthash: string | undefined = undefined;
+    let mintBatonOutIdx: number | undefined = undefined;
+    if (tokenType === SLP_MINT_VAULT) {
+        const scripthashBytes = nextBytesRequired(ops, 'mintVaultScripthash');
+        if (scripthashBytes.length !== SLP_MINT_VAULT_SCRIPTHASH_NUM_BYTES) {
+            throw new Error(
+                `mintVaultScripthash must be exactly ${SLP_MINT_VAULT_SCRIPTHASH_NUM_BYTES} ` +
+                    'bytes long',
+            );
+        }
+        mintVaultScripthash = toHex(scripthashBytes);
+    } else {
+        mintBatonOutIdx = nextMintOutIdx(ops, tokenType);
+    }
+    const initialAtoms = parseSlpAtoms(nextBytesRequired(ops, 'initialAtoms'));
+    nextEnd(ops, 'GENESIS');
+    return {
+        txType: GENESIS_STR,
+        tokenType,
+        genesisInfo: {
+            tokenTicker,
+            tokenName,
+            url,
+            hash: hash.length !== 0 ? toHex(hash) : undefined,
+            mintVaultScripthash,
+            decimals,
+        },
+        initialAtoms,
+        mintBatonOutIdx,
+    };
+}
+
+function nextMint(ops: ScriptOpIter, tokenType: SlpTokenType): SlpMint {
+    const tokenId = nextTokenId(ops);
+    if (tokenType === SLP_MINT_VAULT) {
+        const additionalAtomsArray = nextSlpAtomsArray(ops);
+        return {
+            txType: MINT_STR,
+            tokenType,
+            tokenId,
+            additionalAtomsArray,
+        };
+    } else if (tokenType === SLP_NFT1_CHILD) {
+        throw new Error('SLP_NFT1_CHILD cannot have MINT transactions');
+    } else {
+        const mintBatonOutIdx = nextMintOutIdx(ops, tokenType);
+        const additionalAtoms = parseSlpAtoms(
+            nextBytesRequired(ops, 'additionalAtoms'),
+        );
+        nextEnd(ops, 'MINT');
+        return {
+            txType: MINT_STR,
+            tokenType,
+            tokenId,
+            additionalAtoms,
+            mintBatonOutIdx,
+        };
+    }
+}
+
+function nextSend(ops: ScriptOpIter, tokenType: SlpTokenType): SlpSend {
+    const tokenId = nextTokenId(ops);
+    const sendAtomsArray = nextSlpAtomsArray(ops);
+    return {
+        txType: SEND_STR,
+        tokenType,
+        tokenId,
+        sendAtomsArray,
+    };
+}
+
+function nextBurn(ops: ScriptOpIter, tokenType: SlpTokenType): SlpBurn {
+    const tokenId = nextTokenId(ops);
+    const burnAtoms = parseSlpAtoms(nextBytesRequired(ops, 'burnAtoms'));
+    nextEnd(ops, 'BURN');
+    return {
+        txType: BURN_STR,
+        tokenType,
+        tokenId,
+        burnAtoms,
+    };
+}
+
+function nextBytes(iter: ScriptOpIter): Uint8Array | undefined {
+    const op = iter.next();
+    if (op === undefined) {
+        return undefined;
+    }
+    if (!isPushOp(op)) {
+        throw new Error('SLP only supports push-ops');
+    }
+    return op.data;
+}
+
+function nextBytesRequired(iter: ScriptOpIter, name: string): Uint8Array {
+    const bytes = nextBytes(iter);
+    if (bytes === undefined) {
+        throw new Error('Missing ' + name);
+    }
+    return bytes;
+}
+
+function nextMintOutIdx(
+    iter: ScriptOpIter,
+    tokenType: number,
+): number | undefined {
+    const outIdxBytes = nextBytesRequired(iter, 'mintBatonOutIdx');
+    if (outIdxBytes.length > 1) {
+        throw new Error('mintBatonOutIdx must be at most 1 byte long');
+    }
+    if (outIdxBytes.length === 1) {
+        if (tokenType === SLP_NFT1_CHILD) {
+            throw new Error('SLP_NFT1_CHILD cannot have a mint baton');
+        }
+        const mintBatonOutIdx = outIdxBytes[0];
+        if (mintBatonOutIdx < 2) {
+            throw new Error('mintBatonOutIdx must be at least 2');
+        }
+        return mintBatonOutIdx;
+    }
+    return undefined;
+}
+
+function nextTokenId(iter: ScriptOpIter): string {
+    const tokenIdBytes = nextBytesRequired(iter, 'tokenId');
+    if (tokenIdBytes.length !== TOKEN_ID_NUM_BYTES) {
+        throw new Error(
+            `tokenId must be exactly ${TOKEN_ID_NUM_BYTES} bytes long`,
+        );
+    }
+    return toHex(tokenIdBytes);
+}
+
+function nextSlpAtomsArray(iter: ScriptOpIter): bigint[] {
+    const atomsArray = [];
+    let bytes: Uint8Array | undefined = undefined;
+    while ((bytes = nextBytes(iter)) !== undefined) {
+        atomsArray.push(parseSlpAtoms(bytes));
+    }
+    if (atomsArray.length === 0) {
+        throw new Error('atomsArray cannot be empty');
+    }
+    if (atomsArray.length > SLP_MAX_SEND_OUTPUTS) {
+        throw new Error(
+            `atomsArray can at most be ${SLP_MAX_SEND_OUTPUTS} items long`,
+        );
+    }
+    return atomsArray;
+}
+
+function nextEnd(iter: ScriptOpIter, txType: string) {
+    if (iter.next() !== undefined) {
+        throw new Error(`Superfluous ${txType} bytes`);
+    }
+}
+
+function parseSlpAtoms(bytes: Uint8Array): bigint {
+    if (bytes.length !== SLP_ATOMS_NUM_BYTES) {
+        throw new Error(
+            `SLP atoms must be exactly ${SLP_ATOMS_NUM_BYTES} bytes long`,
+        );
+    }
+    let number = 0n;
+    for (let i = 0; i < SLP_ATOMS_NUM_BYTES; ++i) {
+        number <<= 8n;
+        number |= BigInt(bytes[i]);
+    }
+    return number;
+}
diff --git a/modules/ecash-lib/src/token/slp.test.ts b/modules/ecash-lib/src/token/slp.test.ts
--- a/modules/ecash-lib/src/token/slp.test.ts
+++ b/modules/ecash-lib/src/token/slp.test.ts
@@ -4,7 +4,41 @@
 
 import { expect } from 'chai';
 
-import { slpBurn, slpGenesis, slpMint, slpMintVault, slpSend } from './slp.js';
+import {
+    SLP_FUNGIBLE,
+    SLP_MINT_VAULT,
+    SLP_NFT1_CHILD,
+    SLP_NFT1_GROUP,
+    slpBurn,
+    slpGenesis,
+    slpMint,
+    slpMintVault,
+    slpSend,
+} from './slp.js';
+import {
+    parseSlp,
+    SlpBurn,
+    SlpGenesis,
+    SlpMintClassic,
+    SlpMintVault,
+    SlpData,
+    SlpSend,
+    SlpUnknown,
+} from './slp.parse.js';
+import { Script } from '../script.js';
+import { fromHex } from '../io/hex.js';
+import { strToBytes } from '../io/str.js';
+
+const KNOWN_SLP_TOKEN_TYPES = new Set([
+    SLP_FUNGIBLE,
+    SLP_MINT_VAULT,
+    SLP_NFT1_CHILD,
+    SLP_NFT1_GROUP,
+]);
+
+function parseSlpBytes(bytes: number[] | Uint8Array): SlpData | undefined {
+    return parseSlp(new Script(new Uint8Array(bytes)));
+}
 
 describe('SLP', () => {
     it('SLP invalid usage', () => {
@@ -61,4 +95,633 @@
             'Atoms out of range: 18446744073709551616',
         );
     });
+    it('SLP parse Missing OP_RETURN', () => {
+        expect(parseSlp(new Script())).to.equal(undefined);
+        expect(parseSlp(new Script(fromHex('00')))).to.equal(undefined);
+        expect(parseSlp(new Script(fromHex('69')))).to.equal(undefined);
+    });
+    it('SLP parse Missing "SLP\\0"', () => {
+        expect(parseSlp(new Script(fromHex('6a')))).to.equal(undefined);
+        expect(parseSlp(new Script(strToBytes('\x6a\x03SLP')))).to.equal(
+            undefined,
+        );
+        expect(parseSlp(new Script(strToBytes('\x6a\x04SLP\x01')))).to.equal(
+            undefined,
+        );
+    });
+    it('SLP parse Missing token type', () => {
+        expect(() =>
+            parseSlp(new Script(strToBytes('\x6a\x04SLP\0'))),
+        ).to.throw('Missing tokenType');
+    });
+    it('SLP parse Bad token type', () => {
+        expect(() =>
+            parseSlp(new Script(strToBytes('\x6a\x04SLP\0\0'))),
+        ).to.throw('SLP only supports push-ops');
+        expect(() =>
+            parseSlp(new Script(strToBytes('\x6a\x04SLP\0\x51'))),
+        ).to.throw('SLP only supports push-ops');
+        expect(() =>
+            parseSlp(new Script(strToBytes('\x6a\x04SLP\0\x69'))),
+        ).to.throw('SLP only supports push-ops');
+        expect(() =>
+            parseSlp(new Script(strToBytes('\x6a\x04SLP\0\x4c\0'))),
+        ).to.throw('tokenType must be exactly 1 byte');
+        expect(() =>
+            parseSlp(new Script(strToBytes('\x6a\x04SLP\0\x02xx'))),
+        ).to.throw('tokenType must be exactly 1 byte');
+    });
+    it('SLP parse Missing txType / Unknown tokenType', () => {
+        for (let tokenType = 0; tokenType <= 0xff; ++tokenType) {
+            const script = new Script(
+                new Uint8Array([...strToBytes('\x6a\x04SLP\0\x01'), tokenType]),
+            );
+            if (KNOWN_SLP_TOKEN_TYPES.has(tokenType)) {
+                expect(() => parseSlp(script)).to.throw('Missing txType');
+            } else {
+                expect(parseSlp(script)).to.deep.equal({
+                    txType: 'UNKNOWN',
+                    tokenType,
+                } as SlpUnknown);
+            }
+        }
+    });
+    it('SLP parse Unknown txType', () => {
+        for (const tokenType of KNOWN_SLP_TOKEN_TYPES) {
+            const prefix = [...strToBytes('\x6a\x04SLP\0\x01'), tokenType];
+            expect(() => parseSlpBytes([...prefix, 0])).to.throw(
+                'SLP only supports push-ops',
+            );
+            expect(() =>
+                parseSlpBytes([...prefix, ...strToBytes('\x4c\0')]),
+            ).to.throw('Unknown txType');
+            expect(() =>
+                parseSlpBytes([...prefix, ...strToBytes('\x01x')]),
+            ).to.throw('Unknown txType');
+            expect(() =>
+                parseSlpBytes([...prefix, ...strToBytes('\x07UNKNOWN')]),
+            ).to.throw('Unknown txType');
+        }
+    });
+    it('SLP parse GENESIS missing info', () => {
+        for (const tokenType of KNOWN_SLP_TOKEN_TYPES) {
+            const prefix = [
+                ...strToBytes('\x6a\x04SLP\0\x01'),
+                tokenType,
+                ...strToBytes('\x07GENESIS'),
+            ];
+            expect(() => parseSlpBytes([...prefix])).to.throw(
+                'Missing tokenTicker',
+            );
+            expect(() => parseSlpBytes([...prefix, 0])).to.throw(
+                'SLP only supports push-ops',
+            );
+            expect(() => parseSlpBytes([...prefix, 1, 0])).to.throw(
+                'Missing tokenName',
+            );
+            expect(() => parseSlpBytes([...prefix, 0x4c, 0])).to.throw(
+                'Missing tokenName',
+            );
+            expect(() => parseSlpBytes([...prefix, 1, 0, 0])).to.throw(
+                'SLP only supports push-ops',
+            );
+            expect(() => parseSlpBytes([...prefix, 1, 0, 1, 0])).to.throw(
+                'Missing url',
+            );
+            expect(() => parseSlpBytes([...prefix, 1, 0, 0x4c, 0])).to.throw(
+                'Missing url',
+            );
+            expect(() => parseSlpBytes([...prefix, 1, 0, 1, 0, 0])).to.throw(
+                'SLP only supports push-ops',
+            );
+            expect(() => parseSlpBytes([...prefix, 1, 0, 1, 0, 1, 0])).to.throw(
+                'Missing hash',
+            );
+            expect(() =>
+                parseSlpBytes([...prefix, 1, 0, 1, 0, 0x4c, 0]),
+            ).to.throw('Missing hash');
+            expect(() =>
+                parseSlpBytes([...prefix, 1, 0, 1, 0, 1, 0, 0]),
+            ).to.throw('SLP only supports push-ops');
+            expect(() =>
+                parseSlpBytes([...prefix, 1, 0, 1, 0, 1, 0, 1, 0]),
+            ).to.throw('hash must be either 0 or 32 bytes');
+            expect(() =>
+                parseSlpBytes([...prefix, 1, 0, 1, 0, 1, 0, 0x4c, 0]),
+            ).to.throw('Missing decimals');
+            expect(() =>
+                parseSlpBytes([
+                    ...prefix,
+                    ...[1, 0, 1, 0, 1, 0, 32],
+                    ...new Uint8Array(32),
+                ]),
+            ).to.throw('Missing decimals');
+            expect(() =>
+                parseSlpBytes([...prefix, 1, 0, 1, 0, 1, 0, 0x4c, 0, 0]),
+            ).to.throw('SLP only supports push-ops');
+            expect(() =>
+                parseSlpBytes([...prefix, 1, 0, 1, 0, 1, 0, 0x4c, 0, 0x4c, 0]),
+            ).to.throw('decimals must be exactly 1 byte');
+            expect(() =>
+                parseSlpBytes([
+                    ...prefix,
+                    ...[1, 0, 1, 0, 1, 0, 0x4c, 0, 2, 99, 99],
+                ]),
+            ).to.throw('decimals must be exactly 1 byte');
+            expect(() =>
+                parseSlpBytes([...prefix, 1, 0, 1, 0, 1, 0, 0x4c, 0, 1, 10]),
+            ).to.throw('decimals must be at most 9');
+            expect(() =>
+                parseSlpBytes([...prefix, 1, 0, 1, 0, 1, 0, 0x4c, 0, 1, 9, 0]),
+            ).to.throw('SLP only supports push-ops');
+        }
+    });
+    it('SLP parse GENESIS MINT VAULT', () => {
+        const prefix = strToBytes(
+            '\x6a\x04SLP\0\x01\x02\x07GENESIS\x01\0\x01\0\x01\0\x4c\0\x01\x04',
+        );
+        expect(() => parseSlpBytes(prefix)).to.throw(
+            'Missing mintVaultScripthash',
+        );
+        expect(() =>
+            parseSlpBytes([...prefix, 19, ...new Uint8Array(19)]),
+        ).to.throw('mintVaultScripthash must be exactly 20 bytes long');
+        expect(() =>
+            parseSlpBytes([...prefix, 20, ...new Uint8Array(20)]),
+        ).to.throw('Missing initialAtoms');
+        expect(() =>
+            parseSlpBytes([...prefix, 20, ...new Uint8Array(20), 0]),
+        ).to.throw('SLP only supports push-ops');
+        expect(() =>
+            parseSlpBytes([...prefix, 20, ...new Uint8Array(20), ...[1, 0]]),
+        ).to.throw('SLP atoms must be exactly 8 bytes long');
+        expect(() =>
+            parseSlpBytes([
+                ...prefix,
+                20,
+                ...new Uint8Array(20),
+                ...[8, 0, 0, 0, 0, 0, 0, 0, 0],
+                0,
+            ]),
+        ).to.throw('Superfluous GENESIS bytes');
+
+        // Success
+        expect(
+            parseSlpBytes([
+                ...prefix,
+                20,
+                ...new Uint8Array(20),
+                ...[8, 0, 0, 0, 0, 0, 0, 0, 0],
+            ]),
+        ).to.deep.equal({
+            txType: 'GENESIS',
+            tokenType: SLP_MINT_VAULT,
+            genesisInfo: {
+                tokenTicker: '\0',
+                tokenName: '\0',
+                url: '\0',
+                hash: undefined,
+                mintVaultScripthash: '0000000000000000000000000000000000000000',
+                decimals: 4,
+            },
+            initialAtoms: 0n,
+            mintBatonOutIdx: undefined,
+        } as SlpGenesis);
+    });
+    it('SLP parse GENESIS MINT VAULT BUX', () => {
+        // BUX (0x02) 52b12c03466936e7e3b2dcfcff847338c53c611ba8ab74dd8e4dadf7ded12cf6
+        const buxScriptHex = `6a04534c500001020747454e45534953034255581642616467657220\
+556e6976657273616c20546f6b656e1368747470733a2f2f6275782e6469676974616c4c0001041408d6ed\
+f91c7b93d18306d3b8244587e43f11df4b080000000000000000`;
+        expect(parseSlp(new Script(fromHex(buxScriptHex)))).to.deep.equal({
+            txType: 'GENESIS',
+            tokenType: SLP_MINT_VAULT,
+            genesisInfo: {
+                tokenTicker: 'BUX',
+                tokenName: 'Badger Universal Token',
+                url: 'https://bux.digital',
+                hash: undefined,
+                mintVaultScripthash: '08d6edf91c7b93d18306d3b8244587e43f11df4b',
+                decimals: 4,
+            },
+            initialAtoms: 0n,
+            mintBatonOutIdx: undefined,
+        } as SlpGenesis);
+    });
+    it('SLP parse GENESIS Classic', () => {
+        for (const tokenType of [
+            SLP_FUNGIBLE,
+            SLP_NFT1_CHILD,
+            SLP_NFT1_GROUP,
+        ]) {
+            const prefix = [
+                ...strToBytes('\x6a\x04SLP\0\x01'),
+                tokenType,
+                ...strToBytes('\x07GENESIS\x01\0\x01\0\x01\0\x4c\0\x01\x04'),
+            ];
+            expect(() => parseSlpBytes(prefix)).to.throw(
+                'Missing mintBatonOutIdx',
+            );
+            expect(() => parseSlpBytes([...prefix, 0])).to.throw(
+                'SLP only supports push-ops',
+            );
+            expect(() => parseSlpBytes([...prefix, 0x4c, 2, 0, 0])).to.throw(
+                'mintBatonOutIdx must be at most 1 byte long',
+            );
+            for (const i of [0, 1]) {
+                expect(() => parseSlpBytes([...prefix, 1, i])).to.throw(
+                    tokenType === SLP_NFT1_CHILD
+                        ? 'SLP_NFT1_CHILD cannot have a mint baton'
+                        : 'mintBatonOutIdx must be at least 2',
+                );
+            }
+            expect(() => parseSlpBytes([...prefix, 0x4c, 0])).to.throw(
+                'Missing initialAtoms',
+            );
+            expect(() => parseSlpBytes([...prefix, 0x4c, 0, 0])).to.throw(
+                'SLP only supports push-ops',
+            );
+            expect(() => parseSlpBytes([...prefix, 0x4c, 0, 1, 0])).to.throw(
+                'SLP atoms must be exactly 8 bytes long',
+            );
+            expect(() =>
+                parseSlpBytes([
+                    ...prefix,
+                    ...[0x4c, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 99],
+                ]),
+            ).to.throw('Superfluous GENESIS bytes');
+
+            // Success (no mint baton)
+            expect(
+                parseSlpBytes([...prefix, 0x4c, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0]),
+            ).to.deep.equal({
+                txType: 'GENESIS',
+                tokenType,
+                genesisInfo: {
+                    tokenTicker: '\0',
+                    tokenName: '\0',
+                    url: '\0',
+                    hash: undefined,
+                    mintVaultScripthash: undefined,
+                    decimals: 4,
+                },
+                initialAtoms: 0n,
+                mintBatonOutIdx: undefined,
+            } as SlpGenesis);
+
+            // With mint baton
+            const withMintBaton = [...prefix, 1, 2, 8, 1, 2, 3, 4, 5, 6, 7, 8];
+            if (tokenType !== SLP_NFT1_CHILD) {
+                expect(parseSlpBytes(withMintBaton)).to.deep.equal({
+                    txType: 'GENESIS',
+                    tokenType,
+                    genesisInfo: {
+                        tokenTicker: '\0',
+                        tokenName: '\0',
+                        url: '\0',
+                        hash: undefined,
+                        mintVaultScripthash: undefined,
+                        decimals: 4,
+                    },
+                    initialAtoms: 0x0102030405060708n,
+                    mintBatonOutIdx: 2,
+                } as SlpGenesis);
+            } else {
+                expect(() => parseSlpBytes(withMintBaton)).to.throw(
+                    'SLP_NFT1_CHILD cannot have a mint baton',
+                );
+            }
+
+            // Success, with hash
+            const hash =
+                '2908560932487503948670398463abcefd3947562938923659246757456abcde';
+            expect(
+                parseSlpBytes([
+                    ...strToBytes('\x6a\x04SLP\0\x01'),
+                    tokenType,
+                    ...strToBytes('\x07GENESIS\x01\0\x01\0\x01\0\x20'),
+                    ...fromHex(hash),
+                    ...[1, 4, 0x4c, 0, 8, 0, 0, 0, 0, 0, 0, 0x56, 0x78],
+                ]),
+            ).to.deep.equal({
+                txType: 'GENESIS',
+                tokenType,
+                genesisInfo: {
+                    tokenTicker: '\0',
+                    tokenName: '\0',
+                    url: '\0',
+                    hash,
+                    mintVaultScripthash: undefined,
+                    decimals: 4,
+                },
+                initialAtoms: 0x5678n,
+                mintBatonOutIdx: undefined,
+            } as SlpGenesis);
+        }
+    });
+    it('SLP parse GENESIS SLP FUNGIBLE BUX', () => {
+        // BUX (0x01) 7e7dacd72dcdb14e00a03dd3aff47f019ed51a6f1f4e4f532ae50692f62bc4e5
+        const buxScriptHex = `6a04534c500001010747454e45534953034255581642616467657220\
+556e6976657273616c20546f6b656e1368747470733a2f2f6275782e6469676974616c4c00010401020800\
+00000000000000`;
+        expect(parseSlp(new Script(fromHex(buxScriptHex)))).to.deep.equal({
+            txType: 'GENESIS',
+            tokenType: SLP_FUNGIBLE,
+            genesisInfo: {
+                tokenTicker: 'BUX',
+                tokenName: 'Badger Universal Token',
+                url: 'https://bux.digital',
+                hash: undefined,
+                mintVaultScripthash: undefined,
+                decimals: 4,
+            },
+            initialAtoms: 0n,
+            mintBatonOutIdx: 2,
+        } as SlpGenesis);
+    });
+    it('SLP parse MINT bad token ID', () => {
+        for (const tokenType of KNOWN_SLP_TOKEN_TYPES) {
+            const prefix = [
+                ...strToBytes('\x6a\x04SLP\0\x01'),
+                tokenType,
+                ...strToBytes('\x04MINT'),
+            ];
+            expect(() => parseSlpBytes(prefix)).to.throw('Missing tokenId');
+            expect(() => parseSlpBytes([...prefix, 0])).to.throw(
+                'SLP only supports push-ops',
+            );
+            expect(() => parseSlpBytes([...prefix, 0x4c, 0])).to.throw(
+                'tokenId must be exactly 32 bytes long',
+            );
+            expect(() =>
+                parseSlpBytes([...prefix, 31, ...new Uint8Array(31)]),
+            ).to.throw('tokenId must be exactly 32 bytes long');
+        }
+    });
+    it('SLP parse MINT VAULT', () => {
+        const prefix = [
+            ...strToBytes('\x6a\x04SLP\0\x01\x02\x04MINT\x20'),
+            ...new Uint8Array(32),
+        ];
+        expect(() => parseSlpBytes(prefix)).to.throw(
+            'atomsArray cannot be empty',
+        );
+        expect(() => parseSlpBytes([...prefix, 0])).to.throw(
+            'SLP only supports push-ops',
+        );
+        expect(() => parseSlpBytes([...prefix, 0x4c, 0])).to.throw(
+            'SLP atoms must be exactly 8 bytes long',
+        );
+        expect(() =>
+            parseSlpBytes([...prefix, 7, 0, 0, 0, 0, 0, 0, 0]),
+        ).to.throw('SLP atoms must be exactly 8 bytes long');
+        expect(() =>
+            parseSlpBytes([...prefix, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0x4c, 0]),
+        ).to.throw('SLP atoms must be exactly 8 bytes long');
+        for (let num = 1; num <= 30; ++num) {
+            const scriptBytes = [...prefix];
+            const expectedAtomsArray = [];
+            for (let idx = 0; idx < num; ++idx) {
+                scriptBytes.push(...[8, 0, 0, 0, 0, 0, 0, 0, idx]);
+                expectedAtomsArray.push(BigInt(idx));
+            }
+            if (num <= 19) {
+                expect(parseSlpBytes(scriptBytes)).to.deep.equal({
+                    txType: 'MINT',
+                    tokenType: SLP_MINT_VAULT,
+                    tokenId:
+                        '0000000000000000000000000000000000000000000000000000000000000000',
+                    additionalAtomsArray: expectedAtomsArray,
+                } as SlpMintVault);
+            } else {
+                expect(() => parseSlpBytes(scriptBytes)).to.throw(
+                    'atomsArray can at most be 19 items long',
+                );
+            }
+        }
+    });
+    it('SLP parse MINT VAULT BUX', () => {
+        // BUX tx 09e14665aa2980db8001a04ec350ef7cc2b77094efcd634c62dadf0940870912
+        const buxScriptHex = `6a04534c50000102044d494e542052b12c03466936e7e3b2dcfcff84\
+7338c53c611ba8ab74dd8e4dadf7ded12cf60800000000000007d0080000000000000fa008000000000000\
+9c40`;
+        expect(parseSlp(new Script(fromHex(buxScriptHex)))).to.deep.equal({
+            txType: 'MINT',
+            tokenType: SLP_MINT_VAULT,
+            tokenId:
+                '52b12c03466936e7e3b2dcfcff847338c53c611ba8ab74dd8e4dadf7ded12cf6',
+            additionalAtomsArray: [2000n, 4000n, 40000n],
+        } as SlpMintVault);
+    });
+    it('SLP parse MINT FUNGIBLE and NFT GROUP', () => {
+        for (const tokenType of [SLP_FUNGIBLE, SLP_NFT1_GROUP]) {
+            const prefix = [
+                ...strToBytes('\x6a\x04SLP\0\x01'),
+                tokenType,
+                ...strToBytes('\x04MINT\x20'),
+                ...new Uint8Array(32),
+            ];
+            expect(() => parseSlpBytes(prefix)).to.throw(
+                'Missing mintBatonOutIdx',
+            );
+            expect(() => parseSlpBytes([...prefix, 0x4c, 2, 0, 0])).to.throw(
+                'mintBatonOutIdx must be at most 1 byte long',
+            );
+            for (const i of [0, 1]) {
+                expect(() => parseSlpBytes([...prefix, 1, i])).to.throw(
+                    'mintBatonOutIdx must be at least 2',
+                );
+            }
+            expect(() => parseSlpBytes([...prefix, 0x4c, 0])).to.throw(
+                'Missing additionalAtoms',
+            );
+            expect(() => parseSlpBytes([...prefix, 0x4c, 0, 0])).to.throw(
+                'SLP only supports push-ops',
+            );
+            expect(() => parseSlpBytes([...prefix, 0x4c, 0, 1, 0])).to.throw(
+                'SLP atoms must be exactly 8 bytes long',
+            );
+            expect(() =>
+                parseSlpBytes([
+                    ...prefix,
+                    ...[0x4c, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 99],
+                ]),
+            ).to.throw('Superfluous MINT bytes');
+
+            // Success (no mint baton)
+            expect(
+                parseSlpBytes([...prefix, 0x4c, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0]),
+            ).to.deep.equal({
+                txType: 'MINT',
+                tokenType,
+                tokenId:
+                    '0000000000000000000000000000000000000000000000000000000000000000',
+                additionalAtoms: 0n,
+                mintBatonOutIdx: undefined,
+            } as SlpMintClassic);
+
+            // With mint baton
+            expect(
+                parseSlpBytes([...prefix, 1, 2, 8, 1, 2, 3, 4, 5, 6, 7, 8]),
+            ).to.deep.equal({
+                txType: 'MINT',
+                tokenType,
+                tokenId:
+                    '0000000000000000000000000000000000000000000000000000000000000000',
+                additionalAtoms: 0x0102030405060708n,
+                mintBatonOutIdx: 2,
+            } as SlpMintClassic);
+        }
+    });
+    it('SLP parse MINT FUNGIBLE BUX', () => {
+        // BUX tx 459a8dbf3b31750ddaaed4d2c6a12fb42ef1b83fc0f67175f43332962932aa7d
+        const buxScriptHex = `6a04534c50000101044d494e54207e7dacd72dcdb14e00a03dd3aff4\
+7f019ed51a6f1f4e4f532ae50692f62bc4e501020800000000000030d4`;
+        expect(parseSlp(new Script(fromHex(buxScriptHex)))).to.deep.equal({
+            txType: 'MINT',
+            tokenType: SLP_FUNGIBLE,
+            tokenId:
+                '7e7dacd72dcdb14e00a03dd3aff47f019ed51a6f1f4e4f532ae50692f62bc4e5',
+            additionalAtoms: 12500n,
+            mintBatonOutIdx: 2,
+        } as SlpMintClassic);
+    });
+    it('SLP parse MINT NFT CHILD', () => {
+        expect(() =>
+            parseSlpBytes([
+                ...strToBytes('\x6a\x04SLP\0\x01\x41\x04MINT\x20'),
+                ...new Uint8Array(32),
+            ]),
+        ).to.throw('SLP_NFT1_CHILD cannot have MINT transactions');
+    });
+    it('SLP parse SEND bad token ID', () => {
+        for (const tokenType of KNOWN_SLP_TOKEN_TYPES) {
+            const prefix = [
+                ...strToBytes('\x6a\x04SLP\0\x01'),
+                tokenType,
+                ...strToBytes('\x04SEND'),
+            ];
+            expect(() => parseSlpBytes(prefix)).to.throw('Missing tokenId');
+            expect(() => parseSlpBytes([...prefix, 0])).to.throw(
+                'SLP only supports push-ops',
+            );
+            expect(() => parseSlpBytes([...prefix, 0x4c, 0])).to.throw(
+                'tokenId must be exactly 32 bytes long',
+            );
+            expect(() =>
+                parseSlpBytes([...prefix, 31, ...new Uint8Array(31)]),
+            ).to.throw('tokenId must be exactly 32 bytes long');
+        }
+    });
+    it('SLP parse SEND', () => {
+        for (const tokenType of KNOWN_SLP_TOKEN_TYPES) {
+            const prefix = [
+                ...strToBytes('\x6a\x04SLP\0\x01'),
+                tokenType,
+                ...strToBytes('\x04SEND\x20'),
+                ...new Uint8Array(32),
+            ];
+            expect(() => parseSlpBytes(prefix)).to.throw(
+                'atomsArray cannot be empty',
+            );
+            expect(() => parseSlpBytes([...prefix, 0])).to.throw(
+                'SLP only supports push-ops',
+            );
+            expect(() => parseSlpBytes([...prefix, 0x4c, 0])).to.throw(
+                'SLP atoms must be exactly 8 bytes long',
+            );
+            expect(() =>
+                parseSlpBytes([...prefix, 7, 0, 0, 0, 0, 0, 0, 0]),
+            ).to.throw('SLP atoms must be exactly 8 bytes long');
+            expect(() =>
+                parseSlpBytes([...prefix, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0x4c, 0]),
+            ).to.throw('SLP atoms must be exactly 8 bytes long');
+            for (let num = 1; num <= 30; ++num) {
+                const scriptBytes = [...prefix];
+                const expectedAtomsArray = [];
+                for (let idx = 0; idx < num; ++idx) {
+                    scriptBytes.push(...[8, 0, 0, 0, 0, 0, 0, 0, idx]);
+                    expectedAtomsArray.push(BigInt(idx));
+                }
+                if (num <= 19) {
+                    expect(parseSlpBytes(scriptBytes)).to.deep.equal({
+                        txType: 'SEND',
+                        tokenType,
+                        tokenId:
+                            '0000000000000000000000000000000000000000000000000000000000000000',
+                        sendAtomsArray: expectedAtomsArray,
+                    } as SlpSend);
+                } else {
+                    expect(() => parseSlpBytes(scriptBytes)).to.throw(
+                        'atomsArray can at most be 19 items long',
+                    );
+                }
+            }
+        }
+    });
+    it('SLP parse BURN bad token ID', () => {
+        for (const tokenType of KNOWN_SLP_TOKEN_TYPES) {
+            const prefix = [
+                ...strToBytes('\x6a\x04SLP\0\x01'),
+                tokenType,
+                ...strToBytes('\x04BURN'),
+            ];
+            expect(() => parseSlpBytes(prefix)).to.throw('Missing tokenId');
+            expect(() => parseSlpBytes([...prefix, 0])).to.throw(
+                'SLP only supports push-ops',
+            );
+            expect(() => parseSlpBytes([...prefix, 0x4c, 0])).to.throw(
+                'tokenId must be exactly 32 bytes long',
+            );
+            expect(() =>
+                parseSlpBytes([...prefix, 31, ...new Uint8Array(31)]),
+            ).to.throw('tokenId must be exactly 32 bytes long');
+        }
+    });
+    it('SLP parse BURN', () => {
+        for (const tokenType of KNOWN_SLP_TOKEN_TYPES) {
+            const prefix = [
+                ...strToBytes('\x6a\x04SLP\0\x01'),
+                tokenType,
+                ...strToBytes('\x04BURN\x20'),
+                ...new Uint8Array(32),
+            ];
+            expect(() => parseSlpBytes(prefix)).to.throw('Missing burnAtoms');
+            expect(() => parseSlpBytes([...prefix, 0])).to.throw(
+                'SLP only supports push-ops',
+            );
+            expect(() => parseSlpBytes([...prefix, 0x4c, 0])).to.throw(
+                'SLP atoms must be exactly 8 bytes long',
+            );
+            expect(() => parseSlpBytes([...prefix, 1, 0])).to.throw(
+                'SLP atoms must be exactly 8 bytes long',
+            );
+            expect(() =>
+                parseSlpBytes([...prefix, ...[8, 0, 0, 0, 0, 0, 0, 0, 0, 99]]),
+            ).to.throw('Superfluous BURN bytes');
+
+            // Success (no mint baton)
+            expect(
+                parseSlpBytes([...prefix, 8, 1, 2, 3, 4, 5, 6, 7, 8]),
+            ).to.deep.equal({
+                txType: 'BURN',
+                tokenType,
+                tokenId:
+                    '0000000000000000000000000000000000000000000000000000000000000000',
+                burnAtoms: 0x0102030405060708n,
+            } as SlpBurn);
+        }
+    });
+    it('SLP parse BURN SLP FUNGIBLE BUX', () => {
+        // BUX tx 94006ad05803922d743a44a51145c13d91826c7e97ffbe8cb0c994653166762e
+        const buxScriptHex = `6a04534c50000101044255524e207e7dacd72dcdb14e00a03dd3aff4\
+7f019ed51a6f1f4e4f532ae50692f62bc4e5080000000001481060`;
+        expect(parseSlp(new Script(fromHex(buxScriptHex)))).to.deep.equal({
+            txType: 'BURN',
+            tokenType: SLP_FUNGIBLE,
+            tokenId:
+                '7e7dacd72dcdb14e00a03dd3aff47f019ed51a6f1f4e4f532ae50692f62bc4e5',
+            burnAtoms: 21500000n,
+        } as SlpBurn);
+    });
 });
diff --git a/modules/ecash-lib/src/token/slp.ts b/modules/ecash-lib/src/token/slp.ts
--- a/modules/ecash-lib/src/token/slp.ts
+++ b/modules/ecash-lib/src/token/slp.ts
@@ -10,7 +10,9 @@
 import { BURN, GENESIS, GenesisInfo, MINT, SEND } from './common.js';
 
 /** LOKAD ID for SLP */
-export const SLP_LOKAD_ID = strToBytes('SLP\0');
+export const SLP_LOKAD_ID_STR = 'SLP\0';
+/** LOKAD ID for SLP */
+export const SLP_LOKAD_ID = strToBytes(SLP_LOKAD_ID_STR);
 
 /** SLP fungible token type number */
 export const SLP_FUNGIBLE = 1;
@@ -21,6 +23,25 @@
 /** SLP NFT1 Group token type number */
 export const SLP_NFT1_GROUP = 0x81;
 
+/** How many bytes the GENESIS `hash` field must have (or 0) */
+export const SLP_GENESIS_HASH_NUM_BYTES = 32;
+
+/** How many bytes the GENESIS `mintVaultScripthash` field must have */
+export const SLP_MINT_VAULT_SCRIPTHASH_NUM_BYTES = 20;
+
+/** How many outputs a SEND can specify at most */
+export const SLP_MAX_SEND_OUTPUTS = 19;
+
+/** How many bytes every atoms amount has */
+export const SLP_ATOMS_NUM_BYTES = 8;
+
+/** Supported SLP token types */
+export type SlpTokenType =
+    | typeof SLP_FUNGIBLE
+    | typeof SLP_MINT_VAULT
+    | typeof SLP_NFT1_CHILD
+    | typeof SLP_NFT1_GROUP;
+
 /** Build an SLP GENESIS OP_RETURN, creating a new SLP token */
 export function slpGenesis(
     tokenType: number,