Skip to content
Solana Devnet: test network. Documents and acceptances here are for testing, not production evidence.

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.

Where Stele Gate sitsIllustration
UserDeposit 5,000 USDC
Your appfrontend
Stele SDKstele.gate()
Passporton Solana
Your programrequire_current_passport()
Actiondeposit executes

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
rust
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.

Stele Gate in five stepsIllustration

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.

  1. Deposit 5,000 USDCThe investor connects a wallet and taps Deposit in the Atlas Real Estate Vault.
  2. Not accepted → the modal opensInvestor Offering Terms v1, hash-verified against Solana. “Accept & Deposit”.
  3. Acceptance recorded, deposit succeedsThe acceptance is added to the investor's passport; the vault's check passes.
  4. The fund publishes v2 and requires itThe investor's next deposit is rejected on-chain: their passport names v1.
  5. Accept v2, deposit succeedsThe same passport now names v2. The v1 acceptance stays verifiable forever.
  6. 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

rust
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 Devnet

Versioning

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.

When the policy changesIllustration

Published versions

Offering Terms v1Required

fingerprint 72d7 1ed9 83fa…

The wallet's passport

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.

Can the requirement be skipped?Illustration

Without a program-level gate

Normal frontendAllowed
User
Checkboxfrontend
Program
Script / custom client
Script / CLI
Checkboxskipped
Programexecutes anyway

With Stele Gate

Script / custom client
Script / CLI
Vault program
Passport checkaccepted?
Investor who accepted the current terms
User
Passport checkcurrent version ✓
Vault programdeposit executes

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.

ts
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.

How verification works

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
Acceptances flow from the client into Stele Core; the Gate layer turns them into passports that your program checks before executing.

Proof vs Gate

Gate includes Proof.

Stele Proof compared with Stele Gate
FeatureProofGate
Exact document versionsIncludedIncluded
User acceptanceIncludedIncluded
ReceiptsIncludedIncluded
Independent verificationIncludedIncluded
PasskeysIncludedfor evidence; access needs the wallet
Wallet signaturesIncludedIncluded
Passport (on-chain acceptance per wallet)—Not includedIncluded
Program enforcement—Not includedIncluded
Protected actions—Not includedIncluded
Solana developer SDKoptionalIncluded
Direct bypass resistance—Not includedIncluded

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.