skills/controller-backend/SKILL.md
Integrate Cartridge Controller into backend services using Node.js, Rust, or headless mode. Use when building server-side applications, game backends, automated bots, or any non-browser environment that needs to execute Starknet transactions. Covers SessionProvider for Node.js, Rust SDK setup, and headless Controller with custom signing keys.
npx skillsauth add cartridge-gg/docs controller-backendInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Integrate Controller into server-side applications and automated systems.
Sessions enable pre-approved transactions without manual user approval, making them ideal for automated backends.
Uses SessionProvider with file-based session storage.
pnpm add @cartridge/controller starknet
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);
basePath specifies session storage directory (must be writable)Uses account_sdk crate for native Rust integration.
[dependencies]
account_sdk = { git = "https://github.com/cartridge-gg/controller-rs.git", package = "account_sdk" }
starknet = "0.10"
tokio = { version = "1", features = ["full"] }
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();
}
For server-side execution with custom signing keys.
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;
}
Never commit private keys. Use:
// Good
const privateKey = process.env.PRIVATE_KEY;
// Bad - NEVER do this
const privateKey = "0x1234...";
Use SessionConnector with Telegram WebApp context.
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",
});
For Telegram mini-apps using Next.js:
// next.config.js
module.exports = {
webpack: (config) => {
config.experiments = {
...config.experiments,
asyncWebAssembly: true,
};
return config;
},
};
development
Configure authentication methods for Cartridge Controller including passkeys, social login, and external wallets. Use when implementing user authentication, adding multiple signers for account recovery, customizing signup options, or integrating external wallets like MetaMask or Phantom. Covers WebAuthn passkeys, Google/Discord/Twitter OAuth, wallet connections, and dynamic authentication flows.
data-ai
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.
testing
Configure session keys and policies for Cartridge Controller to enable gasless, pre-approved transactions. Use when defining contract interaction policies, setting spending limits, configuring signed message policies, or implementing error handling for session-based transactions. Covers SessionPolicies type, policy definitions, verified sessions, and error display modes.
development
Integrate Cartridge Controller into React applications using starknet-react. Use when building React/Next.js web apps with Controller, setting up StarknetConfig provider, using hooks like useConnect/useAccount, or implementing wallet connection components. Covers ControllerConnector setup, provider configuration, and transaction execution patterns.