Skip to main content

SDK API Reference

Every public function is exported at the top level:

import * as Specter from '@specterpq/sdk';

This page is reference material. For a guided walkthrough, start with the quickstart. For patterns, see integration.

Note

Two layers exist on purpose. The high-level flow functions (createStealthPayment, scanAnnouncement) cover most apps. The low-level functions (encapsulate, decapsulate, deriveStealthKeys) are there when you need to control each step yourself.

Initialization

initSpecterSdk(opts?)

Loads and caches the WebAssembly module. Call it once before any other crypto. Safe to call repeatedly.

await initSpecterSdk();

// Browser override when you host the WASM yourself:
await initSpecterSdk({ wasmUrl: 'https://cdn.example.com/specter_wasm_bg.wasm' });

Key generation

generateKeysLocal()

Generates one ML-KEM-768 keypair. The public key is safe to log. The secret key is redacted from JSON and console output.

const kp = generateKeysLocal();
kp.publicKey; // safe to share
kp.secretKey; // secret-bearing, redacted

generateSpecterKeys()

Generates a recipient identity: a spending keypair and a viewing keypair.

const keys = generateSpecterKeys();
keys.spending.publicKey; // controls funds
keys.viewing.publicKey; // detects payments

specterKeysViewingPk(keys)

Reads the viewing public key from a full identity object.

const viewingPk = specterKeysViewingPk(keys);

Meta-address

metaAddressFromPublicKeys(spendingPk, viewingPk, metadata?)

Builds the canonical recipient meta-address from the two public keys, plus optional metadata.

const meta = metaAddressFromPublicKeys(
spending.publicKey,
viewing.publicKey,
{ description: 'Alice', avatar: 'ipfs://Qm...', createdAt: 1700000000 },
);

meta.hex; // publishable string
meta.bytes.length; // 1218
meta.address.version; // 2

parseMetaAddress(input)

Parses a serialized meta-address from its hex string or raw bytes.

const parsed = parseMetaAddress(meta.hex);
parsed.address.spendingPk;
parsed.address.viewingPk;

Key encapsulation (KEM)

These are the post-quantum core. The sender encapsulates to the viewing public key; the recipient decapsulates with the viewing secret key. Both sides end up with the same 32-byte shared secret.

encapsulate(publicKey)

Sender-side operation against the recipient viewing public key.

const enc = encapsulate(recipient.viewing.publicKey);
enc.ciphertext; // announce publicly
enc.sharedSecret; // secret-bearing, redacted

decapsulate(ciphertext, secretKey)

Recipient-side operation against the viewing secret key. Returns the same shared secret.

const sharedSecret = decapsulate(enc.ciphertext, recipient.viewing.secretKey);

View tags

The view tag is a one-byte filter, not encryption. It lets a recipient skip almost every announcement that is not theirs before doing real work. See view tags and scanning for the why.

computeViewTag(sharedSecret)

Returns the one-byte view tag as a number from 0 to 255.

const tag = computeViewTag(sharedSecret);

verifyViewTag(sharedSecret, expectedTag)

Returns true if the tag matches.

if (verifyViewTag(sharedSecret, incomingTag)) {
// candidate match, continue to full derivation
}

Stealth derivation

deriveStealthAddress(spendingPk, sharedSecret)

Derives the stealth Ethereum address, 0x plus 20 bytes.

const ethAddress = deriveStealthAddress(recipient.spending.publicKey, sharedSecret);

deriveStealthSuiAddress(spendingPk, sharedSecret)

Derives the stealth Sui address, 0x plus 32 bytes.

const suiAddress = deriveStealthSuiAddress(recipient.spending.publicKey, sharedSecret);

deriveStealthKeys(spendingSk, sharedSecret)

Derives the full spendable key material for wallet import. Needs the recipient's secret spending key — passing a public key throws INVALID_KEY_SIZE. (For a watch-only re-derivation of the addresses and stealth public key without the secret, use deriveStealthPublic(spendingPk, sharedSecret).)

