Skip to main content
Action Codes create a simple bridge between users and apps without requiring wallet connections in your app.

The core idea

Instead of your app connecting to the user’s wallet directly, the user generates a short-lived code in their wallet app. Your app uses this code to request actions, and the user approves them in their wallet.

The lifecycle

Every action code goes through these stages:
1

Generated

User visits actioncode.app and generates a code tied to their wallet.Code is: 8 digits, valid for ~2 minutes, tied to one wallet
2

Shared

User copies the code and shares it with your app — via text input, chat message, or any other method.Your app: Receives the code and calls client.resolve(code) to verify it
3

Attached

Your app attaches an action (transaction or message) to the code.Your app: Calls client.attachTransaction(code, tx) or client.attachMessage(code, msg)User sees: The pending request appears in actioncode.app
4

Approved

User reviews the request in actioncode.app and approves it with their wallet.User wallet: Signs the transaction or message
5

Finalized

Your app receives the signed result.Your app: observeStatus() returns finalizedSignature or signedMessage

Status flow

StatusMeaning
pendingCode exists, nothing attached yet
attachedAction attached, waiting for user approval
finalizedUser approved, signature available
expiredCode timed out (codes live ~2 minutes)

Where users get codes

Users generate codes at actioncode.app.
1

Open in wallet browser

User opens actioncode.app in their wallet’s built-in browser (Phantom, Solflare, Backpack, etc.)
2

Connect wallet

User connects their wallet to actioncode.app
3

Get code

User taps “Get Code” and receives an 8-digit code
4

Share code

User copies the code and shares it with your app
5

Approve requests

When your app attaches an action, user sees it in actioncode.app and can approve
Users should keep actioncode.app open while waiting for requests. When you attach an action, it appears there for them to review and approve.

Why this design?

BenefitHow
No wallet in your appYou never handle private keys or wallet connections
Works everywhereBots, CLIs, embedded apps — anywhere you can accept 8 digits
User stays in controlThey approve each action explicitly in their wallet
Short-livedCodes expire in ~2 minutes, limiting exposure
One-time useEach code can only be used once

SDK methods by stage

StageSDK MethodWhat it does
Verifyclient.resolve(code)Check code validity, get wallet address
Attachclient.attachTransaction(code, tx)Attach a transaction for signing
Attachclient.attachMessage(code, msg)Attach a message for signing
Watchclient.observeStatus(code)Stream status updates
Checkclient.getStatus(code)One-time status check

Example: Sign-in flow

Here’s how you might use Action Codes for wallet-based authentication:
import { ActionCodesClient } from '@actioncodes/sdk'

const client = new ActionCodesClient({
  authToken: process.env.ACTION_CODES_TOKEN
})

async function signIn(code: string) {
  // 1. Verify the code and get the wallet address
  const actionCode = await client.resolve(code)
  const walletAddress = actionCode.pubkey

  // 2. Create a sign-in message with a nonce
  const nonce = crypto.randomUUID()
  const message = `Sign in to MyApp\nWallet: ${walletAddress}\nNonce: ${nonce}`

  // 3. Attach it to the code
  await client.attachMessage(code, message)

  // 4. Wait for user to sign
  for await (const status of client.observeStatus(code)) {
    if (status.signedMessage) {
      // 5. Verify the signature matches the wallet
      const isValid = verifySignature(status.signedMessage, message, walletAddress)

      if (isValid) {
        // Create session for this wallet
        return createSession(walletAddress)
      }
    }
  }

  throw new Error('Sign-in failed or timed out')
}

Next steps