Account types in Anchor
Every field in an Accounts struct has a type, and the type is doing real work. It tells Anchor what kind of account this slot expects, what checks to run before your handler executes, and what your handler can do with the field once it gets there. There are five types you'll use day to day. Each one is a different contract about what the account is and what's already been verified by the time your code sees it. The first thing to learn is when to reach for each one.
What the type is actually doing
When you write pub vault: Account<'info, Vault> inside an Accounts struct, you are not just naming a Rust type. You are telling Anchor to perform a specific set of checks on whichever account appears in that slot of the transaction. If any of those checks fail, the transaction is rejected before your handler runs. If all of them pass, the field becomes available inside your handler as a fully typed value, ready to read or write.
The same idea applies to all five Anchor account types. Each one packages a particular check, or set of checks, behind a name. Picking the right type is how you express what the account is supposed to be in a way the framework can verify mechanically. The point of having five types instead of one is that "what counts as valid" depends on the role the account plays in the instruction. A signer is different from a state account, which is different from a recipient wallet, which is different from a program you're calling into.
In the same way you wouldn't write Object in Java when you mean String, you don't write UncheckedAccount in Anchor when you mean Signer. Pick the most specific type that captures what you need. Anchor takes care of the rest.
The five types, one at a time
Signer<'info>. The account whose keypair authorized the transaction. Anchor verifies one thing: a signature from this account's public key was attached to the incoming transaction. That's it. There's no check on the account's owner, no check on its data, no check on whether it even has a balance. Use it for the wallet that's calling your instruction. Anything labeled "authority," "owner," or "user" in your design almost certainly wants this type.
Account<'info, T>. The workhorse of every Anchor program. It says three things at once: the account is owned by your program, its data starts with the 8-byte discriminator that identifies a T, and the rest of its data deserializes cleanly into the T struct. By the time your handler runs, ctx.accounts.vault is a reference to a typed Vault you can read and write like any Rust struct. Use this for every piece of state your program owns. The whole point of writing a program is to manipulate accounts of this type.
SystemAccount<'info>. A regular wallet on the chain, owned by the System Program. No data, no fancy checks, just a SOL balance. Use it when an instruction needs to reference a wallet that isn't signing the transaction. A common case is a recipient of a SOL transfer: the recipient doesn't sign, but you want to make sure the slot really holds a plain wallet rather than some random account you'd accidentally clobber. SystemAccount is the type that makes that intent explicit.
Program<'info, T>. The account is the deployed program of type T. Anchor verifies the account's address matches the program's known address and that its executable flag is true. Use it when your instruction calls into another program. Need to invoke the System Program to create an account? You'll list it as Program<'info, System>. Need to call the Token Program to move tokens? Program<'info, Token>. Calling your own program recursively? Program<'info, MyProgram>. Without this type, you would not have a way to tell Anchor "yes, this account is supposed to be a program."
UncheckedAccount<'info>. The escape hatch. Anchor performs no checks on it. Whatever account the client sends in this slot is what your handler receives, as a raw AccountInfo you have to validate yourself. Use it only when none of the other types fit, and always pair it with a /// CHECK: doc comment explaining why it's safe and what you're doing to validate it. Anchor's compiler refuses to build without that comment, which is one of the framework's small but effective safety nets. Common legitimate uses include accounts you constrain with a fixed address (#[account(address = ...)]), or accounts whose validation is performed by a CPI you're about to make. Reaching for UncheckedAccount is a signal that you've left the framework's safe path and you should know exactly why.
A real example with all five
A withdrawal from a vault is a good case for seeing every type at once. The vault holds program state, so it's Account<'info, Vault>. The owner has to authorize the withdrawal, so they're a Signer<'info>. The funds go to a recipient wallet that doesn't need to sign, so that's a SystemAccount<'info>. The transfer happens by calling into the Token Program, which appears in the struct as Program<'info, Token>. And there's a price oracle that's not directly typed by Anchor but is constrained to a specific address, so it appears as an UncheckedAccount<'info> with a /// CHECK: comment explaining the address pin.
Anyone reading that struct, without seeing the handler at all, can describe what the instruction does and what assumptions hold by the time the handler runs. That readability is the second-biggest payoff of using specific types. The first is correctness.
A decision rule for picking the type
When you're writing a new instruction and trying to decide which type to use for each field, walk through the questions in order. Does this account need to sign the transaction? Then it's a Signer. Is it a program you call into? Then it's a Program<T>. Does it hold state owned by your program? Then it's an Account<T>. Is it a plain wallet that doesn't sign? Then it's a SystemAccount. If none of those fit, you reach for UncheckedAccount, and the comment you write next is your justification.
The earlier the question lands on a "yes," the safer the resulting type. Signer constrains the account on exactly one dimension, that a signature is present, and demands nothing else. UncheckedAccount constrains it on zero. Most of the security bugs in Solana programs trace back to the same pattern: a developer reached for UncheckedAccount to keep moving, then forgot to add the manual validation that the type's "do nothing" semantics imply.
A note on AccountInfo
If you read older Anchor code or the underlying Solana SDK, you will see AccountInfo<'info> everywhere. It's the raw type that the runtime hands to a program: just an account's public key, owner, lamports, data, and flags. UncheckedAccount is a wrapper around AccountInfo whose only added behavior is the /// CHECK: requirement at compile time. The underlying account data is identical. In a modern Anchor program, prefer UncheckedAccount because the compile-time prompt to justify the unsafe choice is exactly the kind of friction you want on the path to ignoring validation.