const keys = deriveStealthKeys(recipient.spending.secretKey, sharedSecret);
keys.ethAddress;
keys.suiAddress;
keys.ethPrivateKey; // secret-bearing, redacted

High-level flow

These two functions wrap the steps above. Reach for them first.

createStealthPayment(metaAddress)

Sender helper. Parses the meta-address, encapsulates, derives the Ethereum and Sui stealth addresses, and computes the view tag.

const payment = createStealthPayment(meta.hex);
// { ephemeralCiphertext, viewTag, ethAddress, suiAddress }

scanAnnouncement(announcement, viewingKeys, spendingPublicKey[, spendingSecretKey])

Recipient helper for a single announcement. Detection needs only the viewing keys and the spending public key. Pass the spending secret key as the optional fourth argument to also recover the spendable stealthKeys; omit it for a watch-only scan (a match then returns result.detected addresses with no private key).

const result = scanAnnouncement(
{ ephemeralCiphertext: payment.ephemeralCiphertext, viewTag: payment.viewTag },
recipient.viewing,
recipient.spending.publicKey,
recipient.spending.secretKey, // optional: derive the spendable stealthKeys
);

if (result.isMatch) {
result.stealthKeys.ethAddress; // present because the secret key was supplied
result.stealthKeys.ethPrivateKey;
} else {
result.reason; // 'view_tag_mismatch' | 'address_mismatch'
}

scanAnnouncements(announcements, viewingKeys, spendingPublicKey[, spendingSecretKey])

Batch version. Returns an array of results. Same optional spending-secret argument as scanAnnouncement.

const results = scanAnnouncements(batch, recipient.viewing, recipient.spending.publicKey);
const matches = results.filter((r) => r.isMatch);

Announcement metadata

A sender can attach payment context to an announcement: the source-chain transaction hash, the amount, and the EIP-155 source chain id. These fields live in a fixed 77-byte block that encrypts to 93 bytes under AES-256-GCM. Byte 0 is the view tag and stays in the clear so scanners can filter without decrypting. The shared secret derived during encapsulation is the encryption key, so only the intended recipient can read the rest. This mirrors the hosted backend's registry behavior.

Most apps only need the two high-level functions.

sealAnnouncementMetadata(input, sharedSecret)

Sender-side. Encodes the fields, derives the view tag from the shared secret, and encrypts. Returns the 93-byte block as hex. The view tag is always taken from the shared secret, never from the caller.

const sealed = sealAnnouncementMetadata(
{ txHash: '0xabc…', amount: 100000000000000000n, sourceChainId: 42161 },
sharedSecret,
);

openAnnouncementMetadata(encrypted, sharedSecret)

Recipient-side. Decrypts the block and decodes the fields. Throws METADATA_DECRYPTION_FAILED when the authentication tag does not verify, which is the normal outcome for announcements that are not yours.

const meta = openAnnouncementMetadata(sealed, sharedSecret);
meta.viewTag;
meta.txHash; // undefined if the sender omitted it
meta.amount; // big-endian uint256 hex, or undefined
meta.sourceChainId; // EIP-155 id, or undefined

Low-level metadata functions

Reach for these when you need to handle the encoding and encryption steps separately.

FunctionPurpose
encodeAnnouncementMetadata(input)Pack { viewTag, txHash?, amount?, sourceChainId? } into the canonical 77-byte block. Absent fields are encoded as zeros.
decodeAnnouncementMetadata(raw)Parse a 77-byte block back into fields. All-zero fields decode to undefined.
encryptAnnouncementMetadata(plaintext, sharedSecret)AES-256-GCM encrypt a 77-byte block into the 93-byte form.
decryptAnnouncementMetadata(encrypted, sharedSecret)Decrypt a 93-byte block back to 77 bytes. Throws METADATA_DECRYPTION_FAILED on tag failure.

Sizes and constants

Import these for runtime checks and schema alignment instead of hardcoding numbers. The values match ML-KEM-768 as standardized in FIPS 203.

