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.
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.
| Function | Purpose |
|---|---|
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.
| Constant | Bytes | What it measures |
|---|---|---|
KYBER_PUBLIC_KEY_SIZE | 1184 | ML-KEM-768 public key |
KYBER_SECRET_KEY_SIZE | 2400 | ML-KEM-768 secret key |
KYBER_CIPHERTEXT_SIZE | 1088 | Encapsulation ciphertext |
KYBER_SHARED_SECRET_SIZE | 32 | Shared secret |
META_ADDRESS_SIZE | 1218 | Serialized meta-address (version + 33-byte secp256k1 spending key + 1184-byte ML-KEM viewing key) |
VIEW_TAG_SIZE | 1 | View tag |
ETH_ADDRESS_SIZE | 20 | Stealth Ethereum address |
SUI_ADDRESS_SIZE | 32 | Stealth Sui address |
STEALTH_SECP256K1_PUBLIC_SIZE | 65 | Uncompressed secp256k1 public key |
STEALTH_ETH_PRIVATE_KEY_SIZE | 32 | Recovered Ethereum private key |
PLAINTEXT_METADATA_SIZE | 77 | Announcement metadata block, plaintext |
ENCRYPTED_METADATA_SIZE | 93 | Announcement metadata block, AES-256-GCM |
METADATA_TX_HASH_SIZE | 32 | Source-chain transaction hash field |
METADATA_AMOUNT_SIZE | 32 | Amount field (uint256) |
METADATA_SOURCE_CHAIN_ID_SIZE | 8 | Source 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;
}
}
| Code | Meaning |
|---|---|
NOT_INITIALIZED | Crypto called before initSpecterSdk() |
INVALID_KEY_SIZE | Key length does not match the expected size |
INVALID_CIPHERTEXT_SIZE | Ciphertext length is wrong |
INVALID_SHARED_SECRET_SIZE | Shared secret length is wrong |
INVALID_HEX | Input is not valid hex |
INVALID_META_ADDRESS | Meta-address failed to parse |
INVALID_METADATA_JSON | Metadata is not valid JSON |
INVALID_METADATA_SIZE | Metadata block is not the expected length |
INVALID_METADATA_FIELD | A metadata field is out of range |
INVALID_VIEW_TAG | View tag is out of range |
INVALID_API_RESPONSE | A remote response failed validation |
HTTP_ERROR | The trusted API returned a non-success status |
ENCAPSULATION_FAILED | Encapsulation failed inside WASM |
DECAPSULATION_FAILED | Decapsulation failed inside WASM |
STEALTH_DERIVATION_FAILED | Stealth derivation failed |
METADATA_DECRYPTION_FAILED | Metadata authentication tag did not verify |
WASM_LOAD_FAILED | The WebAssembly module could not load |
INTERNAL_ERROR | Unexpected 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).
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.