Alien Solana SSO Core API Reference: AlienSolanaSsoClient
@alien-id/sso-solana is the framework-free client for linking Solana
wallets to verified Alien IDs through on-chain attestations. For the
step-by-step flow, see the
Core Integration guide. For React
apps, prefer the provider and hooks from
@alien-id/sso-solana-react.
AlienSolanaSsoClient
Creates the client. Every method that talks to the SSO server or builds the attestation transaction lives on the instance.
new AlienSolanaSsoClient(config: AlienSolanaSsoClientConfig)| Option | Type | Default | Description |
|---|---|---|---|
ssoBaseUrl | string | — | Base URL of the SSO service. HTTPS is enforced for non-loopback hosts |
providerAddress | string | — | The platform provider address (see below) |
pollingInterval | number | 5000 | Polling interval in milliseconds |
credentialSignerProgramId | string | 9cstDz8WWRAFaq1vVpTjfHz6tjgh6SJaqYFeZWi1pFHG | Credential Signer program ID |
sessionRegistryProgramId | string | DeHa6pyZ2CFSbQQiNMm7FgoCXqmkX6tXG77C4Qycpta6 | Session Registry program ID |
sasProgramId | string | 22zoJMtdu4tQc2PzL74ZUT7FrwgB1Udec8DdW4yw4BdG | SAS program ID |
allowInsecureSsoBaseUrl | boolean | false | Dev opt-in: accept http:// for a non-loopback ssoBaseUrl. localhost / 127.0.0.1 / [::1] are always allowed |
Single-platform-provider model. Solana SSO attestations bind a wallet to the platform’s Alien ID, not to your dApp. The oracle is pinned to a single platform provider address, and the
/solana/attestationendpoint returns 404 unlessproviderAddressmatches that platform provider — third-party provider addresses always getnullfromgetAttestation(). If you are not the platform provider, read the attestation directly on-chain via the attestation PDA instead.
const client = new AlienSolanaSsoClient({
ssoBaseUrl: 'https://sso.alien-api.com',
providerAddress: 'platform-provider-address',
});Methods
generateDeeplink()
Starts the attestation creation flow by generating a deep link and a polling code. Use it when the wallet doesn’t have an attestation yet. Throws on network failure, or when the response doesn’t match the expected schema.
async generateDeeplink(solanaAddress: string): Promise<SolanaLinkResponse>solanaAddress is the user’s Solana wallet public key as a base58
string. The response:
{
deep_link: string; // deep link for a QR code or mobile redirect
polling_code: string; // code for polling the linking status
expired_at: number; // Unix timestamp when the polling code expires
}const userPublicKey = wallet.publicKey.toBase58();
const { deep_link, polling_code } = await client.generateDeeplink(userPublicKey);
displayQRCode(deep_link);pollAuth()
Polls the linking status for a polling code from
generateDeeplink(). Throws on network failure, or
when the response doesn’t match the expected schema.
async pollAuth(pollingCode: string): Promise<SolanaPollResponse>The response:
{
status: 'pending' | 'authorized' | 'rejected' | 'expired';
oracle_signature?: string; // oracle signature (hex-encoded string)
oracle_public_key?: string; // oracle public key (hex-encoded string)
solana_address?: string; // wallet address the attestation is for
timestamp?: number; // Unix timestamp signed by the oracle
session_address?: string; // session identifier
}| Status | Meaning |
|---|---|
pending | The user hasn’t completed attestation creation yet |
authorized | The user approved; oracle signature data is available |
rejected | The user denied attestation creation |
expired | The polling code expired |
No pre-built transaction. The server never returns a pre-built transaction. On
authorized, the SDK assembles the attestation transaction entirely client-side — pass the poll response fields tobuildCreateAttestationTransaction().
const response = await client.pollAuth(polling_code);
if (response.status === 'authorized') {
console.log('Session address:', response.session_address);
console.log('Oracle signature:', response.oracle_signature);
console.log('Timestamp:', response.timestamp);
}getAttestation()
Checks whether a Solana wallet has a linking attestation backed by an
active Alien session, and returns its session address, or null if
none is found. Also returns null (a 404 under the hood) when the
client’s providerAddress doesn’t match the platform provider the
attestation belongs to — see the
single-platform-provider note. Throws on
network failure — and note that when an attestation exists but the
backing Alien session has been revoked, the server responds
403 Session is not active, which the SDK surfaces as a throw
(GetAttestation failed: <statusText>), not as null. The error
message interpolates the response’s statusText, whose exact reason
phrase depends on the fetch runtime (e.g. Forbidden for a 403).
This is not proof of authentication. A non-null result only means a linking record exists for that public wallet address — anyone who knows the address gets the same result. It does not prove the current user controls the wallet. To authenticate a user, require a fresh wallet signature over a server-issued challenge and verify it server-side; this SDK does not provide that primitive.
async getAttestation(solanaAddress: string): Promise<string | null>const sessionAddress = await client.getAttestation(wallet.publicKey.toBase58());
if (sessionAddress) {
// Wallet already has a linking attestation — skip the creation flow
console.log('Wallet is already linked. Session:', sessionAddress);
} else {
// No attestation — start the flow with generateDeeplink()
console.log('No attestation found, need to create one');
}buildCreateAttestationTransaction()
Builds the Solana transaction that creates the on-chain attestation. Internally it fetches the Credential Signer program state account to read the credential and schema addresses, derives the required PDAs, and combines an Ed25519 signature-verification instruction with the attestation instruction.
Returns an unsigned Transaction with neither recentBlockhash
nor feePayer set — fetch a recent blockhash and set both before
asking the wallet to sign, as shown below. Throws when the RPC request
fails, when the program state account is missing, owned by the wrong
program, or too short, and when the oracle public key from the poll
response doesn’t match the one recorded on-chain — a deliberate
security gate that prevents a compromised backend from substituting
its own oracle key.
async buildCreateAttestationTransaction(params: BuildAttestationParams): Promise<Transaction>interface BuildAttestationParams {
connection: Connection; // Solana RPC connection
payerPublicKey: PublicKey; // user's wallet public key
sessionAddress: string; // session address from the poll response
oracleSignature: Uint8Array; // oracle signature, hex-decoded from the poll response
oraclePublicKey: PublicKey; // oracle public key, hex-decoded from the poll response
timestamp: number; // timestamp from the poll response (oracle-signed)
expiry: number; // client-supplied; pass 0 (see below)
}
expiryis a no-op. It is not returned by the server, not covered by the oracle signature, and not validated on-chain — it is only stored verbatim in the attestation record. Pass0.
Timestamp freshness. The on-chain program rejects the transaction with
TimestampTooOldif it is submitted more than 300 seconds (5 minutes) after the oracle-signedtimestamp. The check is two-sided (|now − timestamp| ≤ 300), so a future-skewed timestamp also reverts. Build and submit the transaction promptly after a successful poll.
import { Connection, PublicKey } from '@solana/web3.js';
import { Buffer } from 'buffer';
const connection = new Connection('https://api.mainnet-beta.solana.com');
const transaction = await client.buildCreateAttestationTransaction({
connection,
payerPublicKey: wallet.publicKey,
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, // currently unused — not signed by the oracle or validated on-chain
});
// Set the blockhash and fee payer, then sign and send
const { blockhash } = await connection.getLatestBlockhash();
transaction.recentBlockhash = blockhash;
transaction.feePayer = wallet.publicKey;
const signedTransaction = await wallet.signTransaction(transaction);
const signature = await connection.sendRawTransaction(
signedTransaction.serialize()
);
await connection.confirmTransaction(signature);PDA Derivation
The attestation system stores its state in Program Derived Addresses.
The SDK derives all of them internally when building the attestation
transaction — the derivation helpers are not exported from the
published @alien-id/sso-solana package. To read on-chain state
directly — for example when your provider address isn’t the platform
provider and getAttestation() is not available to you — derive the
PDAs yourself with PublicKey.findProgramAddressSync. Every
derivation returns a [PublicKey, number] tuple: the PDA address and
its bump seed.
The credential and schema addresses an attestation is derived from live in the Credential Signer program state account. Its layout is an 8-byte Anchor discriminator followed by six 32-byte public keys — oracle, credential, schema, event authority, authority, session registry:
import { Connection, PublicKey } from '@solana/web3.js';
import { Buffer } from 'buffer';
// Program IDs as PublicKey instances — readonly fields on the client
// (defaults listed in the Solana Programs table below)
const { credentialSignerProgramId, sasProgramId } = client;
// 1. Read the credential and schema addresses from program state
const [programStatePda] = PublicKey.findProgramAddressSync(
[Buffer.from('program_state')],
credentialSignerProgramId
);
const account = await connection.getAccountInfo(programStatePda);
const credential = new PublicKey(account!.data.slice(8 + 32, 8 + 64));
const schema = new PublicKey(account!.data.slice(8 + 64, 8 + 96));
// 2. Derive the wallet's attestation PDA
const [attestationPda] = PublicKey.findProgramAddressSync(
[
Buffer.from('attestation'),
credential.toBuffer(), // credential PDA from program state
schema.toBuffer(), // schema PDA from program state
userPublicKey.toBuffer(), // the wallet (nonce seed)
],
sasProgramId
);
// 3. A non-null account at attestationPda means the wallet is linked
const attestation = await connection.getAccountInfo(attestationPda);The seed lists below mirror the SDK’s internal helpers (pda.ts) and
the on-chain programs; each heading names the corresponding internal
helper.
deriveProgramStatePda()
Seeds ["program_state"] on the Credential Signer program. Holds
the oracle key and the credential/schema addresses (layout above).
deriveCredentialSignerPda()
Seeds ["credential_signer"] on the Credential Signer program.
deriveSessionRegistryPda()
Seeds ["session_registry"] on the Session Registry program.
deriveSessionEntryPda()
Seeds ["session", sessionAddress] (the session address as UTF-8
bytes) on the Session Registry program. One entry per linked
session.
deriveSolanaEntryPda()
Seeds ["solana", walletPublicKey] on the Session Registry
program. Maps a Solana wallet to its session.
deriveAttestationPda()
Seeds ["attestation", credential, schema, walletPublicKey] on the
SAS program — the wallet’s public key is the nonce seed. This is
the entry point for reading a wallet’s attestation directly on-chain;
see the worked example above.
deriveCredentialPda()
Seeds ["credential", authority, name] (the credential name as UTF-8
bytes) on the SAS program. You normally don’t need this — read the
credential address from program state instead.
deriveSchemaPda()
Seeds ["schema", credentialPda, name, version] on the SAS
program — the first seed input is the credential PDA (not an
authority key), name is UTF-8 bytes, and version is a single
u8 byte. You normally don’t need this — read the schema address from
program state instead.
Types
Canonical definitions for the package’s public types. The React package
re-uses these — import them from @alien-id/sso-solana.
AlienSolanaSsoClientConfig
interface AlienSolanaSsoClientConfig {
ssoBaseUrl: string;
providerAddress: string;
pollingInterval?: number;
credentialSignerProgramId?: string;
sasProgramId?: string;
sessionRegistryProgramId?: string;
allowInsecureSsoBaseUrl?: boolean;
}SolanaLinkResponse
interface SolanaLinkResponse {
deep_link: string;
polling_code: string;
expired_at: number;
}SolanaPollResponse
interface SolanaPollResponse {
status: 'pending' | 'authorized' | 'rejected' | 'expired';
oracle_signature?: string; // hex-encoded
oracle_public_key?: string; // hex-encoded
solana_address?: string;
timestamp?: number;
session_address?: string;
}BuildAttestationParams
interface BuildAttestationParams {
connection: Connection;
payerPublicKey: PublicKey;
sessionAddress: string;
oracleSignature: Uint8Array;
oraclePublicKey: PublicKey;
timestamp: number;
expiry: number; // pass 0 — currently unused
}Solana Programs
The on-chain programs behind Solana SSO. Their IDs are the defaults in the client config and can be overridden there.
| Program | Program ID | Role |
|---|---|---|
| Credential Signer | 9cstDz8WWRAFaq1vVpTjfHz6tjgh6SJaqYFeZWi1pFHG | Manages credential signing and schema definitions |
| Session Registry | DeHa6pyZ2CFSbQQiNMm7FgoCXqmkX6tXG77C4Qycpta6 | Stores session entries and maps Solana addresses to sessions |
| SAS (Solana Attestation Service) | 22zoJMtdu4tQc2PzL74ZUT7FrwgB1Udec8DdW4yw4BdG | Creates and manages on-chain attestations linking credentials, schemas, and user addresses |
Error Handling
All async methods can throw — network failures, schema-validation failures, and missing on-chain accounts surface as exceptions. Wrap calls in try/catch:
try {
const { deep_link, polling_code } = await client.generateDeeplink(solanaAddress);
} catch (error) {
console.error('Failed to generate deep link:', error);
}
try {
const transaction = await client.buildCreateAttestationTransaction({
connection,
payerPublicKey: wallet.publicKey,
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);
}Two on-chain failure modes are worth handling explicitly: the
±300-second timestamp freshness window
(TimestampTooOld), and account already in use when a wallet already
has an attestation — see
Attestation Lifecycle and Re-Linking.
Complete Example
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(wallet: any) {
const solanaAddress = wallet.publicKey.toBase58();
// Check whether the wallet already has a linking attestation
// (existence of a link — not proof the current user controls the wallet)
const existingSession = await client.getAttestation(solanaAddress);
if (existingSession) {
console.log('Wallet already linked:', existingSession);
return;
}
// Generate deep link
const { deep_link, polling_code } = await client.generateDeeplink(solanaAddress);
displayQRCode(deep_link);
// Poll for authorization
const pollInterval = setInterval(async () => {
const response = await client.pollAuth(polling_code);
if (response.status === 'authorized') {
clearInterval(pollInterval);
// Build attestation transaction (submit within 5 minutes of the
// oracle-signed timestamp, or it reverts with TimestampTooOld)
const transaction = await client.buildCreateAttestationTransaction({
connection,
payerPublicKey: wallet.publicKey,
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,
});
// Set the blockhash and fee payer, then sign and send
const { blockhash } = await connection.getLatestBlockhash();
transaction.recentBlockhash = blockhash;
transaction.feePayer = wallet.publicKey;
const signedTransaction = await wallet.signTransaction(transaction);
const signature = await connection.sendRawTransaction(
signedTransaction.serialize()
);
await connection.confirmTransaction(signature);
console.log('Attestation created! Signature:', signature);
// Confirm the link is now visible
const sessionAddress = await client.getAttestation(solanaAddress);
console.log('Linked session:', sessionAddress);
hideQRCode();
} else if (response.status === 'rejected' || response.status === 'expired') {
clearInterval(pollInterval);
console.error('Attestation flow failed:', response.status);
hideQRCode();
}
}, 5000);
}Next Steps
- React API Reference — provider, hooks, and components
- Core Integration — the full flow, step by step
- Introduction — the wallet-linking model and attestation lifecycle
- Demo App — example implementation and source code