Stele Gate · for Solana applications
Investors can't deposit until they've accepted your current terms.
Stele Gate turns versioned document acceptance into a condition your Solana program can enforce. Your frontend checkbox can be skipped. Your program requirement can't.
Architecture
Frontend SDK = UX. On-chain check = enforcement.
The SDK makes acceptance smooth. The requirement itself lives in your program, where a custom client cannot route around it. Stele is never in your execution path.
Frontend SDK = UX
Decides when to show the modal, records the acceptance, continues the action. It can be skipped — and that is fine.
On-chain check = enforcement
Your program checks the AccessPass before it acts. A client that skips the SDK is rejected here.
1/6The investor starts a deposit in your app.
One call
require_current() checks everything.
Add the AccessPass and the policy to your protected instruction and make one call. You don't re-implement a list of checks:
- The policy is a Stele policy at its canonical address
- The investor signed the transaction
- The AccessPass is a Stele account at its canonical address — not a look-alike
- It belongs to this wallet, this document and this organization
- It names the required version or a later one (same fingerprint at the required version)
stele_gate::require_current(
&ctx.accounts.access_pass, // the investor's AccessPass
&ctx.accounts.policy, // the Stele policy your program pins
&ctx.accounts.investor, // a Signer
)?;Pin the policy (store it, or use a constant) — otherwise a caller could present a policy of their own. The demo vault does it with #[account(address = vault.policy)].
How it works
Protect. Check. Accept. Continue. Enforce.
Link the action to a Stele policy
Your program pins the policy and makes one call before it acts. The policy says which version of your terms is required.
Policy pinned
pub fn deposit(ctx: Context<Deposit>, amount: u64) -> Result<()> {
stele_gate::require_current(
&ctx.accounts.access_pass,
&ctx.accounts.policy,
&ctx.accounts.investor,
)?;
// …1/5Protect: Link the action to a Stele policy.
The demo
Atlas Real Estate Vault
A small permissioned investment vault — enough to show Gate, nothing more. No real tokens move.
- Deposit 5,000 USDCThe investor connects a wallet and taps Deposit in the Atlas Real Estate Vault.
- No AccessPass → the modal opensInvestor Offering Terms v1, hash-verified against Solana. “Accept & Deposit”.
- Acceptance recorded, deposit succeedsThe acceptance creates the AccessPass; the vault's require_current() passes.
- The fund publishes v2 and requires itThe investor's next deposit is rejected on-chain: their pass names v1.
- Accept v2, deposit succeedsThe same pass now names v2. The v1 acceptance stays verifiable forever.
- A direct CLI call failsNo frontend, no SDK — deposit() without a current pass is rejected by the program.
The whole integration in the vault program
pub fn deposit(ctx: Context<Deposit>, amount: u64) -> Result<()> {
stele_gate::require_current(
&ctx.accounts.access_pass,
&ctx.accounts.policy,
&ctx.accounts.investor,
)?;
// … record the deposit
Ok(())
}Stele never holds the investor's funds and is not in the deposit's execution path: the vault program reads the AccessPass and the policy itself.
Run it on DevnetVersioning
Publish v2 and old access no longer satisfies the requirement.
The policy names the required version. The organization raises it when the change matters — a typo fix doesn't have to lock every investor out.
Published versions
fingerprint 72d7 1ed9 83fa…
The wallet's AccessPass
accepted: Offering Terms v1
Valid
Protected deposit
deposit(5,000 USDC)
Waiting
—
The evidence for v1 remains. Access now requires v2.
1/6The investor accepted Offering Terms v1. Their AccessPass names v1 — and the policy requires v1.
Bypass resistance
A direct call without a current pass is rejected on-chain.
Without a program-level gate
With Stele Gate
The frontend is optional. The program check is not.
1/4Without program enforcement, the terms checkbox lives in the frontend. A normal investor passes through it.
Tested against the real program binaries: no pass, an older version, someone else's pass, a pass for another document or organization, another policy, an unsigned investor, look-alike and forged accounts, and a forged policy all fail.
Developer experience
Wrap the action you already have.
These samples are the real API used by the vault demo. Acceptance and action are two transactions; the investor sees one “Accept & Deposit” step.
import { createSteleGate } from "@stelehq/sdk";
const stele = createSteleGate({
apiBaseUrl: "https://stele.site/api",
// Any Wallet Standard wallet: the key that signs the deposit.
wallet: {
address: wallet.publicKey.toBase58(),
signMessage: (message) => wallet.signMessage(message),
},
});
// Shows the Stele modal only if the investor's AccessPass is missing or
// outdated, records the acceptance, then sends the deposit.
const signature = await stele.gate({
policy: OFFERING_TERMS_POLICY, // the policy address your program pins
action: () => sendDepositTransaction(5_000_000_000n),
acceptLabel: "Accept & Deposit",
});Why Solana
The check has to run where the action executes.
A frontend can be bypassed: anyone can sign and send a transaction to your program from their own client.
A Solana program evaluates its own accounts, whatever the interface. The AccessPass is an account only the Stele program can write — and only after verifying the investor's signature over the exact terms — so your program can trust it in the same transaction as the deposit. No oracle, no off-chain lookup.
What it costs (measured)
- First acceptance (creates the pass)
- 10,000 lamports fee (Stele, or the organization if it sponsors fees) + ≈0.0025 SOL pass rent paid by Stele
- Later acceptances (new versions)
- 10,000 lamports; the pass is updated in place
- The demo deposit, including require_current()
- 9,406 compute units
- For the investor
- Nothing: no fee for accepting
Architecture
Stele Core, Stele Gate, your program.
Client
@stelehq/sdk
- Stele SDKgate(): read pass, modal, accept, continue
- Embedded modalhash-verified text, wallet signature
Stele Core
programs/stele
- Document registryorganizations, documents
- Version registryfingerprints, hash chain
- Acceptancesignature verified on-chain
- Receipt & verificationopen-source verifier
Stele Gate
programs/stele · crates/stele-gate
- Policyrequired version of a document, set by the organization
- AccessPassper policy and wallet
- Gate instructionscreate_policy, set_policy_requirement, record_gated_acceptance
- require_current()one call in your program (stele-gate crate)
Third party
your program
- Solana programpins the policy; require_current() before every protected action
Proof vs Gate
Gate includes Proof.
| Feature | Proof | Gate |
|---|---|---|
| Exact document versions | Included | Included |
| User acceptance | Included | Included |
| Receipts | Included | Included |
| Independent verification | Included | Included |
| Passkeys | Included | for evidence; access needs the wallet |
| Wallet signatures | Included | Included |
| AccessPass | —Not included | Included |
| Program enforcement | —Not included | Included |
| Protected actions | —Not included | Included |
| Solana developer SDK | optional | Included |
| Direct bypass resistance | —Not included | Included |
Honest limits
What Gate does and doesn't do.
- Gate enforces acceptance of a document version — not KYC, jurisdiction, age or accreditation (future integrations).
- An AccessPass binds the wallet that signed; passkey acceptances create Proof evidence, not passes.
- The SDK is user experience; protection exists only where your program calls require_current().
- Accepting and acting are two transactions (not atomic).
- Passes are public accounts: anyone can see that a wallet accepted a version.
- Stele's program is upgradeable today (moving to a multisig); it runs on Devnet, not Mainnet yet.
Watch a direct deposit fail on Devnet.
The live demo runs the real programs: every rejection is a transaction you can open.