Solana Attestation Service: How On-Chain Credentials Work Without a Central Verifier
Solana Attestation Service puts verifiable credentials on-chain via a Credential-Schema-Attestation model. How it works, and how it differs from EAS.
Almost every dApp that gates access — accredited-investor pools, region-locked features, KYC'd trading — rebuilds the same thing: a database row that says "wallet X passed check Y," re-checked server-side on every request. That works until a second app needs the same check, at which point the user re-verifies from scratch and you own another honeypot of identity data. The Solana Attestation Service (SAS), now live on mainnet, moves that claim on-chain as a reusable credential, so verification happens once and any program or backend can read the result. This post covers how SAS models on-chain credentials, why its design leans on the SVM, and what it changes for gating — and testing — dApp access.
Why "verify once, reuse everywhere" needs an on-chain primitive
A credential is just a signed statement: an issuer you trust asserts something about a wallet. The hard part isn't making the statement — it's making it portable and verifiable by anyone without asking the issuer's server whether it's still good. A database row can't do that; only the app that owns it can read it.
Putting the statement on-chain fixes both properties. The issuer's signature is checkable by anyone, the record lives at a deterministic address, and expiry or revocation is on-chain state, not a private API call. SAS is the shared protocol that standardizes this so a credential issued by one app is legible to the next.
The account model: Credential, Schema, and Attestation
SAS is built from three account types, each a link in a chain of authority:
- Credential — the root. It names an issuing authority and lists the authorized signers allowed to attest under it. Think of it as an organization's namespace.
- Schema — a data template under a Credential. It fixes the field names and types every attestation must follow. The layout is a compact array of type codes (for example, a code for
String, another forU8), so on-chain data stays tightly packed rather than stored as loose JSON. - Attestation — the actual claim: data conforming to a Schema, signed by an authorized signer, optionally carrying an expiry timestamp and a nonce that makes its address unique.
The three nest: a Credential authorizes signers, a Schema constrains shape, an Attestation is one signed instance. Verifying a wallet's status is then a matter of deriving the expected attestation address and reading it back — no call to the issuer required.
// A schema is a layout, not free-form JSON — each field has a fixed type.
// (Illustrative shape of the fields a "region eligibility" schema might fix.)
const schemaFields = [
{ name: "country", type: "String" },
{ name: "verified_at", type: "U64" },
{ name: "tier", type: "U8" },
];Because the schema is stored on-chain, a verifier fetches it first, then uses it to deserialize the attestation's packed bytes — the data can't drift from its declared structure.
Why the SVM design looks nothing like Ethereum's EAS
Ethereum's Attestation Service (EAS) leans on a stateful contract: attestations live inside contract storage, and you interact through the contract's methods. SAS reaches the same goal through the SVM's stateless model, and the difference is worth calling out explicitly so you don't carry EVM assumptions across.
On Solana, programs hold no per-user state internally. Every attestation is its own account, and its address is a Program Derived Address (PDA) — deterministically derived from seeds like the credential, schema, and a nonce (often the subject's wallet). That has two consequences an EVM developer will notice:
- You can compute an attestation's address before it exists. A verifier derives the PDA from known seeds and reads that account directly. There's no registry lookup and no
getAttestation(id)call bouncing through contract storage — the account either exists at the derived address or it doesn't. - Accounts are explicit inputs. A program that gates on an attestation receives that attestation account in the instruction's account list; it doesn't reach into another contract's storage mid-execution. The check is a read of an account you already passed in.
This is the divergence that shows up everywhere between the two chains: Ethereum threads state through contracts, the SVM makes state an addressable account you name up front. The mental model doesn't transfer, and code that assumes a central registry contract won't map onto SAS.
Standard vs. tokenized attestations
SAS supports two representations. A standard attestation is a plain PDA holding the packed data — cheapest, ideal for backend checks. A tokenized attestation also mints a Token-2022 token to the holder's wallet, using the Non-Transferable extension so the credential can't be sold or moved. That makes it visible in wallets and composable with token-gating logic that already reads balances, at the cost of a mint. Use tokenized when the credential should be user-visible and soulbound; use standard when only your program or server reads it.
What this changes for gating — and testing — dApp access
Once a check becomes an on-chain attestation, your gate has two branches that both need to work: a wallet holding a valid, unexpired attestation gets in; one that doesn't hits your fallback (a prompt to verify, a disabled action). Those branches are where UI bugs hide, because they depend on real wallet state, not a mocked API response. It's worth exercising end to end — driving a real wallet through connect and any required signing so you assert on the branch a user actually sees. An E2E harness like @avalix/chroma turns that gate into a test you run in CI rather than a path you check by hand.
The larger shift is architectural: identity checks stop being private rows and become shared, expiring, on-chain facts. If you gate on who a user is rather than what they hold, read the Credential-Schema-Attestation model before you add another is_verified column — and decide whether your credential needs to be tokenized at all.