ConstantBytesWhat it measures
KYBER_PUBLIC_KEY_SIZE1184ML-KEM-768 public key
KYBER_SECRET_KEY_SIZE2400ML-KEM-768 secret key
KYBER_CIPHERTEXT_SIZE1088Encapsulation ciphertext
KYBER_SHARED_SECRET_SIZE32Shared secret
META_ADDRESS_SIZE1218Serialized meta-address (version + 33-byte secp256k1 spending key + 1184-byte ML-KEM viewing key)
VIEW_TAG_SIZE1View tag
ETH_ADDRESS_SIZE20Stealth Ethereum address
SUI_ADDRESS_SIZE32Stealth Sui address
STEALTH_SECP256K1_PUBLIC_SIZE65Uncompressed secp256k1 public key
STEALTH_ETH_PRIVATE_KEY_SIZE32Recovered Ethereum private key
PLAINTEXT_METADATA_SIZE77Announcement metadata block, plaintext
ENCRYPTED_METADATA_SIZE93Announcement metadata block, AES-256-GCM
METADATA_TX_HASH_SIZE32Source-chain transaction hash field
METADATA_AMOUNT_SIZE32Amount field (uint256)
METADATA_SOURCE_CHAIN_ID_SIZE8Source chain id field (big-endian u64)

PROTOCOL_VERSION carries the current protocol version (1).

import {
KYBER_PUBLIC_KEY_SIZE,
KYBER_CIPHERTEXT_SIZE,
META_ADDRESS_SIZE,
VIEW_TAG_SIZE,
PROTOCOL_VERSION,
} from '@specterpq/sdk';

Errors

Every error the SDK throws is a SpecterSdkError with a code, a category, a recoverable flag, and a message.

import { SpecterSdkError, encapsulate } from '@specterpq/sdk';

try {
encapsulate('0xdeadbeef' as never);
} catch (err) {
if (err instanceof SpecterSdkError) {
console.error(err.code, err.category, err.recoverable, err.message);
} else {
throw err;
}
}
CodeMeaning
NOT_INITIALIZEDCrypto called before initSpecterSdk()
INVALID_KEY_SIZEKey length does not match the expected size
INVALID_CIPHERTEXT_SIZECiphertext length is wrong
INVALID_SHARED_SECRET_SIZEShared secret length is wrong
INVALID_HEXInput is not valid hex
INVALID_META_ADDRESSMeta-address failed to parse
INVALID_METADATA_JSONMetadata is not valid JSON
INVALID_METADATA_SIZEMetadata block is not the expected length
INVALID_METADATA_FIELDA metadata field is out of range
INVALID_VIEW_TAGView tag is out of range
INVALID_API_RESPONSEA remote response failed validation
HTTP_ERRORThe trusted API returned a non-success status
ENCAPSULATION_FAILEDEncapsulation failed inside WASM
DECAPSULATION_FAILEDDecapsulation failed inside WASM
STEALTH_DERIVATION_FAILEDStealth derivation failed
METADATA_DECRYPTION_FAILEDMetadata authentication tag did not verify
WASM_LOAD_FAILEDThe WebAssembly module could not load
INTERNAL_ERRORUnexpected internal failure

Type notes

  • Cryptographic strings use branded hex aliases such as KyberPublicKeyHex.
  • Most low-level functions accept either branded hex or a Uint8Array.
  • Prefer the high-level functions for app integration unless you need step-by-step control.

Trusted HTTP client

The SDK is local first. The functions below only run when you build a client against an API you trust. They are documented in integration patterns, and their trust implications are covered in the security model.

import { createSpecterApiClient } from '@specterpq/sdk';

const api = createSpecterApiClient({
baseUrl: 'https://api.example.com',
headers: { authorization: `Bearer ${token}` },
});

Available methods: createStealthPaymentRemote(metaAddress) and publishAnnouncement(input).

Warning

The HTTP client is public-data only — it never transmits secret keys. The former generateKeysRemote() and scanRemote() methods were removed in v1.0.0 because they could send secret material over the wire. Generate keys with generateSpecterKeys() and scan with scanAnnouncement(); both run entirely on the device.