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 passport 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_passport() checks everything.
Add the passport 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 passport is a Stele account at its canonical address — not a look-alike
- It belongs to this wallet and has an entry for the policy's document
- The entry names the required version or a later one
stele_gate::require_current_passport(
&ctx.accounts.passport, // the investor's Stele passport
&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_passport(
&ctx.accounts.passport,
&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.
- Not accepted → the modal opensInvestor Offering Terms v1, hash-verified against Solana. “Accept & Deposit”.
- Acceptance recorded, deposit succeedsThe acceptance is added to the investor's passport; the vault's check passes.
- The fund publishes v2 and requires itThe investor's next deposit is rejected on-chain: their passport names v1.
- Accept v2, deposit succeedsThe same passport now names v2. The v1 acceptance stays verifiable forever.
- A direct CLI call failsNo frontend, no SDK — deposit() without a current passport 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_passport(
&ctx.accounts.passport,
&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 passport 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 passport
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 passport names v1 — and the policy requires v1.
Bypass resistance
A direct call without a current passport 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 passport, an older version, someone else's passport, an entry 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 passport has no current entry
// for the policy's document, 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 passport 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 passport)
- 10,000 lamports fee (Stele, or the organization if it sponsors fees) + ≈0.002 SOL refundable deposit
- Each further document
- 10,000 lamports + ≈0.0005 SOL refundable deposit
- Later acceptances (new versions)
- 10,000 lamports; the entry is updated in place
- The demo deposit, including the passport check
- ≈9,200 compute units, no extra fee
- For the investor
- Nothing: no fee for accepting
Stele Verified
Show your users that the check is real.
Stele runs your deployed program in simulation: a wallet that never accepted must be refused, one that accepted must be let through. It re-checks after every upgrade.
The badge on your site opens the evidence — and a button that repeats the checks in the visitor's own browser, against Solana.
Live status · opens the evidence · “Not valid on this site” anywhere else
Architecture
Stele Core, Stele Gate, your program.
Client
@stelehq/sdk
- Stele SDKgate(): read passport, 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
- Passportone per wallet, an entry per document
- Gate instructionscreate_policy, set_policy_requirement, record_passport_acceptance
- require_current_passport()one call in your program (stele-gate crate)
Third party
your program
- Solana programpins the policy; checks the passport 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 |
| Passport (on-chain acceptance per wallet) | —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).
- A passport binds the wallet that signed; passkey acceptances create Proof evidence, not passport entries.
- The SDK is user experience; protection exists only where your program calls require_current_passport().
- Accepting and acting are two transactions (not atomic).
- Passports are public accounts: anyone can see which versions a wallet accepted.
- 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.