controller-backend
Controller Backend Integration
Integrate Controller into server-side applications and automated systems.
Sessions enable pre-approved transactions without manual user approval, making them ideal for automated backends.
Node.js
Uses SessionProvider with file-based session storage.
Installation
pnpm add @cartridge/controller starknet
Setup
import SessionProvider, {
ControllerError,
} from "@cartridge/controller/session/node";
import { constants } from "starknet";
import path from "path";
const ETH_CONTRACT =
"0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7";
async function main() {
const storagePath =
process.env.CARTRIDGE_STORAGE_PATH ||
path.join(process.cwd(), ".cartridge");
const provider = new SessionProvider({
rpc: "https://api.cartridge.gg/x/starknet/mainnet",
chainId: constants.StarknetChainId.SN_MAIN,
policies: {
contracts: {
[ETH_CONTRACT]: {
methods: [
{
name: "approve",
entrypoint: "approve",
spender: "0x1234567890abcdef1234567890abcdef12345678",
amount: "0xffffffffffffffffffffffffffffffff",
description: "Approve spending of tokens",
},
{ name: "transfer", entrypoint: "transfer" },
],
},
},
},
basePath: storagePath,
});
try {
const account = await provider.connect();
if (account) {
console.log("Account address:", account.address);
// Example: Transfer ETH
const amount = "0x0";
const recipient = account.address; // Replace with actual recipient
const result = await account.execute([
{
contractAddress: ETH_CONTRACT,
entrypoint: "transfer",
calldata: [recipient, amount, "0x0"],
},
]);
console.log("Transaction hash:", result.transaction_hash);
} else {
console.log("Please complete the session creation in your browser");
}
} catch (error: unknown) {
const controllerError = error as ControllerError;
if (controllerError.code) {
console.error("Session error:", {
code: controllerError.code,
message: controllerError.message,
data: controllerError.data,
});
} else {
console.error("Session error:", error);
}
}
}
main().catch(console.error);
Notes
basePathspecifies session storage directory (must be writable)- First run requires browser-based session creation
- Session persists between runs
Rust
Uses account_sdk crate for native Rust integration.
Installation
[dependencies]
account_sdk = { git = "https://github.com/cartridge-gg/controller-rs.git", package = "account_sdk" }
starknet = "0.10"
tokio = { version = "1", features = ["full"] }
Setup
use account_sdk::{controller::Controller, signers::Signer};
use starknet::{
accounts::Account,
providers::Provider,
signers::SigningKey,
core::types::FieldElement,
};
use std::env;
#[tokio::main]
async fn main() {
// Load private key from environment
let private_key = env::var("PRIVATE_KEY")
.expect("PRIVATE_KEY must be set");
let owner = Signer::Starknet(
SigningKey::from_secret_scalar(
FieldElement::from_hex_be(&private_key).unwrap()
)
);
let rpc_url = "https://api.cartridge.gg/x/starknet/mainnet";
let provider = Provider::try_from(rpc_url).unwrap();
let chain_id = provider.chain_id().await.unwrap();
let controller = Controller::new(
"my_app".to_string(),
"username".to_string(),
FieldElement::from_hex_be("0xCLASS_HASH").unwrap(),
rpc_url.parse().unwrap(),
owner,
FieldElement::from_hex_be("0xCONTROLLER_ADDRESS").unwrap(),
chain_id,
);
// Deploy if needed
controller.deploy().await.unwrap();
// Execute transaction
let call = starknet::core::types::FunctionCall {
contract_address: FieldElement::from_hex_be("0xCONTRACT").unwrap(),
entry_point_selector: starknet::core::utils::get_selector_from_name("transfer").unwrap(),
calldata: vec![FieldElement::from(123)],
};
controller.execute(vec![call], None).await.unwrap();
}
Headless Controller
For server-side execution with custom signing keys.
Use Cases
- Single owner, multiple accounts
- Automated game backends (bots, NPCs)
- Custom key management requirements
C++ Example
Warning: Never commit private keys. Use environment variables or secret managers.
#include "controller.hpp"
#include <cstdlib>
int main() {
// Load from environment - NEVER hardcode!
const char* pk = std::getenv("PRIVATE_KEY");
if (!pk) {
std::cerr << "PRIVATE_KEY not set" << std::endl;
return 1;
}
std::string private_key(pk);
auto owner = controller::Owner::init(private_key);
auto ctrl = controller::Controller::new_headless(
"my_app",
"bot_account",
controller::get_controller_class_hash(controller::Version::kLatest),
"https://api.cartridge.gg/x/starknet/mainnet",
owner,
"0x534e5f4d41494e" // SN_MAIN
);
// Register new account onchain
ctrl->signup(
controller::SignerType::kStarknet,
std::nullopt,
std::nullopt
);
// Execute transaction
controller::Call call{
"0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7",
"transfer",
{"0xRECIPIENT", "0x100", "0x0"}
};
std::string tx_hash = ctrl->execute({call});
std::cout << "Transaction: " << tx_hash << std::endl;
return 0;
}
Security Best Practices
Never commit private keys. Use:
- Environment variables
- Secret managers (AWS Secrets Manager, HashiCorp Vault)
- Hardware security modules (HSM)
// Good
const privateKey = process.env.PRIVATE_KEY;
// Bad - NEVER do this
const privateKey = "0x1234...";
Telegram Mini-Apps
Use SessionConnector with Telegram WebApp context.
Integration Flow
- Configure session policies
- Set up SessionProvider with cloud storage
- Handle authentication redirects
- Execute transactions via session
Setup
import { SessionConnector } from "@cartridge/connector";
import { constants } from "starknet";
const policies = {
contracts: {
"0x...": {
methods: [{ name: "action", entrypoint: "action" }],
},
},
};
const connector = new SessionConnector({
policies,
rpc: "https://api.cartridge.gg/x/starknet/mainnet",
chainId: constants.StarknetChainId.SN_MAIN,
redirectUrl: window.Telegram?.WebApp?.initData
? "https://t.me/MyBot/app"
: "https://myapp.com/callback",
});
Next.js WebAssembly Configuration
For Telegram mini-apps using Next.js:
// next.config.js
module.exports = {
webpack: (config) => {
config.experiments = {
...config.experiments,
asyncWebAssembly: true,
};
return config;
},
};
More from cartridge-gg/docs
controller-setup
Integrate Cartridge Controller wallet into Starknet applications. Use when setting up Controller for the first time, installing packages, configuring chains/RPC endpoints, or troubleshooting basic integration issues. Covers installation, Controller instantiation, ControllerConnector vs SessionConnector choice, chain configuration, and package compatibility.
69controller-native
Integrate Cartridge Controller into native mobile applications (iOS, Android, React Native) and web wrappers (Capacitor). Use when building mobile games or apps that need Controller wallet functionality with local keypair signing. Covers SessionConnector for native auth flows, Controller.c FFI bindings, passkey configuration with Apple App Site Association, and platform-specific setup.
58slot-deploy
Create, update, and manage Slot deployments for Katana and Torii services.
57slot-paymaster
Set up and manage Slot paymasters to sponsor transaction fees for gasless user experiences.
56create-a-plan
Conduct a focused technical planning interview to produce an implementable, parallelizable plan or spec with clear dependencies, risks, and open questions.
56cartridge-vrng
Integrate Cartridge's verifiable random number generator (vRNG) into onchain games.
1