Alien Solana SSO Core Integration: Vanilla JS Setup Guide
Link Solana wallets to verified Alien IDs from any JavaScript or
TypeScript project with @alien-id/sso-solana — no framework
required.
Single-platform provider. The SSO service and its oracle only serve the configured platform provider;
/solana/attestationreturns404for any otherproviderAddress. Attestations bind a wallet to the platform’s Alien ID, not to your individual dApp. Third-party dApps read attestations on-chain by deriving the attestation PDA instead — see the introduction.
Quick Start
Install the SDK:
npm install @alien-id/sso-solana @solana/web3.jsYou need @solana/web3.js ^1.95.0 and the platform provider address
from the Developer Portal . The core SDK
keeps no browser storage — persisting the link state is up to you.
To complete a link you also need the Alien App on a phone with an
Alien ID. The examples below use the mainnet-beta endpoint; the
demo app runs against devnet — confirm the cluster and RPC
endpoint with the platform operator.
The whole linking flow — check, deep link, poll, build, sign, send — in one function:
import { AlienSolanaSsoClient } from '@alien-id/sso-solana';
import { Connection, PublicKey } from '@solana/web3.js';
import { Buffer } from 'buffer';
const client = new AlienSolanaSsoClient({
ssoBaseUrl: 'https://sso.alien-api.com',
providerAddress: 'platform-provider-address',
});
const connection = new Connection('https://api.mainnet-beta.solana.com');
async function linkWallet(userWallet: any) {
const userPublicKey = userWallet.publicKey;
const solanaAddress = userPublicKey.toBase58();
// 1. Skip the flow if the wallet is already linked
const existingSession = await client.getAttestation(solanaAddress);
if (existingSession) return existingSession;
// 2. Generate a deep link and show it as a QR code
const { deep_link, polling_code } = await client.generateDeeplink(solanaAddress);
displayQRCode(deep_link);
// 3. Poll until the user approves in the Alien App
const pollInterval = setInterval(async () => {
const response = await client.pollAuth(polling_code);
if (response.status === 'rejected' || response.status === 'expired') {
clearInterval(pollInterval);
return;
}
if (response.status !== 'authorized') return; // 'pending' — keep polling
clearInterval(pollInterval);
// 4. Build the attestation transaction locally
// (oracle fields are hex strings; expiry is unused — pass 0)
const transaction = await client.buildCreateAttestationTransaction({
connection,
payerPublicKey: userPublicKey,
sessionAddress: response.session_address!,
oracleSignature: Uint8Array.from(Buffer.from(response.oracle_signature!, 'hex')),
oraclePublicKey: new PublicKey(Buffer.from(response.oracle_public_key!, 'hex')),
timestamp: response.timestamp!,
expiry: 0,
});
// 5. Set the blockhash and fee payer, sign with the user's
// wallet, and send promptly (±300 s window)
const { blockhash } = await connection.getLatestBlockhash();
transaction.recentBlockhash = blockhash;
transaction.feePayer = userPublicKey;
const signedTransaction = await userWallet.signTransaction(transaction);
const signature = await connection.sendRawTransaction(
signedTransaction.serialize()
);
await connection.confirmTransaction(signature);
// 6. Re-check — the wallet is now linked
console.log('Linked:', await client.getAttestation(solanaAddress));
}, 5000);
}Each step is explained in detail below.
Configuration
| Option | Type | Default | Description |
|---|---|---|---|
ssoBaseUrl | string | — | Base URL of the SSO service (required) |
providerAddress | string | — | The platform provider address (required) |
pollingInterval | number | 5000 | Polling interval in milliseconds |
credentialSignerProgramId | string | — | Credential Signer program ID override |
sessionRegistryProgramId | string | — | Session Registry program ID override |
sasProgramId | string | — | SAS (Solana Attestation Service) program ID override |
See the core API reference for the full config shape and the default program IDs.
How Each Step Works
Step 1: Connect a Wallet
First, ensure the user has connected their Solana wallet:
import { Connection, PublicKey } from '@solana/web3.js';
// Get user's wallet public key (from wallet adapter or similar)
const userPublicKey = new PublicKey('user-wallet-address');
const solanaAddress = userPublicKey.toBase58();Step 2: Check for an Existing Attestation
Before initiating the attestation creation flow, check if the wallet already has an attestation:
const sessionAddress = await client.getAttestation(solanaAddress);
if (sessionAddress) {
console.log('Wallet is already linked! Session:', sessionAddress);
// Wallet already has an attestation, skip the creation flow
return;
}
// No attestation found, proceed with creation flow
console.log('No attestation found, starting attestation creation...');Linking is not authentication. An existing attestation does not prove the current user controls the wallet — to authenticate them, verify a fresh wallet signature server-side; the SDK does not provide that primitive. See the introduction.
Step 3: Generate a Deep Link (If No Attestation)
const { deep_link, polling_code, expired_at } = await client.generateDeeplink(solanaAddress);
// Display QR code with deep_link
displayQRCode(deep_link);
// Or redirect mobile users
window.location.href = deep_link;generateDeeplink() takes the user’s Solana wallet address and
returns:
deep_link— link for the QR code or mobile redirectpolling_code— code for polling the linking statusexpired_at— Unix timestamp when the polling code expires
Step 4: Poll for Authorization
let pollResponse;
const pollInterval = setInterval(async () => {
pollResponse = await client.pollAuth(polling_code);
if (pollResponse.status === 'authorized') {
clearInterval(pollInterval);
// Proceed to transaction building
console.log('Authorized! Session address:', pollResponse.session_address);
} else if (pollResponse.status === 'rejected') {
clearInterval(pollInterval);
console.error('User rejected the linking request');
} else if (pollResponse.status === 'expired') {
clearInterval(pollInterval);
console.error('Linking request expired');
}
// If status is 'pending', continue polling
}, 5000);The status is one of pending, authorized, rejected, or
expired. When it is 'authorized', the response includes:
session_address— session identifieroracle_signature— oracle signature as a hex-encoded stringoracle_public_key— oracle public key as a hex-encoded stringsolana_address— the wallet address being linkedtimestamp— Unix timestamp signed by the oracle (valid on-chain for ±300 seconds)
The attestation transaction is built locally by the SDK from these fields — the server does not return a pre-built transaction.
Step 5: Build the Attestation Transaction
import { Connection, PublicKey } from '@solana/web3.js';
import { Buffer } from 'buffer';
// Create Solana connection
const connection = new Connection('https://api.mainnet-beta.solana.com');
// Build attestation transaction
const transaction = await client.buildCreateAttestationTransaction({
connection,
payerPublicKey: userPublicKey,
sessionAddress: pollResponse.session_address!,
oracleSignature: Uint8Array.from(Buffer.from(pollResponse.oracle_signature!, 'hex')),
oraclePublicKey: new PublicKey(Buffer.from(pollResponse.oracle_public_key!, 'hex')),
timestamp: pollResponse.timestamp!,
expiry: 0,
});Hex decoding.
oracle_signatureandoracle_public_keyare hex-encoded strings — decode them withBuffer.from(..., 'hex'). Theexpiryparameter is client-supplied: it is forwarded on-chain to SAS as the attestation’s expiry, but it is not signed by the oracle nor validated by the credential signer — pass0for no expiry. In browsers there is no globalBuffer— import it from thebufferpackage (the SDK does the same); Vite and webpack 5 do not polyfill Node globals.
Step 6: Sign and Send the Transaction
The transaction returned by buildCreateAttestationTransaction()
has no recentBlockhash or feePayer set — set both before asking
the wallet to sign, then send the signed transaction raw:
// Set the blockhash and fee payer
const { blockhash } = await connection.getLatestBlockhash();
transaction.recentBlockhash = blockhash;
transaction.feePayer = userPublicKey;
// Sign with the user's wallet
// (implementation depends on your wallet integration)
const signedTransaction = await userWallet.signTransaction(transaction);
// Send the signed transaction and confirm it
const signature = await connection.sendRawTransaction(
signedTransaction.serialize()
);
await connection.confirmTransaction(signature);
console.log('Attestation created! Signature:', signature);Warning: submit promptly. The on-chain check is two-sided —
create_attestationreverts withTimestampTooOldwhen the clock differs from the oracle-signedtimestampby more than 300 seconds in either direction. The window starts when the oracle signs, before your poll even returns, so the real budget after a successful poll is less than 5 minutes. Build, sign, and send the transaction immediately after authorization.
Step 7: Verify the Attestation
After the transaction is finalized, verify the on-chain attestation:
const sessionAddress = await client.getAttestation(solanaAddress);
if (sessionAddress) {
console.log('Attestation created successfully! Session:', sessionAddress);
} else {
console.log('Attestation not found, please try again');
}Derive Attestation PDAs
Reading an attestation on-chain means deriving its Program Derived Address (PDA). The SDK derives these internally when building transactions, but its helper functions are not currently exported from the published package — derive the PDA directly:
import { PublicKey } from '@solana/web3.js';
import { Buffer } from 'buffer';
// credential and schema are PDAs read from the on-chain
// ProgramState account of the Credential Signer program
const [attestationPda] = PublicKey.findProgramAddressSync(
[
Buffer.from('attestation'),
credential.toBuffer(), // credential PDA from ProgramState
schema.toBuffer(), // schema PDA from ProgramState
userPublicKey.toBuffer(), // the wallet (nonce seed)
],
sasProgramId
);See the core API reference for the seed layouts of the other accounts.
Handle Errors
try {
const { deep_link, polling_code } = await client.generateDeeplink(solanaAddress);
} catch (error) {
console.error('Failed to generate deep link:', error);
// Handle network error or server error
}
try {
const transaction = await client.buildCreateAttestationTransaction({
connection,
payerPublicKey: userPublicKey,
sessionAddress: pollResponse.session_address!,
oracleSignature: Uint8Array.from(Buffer.from(pollResponse.oracle_signature!, 'hex')),
oraclePublicKey: new PublicKey(Buffer.from(pollResponse.oracle_public_key!, 'hex')),
timestamp: pollResponse.timestamp!,
expiry: 0,
});
} catch (error) {
console.error('Transaction building failed:', error);
// Could be due to missing program state or invalid parameters
}Common on-chain failure modes:
TimestampTooOld— the transaction was submitted more than ±300 seconds after the oracle-signedtimestamp. Re-run the polling flow and submit promptly.account already in use— the wallet already has an attestation; only one attestation can exist per wallet at a time. Re-linking requires the wallet-signedclose_attestationinstruction first — see Attestation Lifecycle and Re-Linking.
The Quick Start block above is already the complete flow — wrap it in your own error handling and QR display. For a full working project, run the demo app.
Next Steps
- API Reference - Core — every method, PDA utility, and type
- React Integration — pre-built provider, hooks, and modal
- Demo App — run the example app