SolRengine

Issuing a Solana Token from Rails, Without Touching a Wallet

Issue a real SPL token from a Rails app — no keypair, no Anchor program. Register, deploy, mint, and burn through the Solana Developer Platform with solrengine-sdp.

· moviendo.me

Issuing a Solana token the traditional way means managing a fee-payer keypair, running spl-token create-token from the CLI, and storing a private key somewhere your server can reach it. When the mint instruction times out, you wonder whether to retry. When your .env leaks, you lose the treasury wallet.

With the SolRengine SDP gems, your Rails app POSTs to the Solana Developer Platform’s REST API. SDP handles custody, signing, and fee payment. The SPL token appears on chain. No seed phrase in your codebase, no Anchor program, no client-side wallet prompt.

Two Paths in SolRengine

SolRengine covers two ways to build a Solana app with Rails:

  • BYO wallet: users connect Phantom/Solflare/Backpack; the app never holds keys. Gems: solrengine-auth, solrengine-rpc, solrengine-transactions.
  • Wallet-per-User (SDP): the app provisions a custody wallet per user via the Solana Developer Platform; SDP absorbs custody, signing, and fees. Gems: solana-sdp, solrengine-sdp.

Token issuance lives on the SDP path. Your app is the operator; users sign up with an email address. They never see a seed phrase.

What the Issuance Surface Gives You

solrengine-sdp 0.3 adds a persisted Token model with four operations on top of the existing wallet and payments calls:

  1. register — records a token (name, symbol, decimals) with SDP off-chain
  2. deploy — SDP creates the SPL mint on chain and records the mint_address
  3. mint — credits tokens to a custody wallet (earn)
  4. burn — removes tokens from a custody wallet, signed by the holder (redeem)

The engine drives this through Solrengine::Sdp::Token — an ActiveRecord model where the row is the audit trail. No custom Anchor program. No client-side transaction.

Setup

# Gemfile
gem "solana-sdp",     "~> 0.2"   # Ruby client for the SDP API
gem "solrengine-sdp", "~> 0.3"   # Rails engine: Wallet-per-User + token issuance
bin/rails generate solrengine:sdp:install
bin/rails db:migrate
# config/initializers/solrengine_sdp.rb
Solrengine::Sdp.configure do |c|
  c.api_key          = ENV.fetch("SDP_API_KEY")
  c.base_url         = ENV.fetch("SDP_API_BASE_URL", "http://127.0.0.1:8787")
  c.custody_provider = ENV["SDP_CUSTODY_PROVIDER"]   # a managed provider, e.g. "privy"
  c.label_namespace  = "myapp"
end

Token Genesis

Issuing the token runs once per app: register it, then deploy the mint. The mint authority is a custody wallet your business controls — find-or-create one by a stable label so setup is idempotent:

class Treasury
  LABEL = "myapp-treasury"

  def self.wallet_id
    client = Solrengine::Sdp.client
    (client.list_wallets.find { |w| w.label == LABEL } || client.create_wallet(label: LABEL)).id
  end
end
# Run once — e.g. a rake task or a guarded setup action.
return if Solrengine::Sdp::Token.exists?   # one token per app

token = Solrengine::Sdp::Token.register!(   # off-chain record; raises if SDP rejects it
  name: "Community Points", symbol: "PTS", decimals: 0,
  signing_wallet_id: Treasury.wallet_id     # mint authority
)
token.deploy!        # SDP signs + pays fees, records mint_address. Never retried (money-path).
token.deployed?      # => true

deploy! runs inline and re-raises on failure, marking the row failed with the error — so a timeout is a failed row to reconcile, never a blind retry that risks a double-deploy. Points use decimals: 0, so amounts are whole numbers; for a fractional token, amounts are still decimal/UI values (SDP scales by the token’s decimals — never pass base units).

Minting to Users

When a user earns points, call mint! against their custody wallet. That’s the whole call — the engine records a TokenMint audit row, enqueues a never-retried background job, and returns immediately:

# e.g. in a controller or a service, when the user earns
token = Solrengine::Sdp::Token.first
return unless user.wallet_ready?           # the engine drives pending → provisioning → ready

token.mint!(destination: user.wallet_address, amount: 10, memo: "signup-bonus")

You don’t write the job or the idempotency guard yourself: SDP has no idempotency key, so the engine writes the audit row, claims it atomically, and sends a single POST that is never re-sent. A crash or a double-fire can’t mint twice. (solrengine-sdp 0.3 sets sdp_wallet_id and wallet_address on the user once the wallet reaches readyprovision_wallet! on signup drives that automatically.)

Redeeming: Burn, Signed by the Holder

Redeeming a reward burns the user’s points. A burn is signed by the holder’s wallet — burn authority is the token-account owner, not the mint authority — so you pass the user’s wallet as both source and signing_wallet_id:

token.burn!(
  source:            user.wallet_address,
  signing_wallet_id: user.sdp_wallet_id,
  amount:            reward.points_cost
)

Same money-path posture as mint: an audit row, an atomic claim, a single never-retried POST.

Live Balances Without Polling

bin/sdp_watcher (started by the install generator’s Procfile.dev entry) holds one subscription per wallet-ready user. When a mint or burn settles on chain, the engine re-fetches the balance from SDP and pushes a Turbo Stream — wire up what to re-render in the initializer:

# config/initializers/solrengine_sdp.rb
c.broadcast_targets = [
  {
    name:   :balance,
    fetch:  ->(user) { Solrengine::Sdp.client.wallet_balances(user.sdp_wallet_id) },
    render: ->(user, balances) {
      Turbo::StreamsChannel.broadcast_replace_to(
        user.realtime_stream, target: "balance",
        partial: "dashboard/balance", locals: { balances: balances }
      )
    }
  }
]

These lambdas run in the watcher process — outside any request, so no Current or session; everything comes from the user argument. The member’s balance updates in the browser the moment the mint settles. No polling loop, no manual ActionCable channel.

Compare: In Next.js

For the same token genesis, a Next.js backend would import a keypair from a secret, build a createMint call with @solana/spl-token, sign with the fee payer, send with sendAndConfirmTransaction, and implement its own retry logic on timeout. There is no managed custody layer — the keypair lives in an environment variable, and its compromise means losing the treasury wallet.

The SDP path moves that surface out of your app entirely. The tradeoff is a dependency on a running SDP instance and a managed custody provider.

Prerequisites

SDP is pre-mainnet and devnet-oriented. You need:

  • A self-hosted SDP instance or managed environment (see the self-hosting guide)
  • A managed custody provider (e.g. Privy) — local custody is single-root-wallet and rejects per-user provisioning
  • Kora as the fee-payment provider — the native adapter cannot submit transfers
  • A Helius-class RPC endpoint for SPL token balance indexing (public devnet RPC lacks it)

None of this requires holding a private key in your application.

What This Enables

Rails has always been the right stack for multi-step transactional flows: background jobs, mailers, real-time updates, admin dashboards. Token issuance is a transactional flow that happens to produce an on-chain asset instead of a database row.

The practical applications are any use case where you want to issue internal currency without requiring users to install a wallet extension: community rewards, loyalty tokens, in-app credits, contributor points. Users sign up with an email address. Their balance updates in real time. They earn and redeem. The blockchain is infrastructure, not UX.

It’s exactly how Kudos, the issuance reference app, is built — and the getting-started tutorial walks from rails new to that app. solana-sdp 0.2.0 and solrengine-sdp 0.3.0 are on RubyGems now.

← All posts