Issuer quickstart
The guide explains the shortest path from invite data to first AMP2 wallet registration for an issuer. The issuer creates an authenticated SDK session and receives a WID.
This guide walks you through the shortest path from receiving invite data to registering your first wallet in AMP2. By the end, you will have an authenticated SDK session and a wallet ID ready for asset operations.
Prerequisites
- Node.js 18+
- npm access to
@blockstream/cryptic,@blockstream/ecs-registry, and@blockstream/amp-registry(see Environments for access details) - Invite data from Blockstream (device UUID, invite string, one time key, server keys, API URL)
1. Install the SDK
npm install @blockstream/cryptic @blockstream/ecs-registry @blockstream/amp-registryIf the packages are hosted on a private registry, configure your .npmrc first:
@blockstream:registry=https://enterprise.blockstream.com/integrations/npm
//enterprise.blockstream.com/integrations/npm/:_authToken=${NPM_TOKEN}
2. Generate device key pairs
Every AMP2 device needs an RSA key pair (for encryption) and an ECDSA P-256 key pair (for signing). The @blockstream/cryptic package provides a helper:
import { generateUserKeyPairs } from '@blockstream/cryptic/utils';
const deviceKeys = await generateUserKeyPairs();
// deviceKeys.rsa.privateKeyPem - Promise<string> (RSA-2048 PKCS#8 PEM)
// deviceKeys.rsa.publicKeyPem - Promise<string> (RSA-2048 SPKI PEM)
// deviceKeys.ecdsa.privateKeyHex - ECDSA P-256 private key (hex string)
// deviceKeys.ecdsa.publicKeyHex - ECDSA P-256 compressed public key (hex string)These keys are used to establish the COSE encryption envelope for all subsequent requests. Store the private keys securely; they are never sent to AMP2.
3. Create a one time broadcaster with mTLS
The one time broadcaster uses the invite's one time private key to authenticate the initial authorization request. This key is consumed during activation and cannot be reused. The transport flow is:
- Build the encrypted payload with
blockstream.request(...) - Provision or load an mTLS client identity in Node
- Send the
/requestbody through a broadcasterrequestCallbackthat usesmtlsRequest(...) - Parse the response with
blockstream.parse(...)
import { Buffer } from 'buffer'
import { Blockstream } from '@blockstream/cryptic'
import { Broadcaster } from '@blockstream/cryptic/broadcaster'
import { provisionMtlsClientCertificate, PROD_CA_CERT_PEM, buildMtlsSecret, MtlsProfile, MtlsSecretType } from '@blockstream/cryptic/mtls'
import { mtlsRequest } from '@blockstream/cryptic/mtls/node'
const otBlockstream = new Blockstream(
rsaPrivateKeyPem,
Buffer.from(oneTimePrivateKey, 'hex'),
deviceUuid,
serverRsaPublicKeyPem,
Buffer.from(serverEcdsaPublicKeyHex, 'hex'),
)
const ecdsaPublicKey = Buffer.from(
p256.getPublicKey(
new Uint8Array(Buffer.from(oneTimePrivateKey, 'hex')),
true,
),
)
const secret = buildMtlsSecret(ecdsaPublicKey)
const identity = await provisionMtlsClientCertificate('https://enterprise.blockstream.com', {
profile: MtlsProfile.MtlsClientV1,
deviceUuid,
secret,
secretType: MtlsSecretType.PubKey,
})
const otBroadcaster = new Broadcaster(ECS_BASE_URL, otBlockstream, async request => {
const response = await mtlsRequest({
url: request.url,
method: request.method,
headers: request.headers,
body: request.body,
certPem: identity.certificatePem,
keyPem: identity.privateKeyPem,
caPem: PROD_CA_CERT_PEM,
})
return {
status: response.status,
body: response.body,
}
})Parameters:
rsaPrivateKeyPem: your RSA private key (awaitdeviceKeys.rsa.privateKeyPem), used to decrypt AMP2 responses.oneTimePrivateKey: the hex encoded key from the invite, used to sign this first request only.device_uuid: the unique device identifier from the invite.SERVER_RSA_PUBLIC_KEY_PEM: AMP2's RSA public key (from invite data).SERVER_ECDSA_PUBLIC_KEY: AMP2's ECDSA public key as compressed hex (same as in invite data); decode withBuffer.from(..., 'hex'), notnew Uint8Array(string).
Notes:
- Number of mTLS certificates are strictly limited (3 per month). Store them securely and reuse until they expire.
- Provisioning (
@blockstream/cryptic/mtls) is browser-safe; the client-certificate request transportmtlsRequest(@blockstream/cryptic/mtls/node) is Node-only. - In Electron, do the mTLS transport in main or preload, not in page JS.
- mTLS only changes the TLS handshake. The ECS request body is still the same encrypted COSE payload.
- If you need per-request variation,
requestCallbackis also available inBroadcastOptionson the standalone broadcaster helpers.
The browser-safe / Node split shown above lands in @blockstream/cryptic 1.0.4. On earlier versions the entire mTLS API was a single Node-only @blockstream/cryptic/mtls entrypoint — import everything (including mtlsRequest) from there:
import { mtlsRequest, provisionMtlsClientCertificate, PROD_CA_CERT_PEM, buildMtlsSecret, MtlsProfile, MtlsSecretType } from '@blockstream/cryptic/mtls'4. Authorize the device
The authorize call activates your account and registers your device keys with AMP2. After this, the one time key is consumed.
The authorize endpoint is an ECS endpoint. Use the ecsResources action map from @blockstream/ecs-registry:
import type { ecsResources } from '@blockstream/ecs-registry'
const authorizePayload = await otBroadcaster.processBroadcast<
typeof ecsResources.auth.authorize
>(
{
action: 'add',
resource: '/account-management/authorize',
details: {
invite_string: inviteInfo.invite_string!,
device_name: 'Issuer Backend',
password: 'StrongPassword123!',
device_signature_pubkey: deviceKeys.ecdsa.publicKeyHex,
device_encryption_pubkey: await deviceKeys.rsa.publicKeyPem,
},
},
oneTimeBroadcaster,
);What this does:
- Consumes the invite string (single use).
- Sets the account password.
- Registers your ECDSA public key (for future request signature verification).
- Registers your RSA public key (for encrypting responses to you).
If authorization fails, check that the invite string hasn't already been used and that the server keys match the target environment.
5. Create a durable broadcaster with mTLS
After authorization, switch to a broadcaster that uses your permanent device keys instead of the one time key:
import { Buffer } from 'node:buffer';
import { Blockstream } from '@blockstream/cryptic';
const blockstream = new Blockstream(
await deviceKeys.rsa.privateKeyPem,
Buffer.from(deviceKeys.ecdsa.privateKeyHex, 'hex'),
inviteInfo.device_uuid!,
SERVER_RSA_PUBLIC_KEY_PEM,
Buffer.from(SERVER_ECDSA_PUBLIC_KEY, 'hex'),
);
const broadcaster = new Broadcaster(ECS_BASE_URL, blockstream, async request => {
const response = await mtlsRequest({
url: request.url,
method: request.method,
headers: request.headers,
body: request.body,
certPem: identity.certificatePem,
keyPem: identity.privateKeyPem,
caPem: PROD_CA_CERT_PEM,
})
return {
status: response.status,
body: response.body,
}
})This broadcaster handles all future SDK requests. Every call will be ECDSA signed and RSA encrypted automatically.
6. Fetch the AMP xpub
To build a wallet descriptor, you need the AMP2 server's keyorigin xpub. This is the server's half of the 2-of-2 multisig that protects every AMP2 wallet.
import { AmpActions } from '@blockstream/amp-registry';
const xpubResp = await broadcaster.processBroadcast<
typeof AmpActions.xpub
>(
{
action: 'get',
resource: '/amp/xpub',
details: {},
},
);
const ampKeyoriginXpub = xpubResp.details.keyorigin_xpub;7. Build a wallet descriptor
To register a wallet, you first need a confidential 2-of-2 multisig descriptor. Use LWK (Liquid Wallet Kit) to generate the key material and construct the descriptor. See the LWK documentation for installation and detailed usage.
The high level steps are:
- Create a signer from a BIP 39 mnemonic using LWK (e.g.,
SwSigner). - Derive your keyorigin xpub from the signer (e.g.,
signer.keyorigin_xpub()). - Get AMP2's keyorigin xpub from the SDK (see step 6 above) or from the invite data.
- Build the descriptor by combining both xpubs into a
ct(elip151,elwsh(multi(2, <your_xpub>, <amp_xpub>)))confidential descriptor.
The resulting descriptor fully describes the 2-of-2 multisig wallet where both your key and AMP2's key are required to sign transactions. Here is the example (using lwk_wasm):
import * as lwk from 'lwk_wasm'
const mnemonic = lwk.Mnemonic.fromRandom(12)
const network = lwk.Network.testnet()
let signer = new lwk.Signer(mnemonic, network)
const signerKeyoriginXpub = signer.keyoriginXpub(lwk.Bip.bip84())
console.log(`signer_keyoriginxpub: ${signerKeyoriginXpub}`)
const threshold = 2
const desc = `ct(elip151,elwsh(multi(${threshold},${signerKeyoriginXpub.trim()}/<0;1>/*,${ampKeyoriginXpub.trim()}/<0;1>/*)))`
let walletDescriptor = new lwk.WolletDescriptor(desc)
let wallet = new lwk.Wollet(network, walletDescriptor)8. Register the wallet
Submit the descriptor to AMP2:
import { AmpWalletRequestSchema } from '@blockstream/amp-registry';
const registerResp = await broadcaster.processBroadcast<
typeof AmpActions.wallet
>(
AmpWalletRequestSchema.parse({
action: 'add',
resource: '/amp/wallets',
details: {
descriptor: walletDescriptor.toString(),
},
}),
);
if (registerResp.status !== 'success') {
throw new Error(`Wallet registration failed: ${registerResp.message}`);
}
const wid = registerResp.details.wid;The returned wid (wallet ID) is a deterministic identifier derived from the descriptor. Use it in all subsequent asset operations.
9. Verify it worked
Check the wallet balance to confirm registration:
const balanceResp = await broadcaster.processBroadcast<
typeof AmpActions.walletBalance
>(
{
action: 'get',
resource: `/amp/wallets/${wid}/balance`,
details: { with_names: true },
},
);
console.log('Wallet balance:', balanceResp.details);A newly registered wallet will have zero balances. Fund it with LBTC (for transaction fees) before issuing or sending assets.
What just happened
- You generated RSA and ECDSA key pairs for your device.
- You used a one time broadcaster to activate your AMP2 account.
- You switched to a durable broadcaster using your permanent keys.
- You fetched AMP2's xpub and built a wallet descriptor using LWK.
- You registered the descriptor and received a wallet ID (
wid) that you'll use for all asset operations.
Next steps
- Asset Issuance: mint your first asset
- Authentication: detailed auth flow reference
- Wallet Registration: descriptor construction details
- Environments: testnet vs. mainnet configuration
Environments
AMP2 uses separate infrastructure for testing and production. Testnet and mainnet use separate Liquid networks, credentials, and server URLs.
Investor and venue quickstart
The guide explains the shortest path from LWK initialization to wallet registration and PSET cosigning. The venue creates an AMP2-connected wallet for asset receipt and transfers.