Skip to Content
GuideCore Integration
View .md

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/attestation returns 404 for any other providerAddress. 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.js

You 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

OptionTypeDefaultDescription
ssoBaseUrlstringBase URL of the SSO service (required)
providerAddressstringThe platform provider address (required)
pollingIntervalnumber5000Polling interval in milliseconds
credentialSignerProgramIdstringCredential Signer program ID override
sessionRegistryProgramIdstringSession Registry program ID override
sasProgramIdstringSAS (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.

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 redirect
  • polling_code — code for polling the linking status
  • expired_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 identifier
  • oracle_signature — oracle signature as a hex-encoded string
  • oracle_public_key — oracle public key as a hex-encoded string
  • solana_address — the wallet address being linked
  • timestamp — 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_signature and oracle_public_key are hex-encoded strings — decode them with Buffer.from(..., 'hex'). The expiry parameter 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 — pass 0 for no expiry. In browsers there is no global Buffer — import it from the buffer package (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_attestation reverts with TimestampTooOld when the clock differs from the oracle-signed timestamp by 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-signed timestamp. 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-signed close_attestation instruction 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

Last updated on