Skip to main content
gen v0.14.0get-account’s response no longer exposes a flat token_holder_component_id; the holder lives under result.account.components[] with component_type_index == 2. The CLI tab below uses wallet transfer, which wraps the build / sign / submit primitive and resolves the holder for you. The Rust SDK tab shows the primitive directly, because ActivationPayload::builder() still gives you full control over each call.
This is the lesson where you learn the one workflow that powers every state change on the Grid: build an unsigned activation → sign and submit it. Every contract call you’ll ever make follows this same shape. A transfer is just the smallest meaningful example.
Prerequisites · Your first account + Fund from the faucet done. $ALICE is funded. You’re back on local (or still on DevNet — either works).

Running against local or DevNet

Pick the tab for the environment you’re targeting and paste once. The commands further down the lesson use $SERVER_URL and (on DevNet) the persisted auth header, so they don’t need per-environment edits.
Make sure a local validator is running (gen service validator local --embedded-config in another terminal), then:
export SERVER_URL="http://127.0.0.1:30001"

Set up a second account

We need a recipient. Repeat lesson 1 with a new name:
"$GEN" wallet --rpc-url "$SERVER_URL" --json create bob --plaintext
BOB=$("$GEN" wallet --rpc-url "$SERVER_URL" --json show bob | jq -r '.result.account')
echo "$BOB"
Bob has no balance and no on-chain presence yet. The transfer we’re about to do will give him both.

Steps

1

Send 0.5 USDG from alice to bob

The CLI ships wallet transfer as a one-liner that wraps the build / sign / submit primitive internally — it resolves both accounts’ token- holder components, encodes the transfer_with_confirmation call, signs it with the source wallet, and submits the activation.
"$GEN" wallet --rpc-url "$SERVER_URL" --json transfer \
  --wallet alice \
  --to "$BOB" \
  --amount 500000000 \
  --yes
Flags:
  • --to "$BOB" — recipient account in grd@... form (NOT a token-holder component; the CLI resolves that for you).
  • --amount — subunits, same convention as the faucet (1_000_000_000 subunits = 1 USDG).
  • --yes — skip the interactive confirmation prompt (useful in scripts).
Behind this one command, the CLI is doing exactly the build-activation → sign-and-submit-activation flow you’ll see in the Rust tab. Splitting those two steps is how Grid lets you build agentic systems where the signer (a wallet the user controls) doesn’t have to trust the builder (your backend, an MCP server). The Rust example below shows the primitives directly.
2

Confirm the move

"$GEN" client --rpc-url "$SERVER_URL" --json balance --account "$ALICE"
"$GEN" client --rpc-url "$SERVER_URL" --json balance --account "$BOB"
Alice should be down 500 000 000 subunits; Bob should be up the same. The universe is in balance.

What just happened

You executed the only write pattern the Grid has:
build-activation  →  sign-and-submit-activation
   (off-chain)       (one round-trip on-chain)
Every contract call in the deploy and call chapters, every transfer your production app will ever do — they all follow this exact shape. Internalize it once.
Receipt · 1 activation · 0.001¢ USDG · ~10 ms · And it’ll cost 0.001¢ next month, next quarter, next year. The price doesn’t move because the unit is USDG, not GEN.

The non-volatility moment, plainly

Imagine you’re building an agent that does 1 000 transfers per day. On chains that price gas in native token:
  • A 3× price spike means a 3× cost spike, with no warning.
  • You can either over-provision (waste capital) or under-provision (your agent stalls).
  • You spend engineering time building “wait for cheap gas” heuristics.
On Grid, 1 000 transfers/day is 1 000 × 0.001¢ = 1¢ per day. That number does not move. Budgeting becomes arithmetic, not forecasting.

From the Rust tab

The CLI tab and the Rust tab hit the same JSON-RPC. Two things the Rust path lets you control that wallet transfer doesn’t:

Authenticating to DevNet

DevNet’s RPC requires a bearer token. Build a ClientConfig so every request carries the Authorization header:
use std::collections::BTreeMap;
use client_sdk::{ClientConfig, GenClient};

let mut headers = BTreeMap::new();
headers.insert(
    "authorization".into(),
    format!("Bearer {}", std::env::var("GEN_DEVNET_BEARER_TOKEN")?),
);

let config = ClientConfig::builder()
    .server_url("<DEVNET_RPC_URL>".into())
    .headers(headers)
    .build();

let client = GenClient::new_http_with_rpc_config(config)?;
Header names are lowercased. Read the JWT from a secret store (env var, secret manager, KMS); don’t hardcode it.

The SdkError variants you’ll see in production

sign_and_submit_and_wait_activation_with_options returns Result<TraceOutcome, SdkError>. The variants worth branching on:
  • SdkError::Transport(_) — endpoint unreachable, TLS failed, or timeout. Retry with backoff, or fail over to a backup endpoint.
  • SdkError::Server { .. } — endpoint replied with HTTP error. 401/403 means the JWT is missing or expired (rotate it). 429 means rate-limited (back off).
  • SdkError::ActivationPolling(_) — submit succeeded but the trace took too long to finalize. Re-fetch by activation_id instead of resubmitting; duplicate submissions cost gas and may be rejected once valid_until_block lapses.
  • SdkError::EddsaSigner(_) — the signer failed (KMS down, key locked, hardware wallet timeout). The original error is preserved as the source.
  • Activation finalized with non-success status. This is not an Err; the call returns Ok(outcome) with the failure recorded in the trace (insufficient balance, contract-side error, etc.). Always inspect outcome before treating the transfer as done.
Insufficient balance surfaces in the activation status, not as a transport-level error; the validator simulates the call and records the failure on chain. For production deployments where the key must stay outside the process, see Sign with an external KMS.

Troubleshooting

Check that --amount in subunits is what you think it is. 1 USDG is 1_000_000_000 subunits — easy to lose a zero.
You resolved the holder component for a contract that doesn’t expose that function. The default token-holder used by the faucet does — but custom token contracts may use a different name. client get-account shows the available methods per component.

What’s next

Deploy a contract from ABI

Push, deploy, and install the simple-token contract you built in the Contract Development workshop.