# How It Works
Source: https://docs.actioncodes.org/how-it-works
Understand the Action Codes flow
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.
```mermaid theme={null}
sequenceDiagram
participant User as User Wallet
participant App as Your App
participant AC as actioncode.app
User->>AC: Generate code
AC-->>User: 8-digit code
User->>App: Share code
App->>App: Attach action to code
User->>AC: See pending request
User->>AC: Approve with wallet
AC-->>App: Signed result
```
## The lifecycle
Every action code goes through these stages:
User visits [actioncode.app](https://actioncode.app) and generates a code tied to their wallet.
**Code is:** 8 digits, valid for \~2 minutes, tied to one wallet
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
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
User reviews the request in actioncode.app and approves it with their wallet.
**User wallet:** Signs the transaction or message
Your app receives the signed result.
**Your app:** `observeStatus()` returns `finalizedSignature` or `signedMessage`
## Status flow
```mermaid theme={null}
stateDiagram-v2
[*] --> pending: Code generated
pending --> attached: Action attached
attached --> finalized: User approves
attached --> expired: Timeout (~2 min)
pending --> expired: Timeout (~2 min)
finalized --> [*]
expired --> [*]
```
| Status | Meaning |
| ----------- | ------------------------------------------ |
| `pending` | Code exists, nothing attached yet |
| `attached` | Action attached, waiting for user approval |
| `finalized` | User approved, signature available |
| `expired` | Code timed out (codes live \~2 minutes) |
## Where users get codes
Users generate codes at **[actioncode.app](https://actioncode.app)**.
User opens actioncode.app in their wallet's built-in browser (Phantom, Solflare, Backpack, etc.)
User connects their wallet to actioncode.app
User taps "Get Code" and receives an 8-digit code
User copies the code and shares it with your app
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?
| Benefit | How |
| ------------------------- | ------------------------------------------------------------ |
| **No wallet in your app** | You never handle private keys or wallet connections |
| **Works everywhere** | Bots, CLIs, embedded apps — anywhere you can accept 8 digits |
| **User stays in control** | They approve each action explicitly in their wallet |
| **Short-lived** | Codes expire in \~2 minutes, limiting exposure |
| **One-time use** | Each code can only be used once |
## SDK methods by stage
| Stage | SDK Method | What it does |
| ------ | ------------------------------------ | --------------------------------------- |
| Verify | `client.resolve(code)` | Check code validity, get wallet address |
| Attach | `client.attachTransaction(code, tx)` | Attach a transaction for signing |
| Attach | `client.attachMessage(code, msg)` | Attach a message for signing |
| Watch | `client.observeStatus(code)` | Stream status updates |
| Check | `client.getStatus(code)` | One-time status check |
## Example: Sign-in flow
Here's how you might use Action Codes for wallet-based authentication:
```typescript theme={null}
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
Get integrated in 5 minutes
Full API reference
Real-world examples
How the protocol works internally
# Action Codes
Source: https://docs.actioncodes.org/index
Let users approve blockchain actions without connecting their wallet to your app
## What are Action Codes?
Action Codes are **short-lived, one-time codes** that let users approve blockchain actions from anywhere — without connecting their wallet to your app.
Generate a code in your wallet, share it, and approve requests as they come in.
Request approvals via a simple SDK. No wallet integration needed in your app.
## How it works
User opens [actioncode.app](https://actioncode.app) in their wallet browser and gets an 8-digit code.
User pastes the code into your app, bot, or anywhere you accept it.
Your app attaches a transaction or message to the code via the SDK.
User sees the request in [actioncode.app](https://actioncode.app) and signs with their wallet.
Your app receives the signed transaction or message.
## See it in code
```typescript theme={null}
import { ActionCodesClient } from '@actioncodes/sdk'
const client = new ActionCodesClient({
authToken: process.env.ACTION_CODES_TOKEN
})
// 1. User gives you their code (e.g., "48291037")
const code = userInput
// 2. Resolve the code to verify it's valid
const actionCode = await client.resolve(code)
console.log('Code belongs to wallet:', actionCode.pubkey)
// 3. Attach a transaction for them to sign
await client.attachTransaction(code, serializedTransaction)
// 4. Wait for user to approve in their wallet
for await (const status of client.observeStatus(code)) {
if (status.finalizedSignature) {
console.log('Approved! Signature:', status.finalizedSignature)
break
}
}
```
**Need an auth token?** Request one by DMing [@beharefe](https://t.me/beharefe) on Telegram or emailing [gm@actioncodes.org](mailto:gm@actioncodes.org).
**Where do users get codes?** Users visit [actioncode.app](https://actioncode.app) in their Solana wallet's browser (like Phantom or Solflare), connect their wallet, and get a code. The code is valid for about 2 minutes.
## When to use Action Codes
| Use Case | How it works |
| ---------------------- | -------------------------------------------------------------------- |
| **Chat bots** | User sends code to bot, bot attaches action, user approves in wallet |
| **Embedded apps** | No wallet extension needed — just accept a code |
| **Cross-device** | Start on desktop, approve on mobile wallet |
| **Backend services** | Server requests approval, user approves asynchronously |
| **Telegram / Discord** | Bots can request signatures without OAuth or deep links |
## What it replaces
| Traditional Approach | With Action Codes |
| -------------------------- | -------------------- |
| Wallet connect modals | User enters 8 digits |
| OAuth redirects | No redirects needed |
| QR code scanning | Just type a code |
| Deep links (break easily) | Works everywhere |
| Browser extension required | No extension needed |
## Get started
Integrate in 5 minutes with our SDK
Understand the flow in detail
Try it hands-on with live demos
Full SDK method documentation
***
**Action Codes Protocol** is open-source (Apache-2.0).
Won 4th place in infrastructure track at [Colosseum Breakout Hackathon](https://arena.colosseum.org/projects/explore/one-time-action-codes-1).
For integrations or support, contact [gm@actioncodes.org](mailto:gm@actioncodes.org) or DM [@beharefe](https://t.me/beharefe) on Telegram.
# Playground
Source: https://docs.actioncodes.org/playground/index
Try Action Codes hands-on
Test Action Codes directly in your browser. These demos show real interactions with the Action Codes relayer.
## Before you start
You'll need an action code. Here's how to get one:
Use Phantom, Solflare, Backpack, or any Solana wallet with a built-in browser
In your wallet's browser, navigate to [actioncode.app](https://actioncode.app)
Connect your wallet and tap "Get Code" — you'll get an 8-digit code
Paste the code into any demo, then approve requests in actioncode.app
Codes expire in \~2 minutes. Get a fresh code right before using a demo.
## Available demos
Attach a custom message and sign it with your wallet
More interactive demos coming soon at [actioncodes.org/demo](https://actioncodes.org/demo)
## What you'll learn
Each demo shows:
* How codes are resolved and verified
* How actions are attached to codes
* How the approval flow works
* What data you get back after approval
## SDK calls used
| Demo | SDK Methods |
| ------------ | --------------------------------------------------- |
| Sign Message | `resolve()` → `attachMessage()` → `observeStatus()` |
All demos use `@actioncodes/sdk` — the same code you'd use in your own app.
# Sign Message
Source: https://docs.actioncodes.org/playground/sign-message
Sign a message using an action code
This demo shows how message signing works with Action Codes. You'll attach a message to your code and sign it with your wallet.
This only demonstrates the signing flow. No transactions are sent — you're just signing a message.
## How to use this demo
Open [actioncode.app](https://actioncode.app) in your Solana wallet's browser (Phantom, Solflare, etc.), connect your wallet, and tap "Get Code"
Paste your 8-digit code and type the message you want to sign
Go back to actioncode.app — you'll see the signing request. Approve it with your wallet.
Once approved, the signed message appears below
Codes expire in \~2 minutes. Get a fresh code right before using this demo.
***
***
## What's happening
This demo calls the Action Codes SDK:
```typescript theme={null}
// 1. Resolve the code to verify it
const actionCode = await client.resolve(code)
// 2. Attach the message
await client.attachMessage(code, message)
// 3. Wait for user approval
for await (const status of client.observeStatus(code)) {
if (status.signedMessage) {
// User signed the message!
}
}
```
For the full SDK reference, see [SDK Methods](/sdk/methods).
# ActionCode
Source: https://docs.actioncodes.org/protocol/components/actioncode
The `ActionCode` class represents an action code in the Action Codes Protocol. It encapsulates all the data and logic for a single action code, including its status, metadata, transaction details, and utility methods for validation and display.
***
## Type Definitions
### ActionCodeStatus
```ts theme={null}
type ActionCodeStatus = 'pending' | 'resolved' | 'finalized' | 'expired' | 'error';
```
Represents the status of an action code.
***
### ActionCodeMetadata
```ts theme={null}
interface ActionCodeMetadata {
description?: string;
params?: Record;
}
```
Metadata for the action code, including an optional description and parameters.
***
### ActionCodeTransaction
```ts theme={null}
interface ActionCodeTransaction {
transaction?: string; // Solana: base64 string
txSignature?: string; // Solana signature
txType?: string; // Transaction type for categorization
message?: string; // For sign-only mode: the message to be signed
signedMessage?: string; // For sign-only mode: the signed message or signature
intentType?: 'transaction' | 'sign-only'; // Explicit intent type
}
```
Represents transaction or message data attached to an action code.
***
### ActionCodeFields
```ts theme={null}
interface ActionCodeFields {
code: string;
prefix: string;
pubkey: string;
timestamp: number;
signature: string;
chain: string; // e.g., 'solana'
transaction?: ActionCodeTransaction;
metadata?: ActionCodeMetadata;
expiresAt: number;
status: ActionCodeStatus;
}
```
All fields required to construct an ActionCode instance.
***
## ActionCode Class
### Constructor
```ts theme={null}
new ActionCode(fields: ActionCodeFields)
```
Creates a new ActionCode instance from the provided fields.
***
### Static Methods
#### fromPayload
```ts theme={null}
static fromPayload(input: ActionCodeFields): ActionCode
```
Creates an ActionCode from a plain object. Throws if required fields are missing.
#### fromEncoded
```ts theme={null}
static fromEncoded(encoded: string): ActionCode
```
Creates an ActionCode from a base64-encoded string.
***
### Instance Properties & Methods
#### encoded
```ts theme={null}
get encoded: string
```
Returns a base64-encoded string of the action code fields.
#### isValid
```ts theme={null}
isValid(protocol: ActionCodesProtocol): boolean
```
Checks if the action code is valid for the given protocol (signature, code format, not expired).
#### updateStatus
```ts theme={null}
updateStatus(status: ActionCodeStatus): void
```
Updates the status of the action code.
#### json
```ts theme={null}
get json: ActionCodeFields
```
Returns the raw fields as a plain object.
#### remainingTime
```ts theme={null}
get remainingTime: number
```
Milliseconds remaining until expiration (0 if expired).
#### expired
```ts theme={null}
get expired: boolean
```
Returns true if the code is expired.
#### chain
```ts theme={null}
get chain: string
```
Returns the chain identifier (e.g., 'solana').
#### status
```ts theme={null}
get status: ActionCodeStatus
```
Returns the current status of the action code.
#### code
```ts theme={null}
get code: string
```
Returns the 8-character action code string.
#### prefix
```ts theme={null}
get prefix: string
```
Returns the normalized prefix for the code.
#### pubkey
```ts theme={null}
get pubkey: string
```
Returns the user's public key.
#### transaction
```ts theme={null}
get transaction: ActionCodeTransaction | undefined
```
Returns the transaction data, if any.
#### metadata
```ts theme={null}
get metadata: ActionCodeMetadata | undefined
```
Returns the metadata object, if any.
#### description
```ts theme={null}
get description: string | undefined
```
Returns the human-readable description from metadata.
#### params
```ts theme={null}
get params: Record | undefined
```
Returns the parameters from metadata.
#### timestamp
```ts theme={null}
get timestamp: number
```
Returns the timestamp when the code was generated.
#### signature
```ts theme={null}
get signature: string
```
Returns the user's signature string.
#### displayString
```ts theme={null}
get displayString: string
```
Returns a formatted string for display (e.g., "PREFIX-XXXXXX (solana, pending)").
#### remainingTimeString
```ts theme={null}
get remainingTimeString: string
```
Returns a human-readable string for remaining time (e.g., "1m 30s remaining" or "Expired").
#### codeHash
```ts theme={null}
get codeHash: string
```
Returns the code hash (used as code ID in protocol meta).
#### intentType
```ts theme={null}
get intentType: 'transaction' | 'sign-only'
```
Returns the intent type for the action code.
***
## Example
```ts theme={null}
import { ActionCode } from "@actioncodes/protocol";
const fields: ActionCodeFields = {
code: "ABC12345",
prefix: "DEFAULT",
pubkey: "...",
timestamp: Date.now(),
signature: "...",
chain: "solana",
expiresAt: Date.now() + 60000,
status: "pending",
};
const actionCode = new ActionCode(fields);
console.log(actionCode.displayString); // e.g., "ABC12345 (solana, pending)"
```
***
# Adapter
Source: https://docs.actioncodes.org/protocol/components/adapter
The `BaseChainAdapter` class is an abstract base class for implementing chain-specific protocol meta operations. It defines the required interface for encoding, decoding, injecting, and validating protocol meta in blockchain transactions.
***
## Type Parameter
* `T`: The chain-specific transaction type handled by the adapter.
***
## Abstract Methods
### encodeMeta
```ts theme={null}
abstract encodeMeta(meta: ProtocolMetaV1): any
```
Encodes protocol meta for this chain.
***
### decodeMeta
```ts theme={null}
abstract decodeMeta(tx: T): ProtocolMetaV1 | null
```
Decodes protocol meta from a chain-specific transaction.
***
### injectMeta
```ts theme={null}
abstract injectMeta(serializedTx: string, meta: ProtocolMetaV1): string
```
Injects protocol meta into a serialized transaction string.
***
### validate
```ts theme={null}
abstract validate(tx: T, authorities: string[], expectedPrefix?: string): boolean
```
Validates a transaction with protocol meta and authority list.
***
### hasIssuerSignature
```ts theme={null}
abstract hasIssuerSignature(tx: T, issuer: string): boolean
```
Checks if the issuer has signed the transaction.
***
### detectTampering
```ts theme={null}
detectTampering(tx: T, authorities: string[], expectedPrefix: string = 'DEFAULT'): boolean
```
Detects tampered transactions by cross-checking metadata and signatures. (Default implementation provided.)
***
### validateTransactionIntegrity
```ts theme={null}
protected abstract validateTransactionIntegrity(tx: T, meta: ProtocolMetaV1): boolean
```
Chain-specific transaction integrity validation. Override for additional logic.
***
### getCodeSignatureMessage
```ts theme={null}
getCodeSignatureMessage(code: string, timestamp: number, prefix: string = PROTOCOL_CODE_PREFIX): string
```
Returns the code signature message for signing and verification.
***
### verifyCodeSignature
```ts theme={null}
abstract verifyCodeSignature(actionCode: ActionCode): boolean
```
Verifies the code signature for the action code. Chain-specific implementation required.
***
### signWithProtocolKey
```ts theme={null}
abstract signWithProtocolKey(actionCode: ActionCode, key: any): Promise
```
Signs the transaction with the protocol key using a callback approach. Chain-specific implementation required.
***
### verifyFinalizedTransaction
```ts theme={null}
abstract verifyFinalizedTransaction(tx: any, actionCode: ActionCode): boolean
```
Verifies the finalized transaction. Chain-specific implementation required.
***
### validateSignedMessage
```ts theme={null}
abstract validateSignedMessage(message: string, signedMessage: string, pubkey: string): boolean
```
Validates a signed message for sign-only mode. Chain-specific implementation required.
***
# CodeGenerator
Source: https://docs.actioncodes.org/protocol/components/codegen
The `CodeGenerator` class provides static utilities for generating, validating, and normalizing action codes and prefixes in the Action Codes Protocol. It ensures codes are deterministic, secure, and conform to protocol standards.
***
## Constants
* `TIME_WINDOW_MS`: Code validity window in milliseconds (default: protocol TTL)
* `CODE_DIGITS`: Number of digits in the code (default: 8)
* `MIN_PREFIX_LENGTH`: Minimum allowed prefix length
* `MAX_PREFIX_LENGTH`: Maximum allowed prefix length
***
## Static Methods
### validatePrefix
```ts theme={null}
static validatePrefix(prefix: string): boolean
```
Validates the format of a prefix. Returns `true` if valid, `false` otherwise.
***
### validateCodeFormat
```ts theme={null}
static validateCodeFormat(code: string): boolean
```
Validates that a code is in the correct format (prefix + exactly 8 digits). Returns `true` if valid.
***
### validateCodeDigits
```ts theme={null}
static validateCodeDigits(code: string): boolean
```
Checks that the numeric part of a code is exactly 8 digits. Returns `true` if valid.
***
### normalizePrefix
```ts theme={null}
static normalizePrefix(prefix: string): string
```
Normalizes a prefix (converts protocol default to empty string, validates others). Throws if invalid.
***
### generateCode
```ts theme={null}
static generateCode(pubkey: string, prefix?: string, timestamp?: number): { code: string; issuedAt: number; expiresAt: number }
```
Generates a deterministic 8-digit code based on public key, prefix, and timestamp. Returns the code, issuedAt, and expiresAt.
***
### deriveCodeHash
```ts theme={null}
static deriveCodeHash(pubkey: string, prefix?: string, timestamp?: number): string
```
Derives a full SHA-256 hash for storage or encryption key generation.
***
### getExpectedCode
```ts theme={null}
static getExpectedCode(pubkey: string, timestamp: number, prefix?: string): string
```
Returns the expected code for a given public key and timestamp.
***
### validateCode
```ts theme={null}
static validateCode(code: string, pubkey: string, timestamp: number, prefix?: string): boolean
```
Validates if a code matches the expected code for a given public key and timestamp.
***
### isValidTimestamp
```ts theme={null}
static isValidTimestamp(timestamp: number): boolean
```
Checks if a timestamp falls within the valid time window.
***
## Example
```ts theme={null}
import { CodeGenerator } from "@actioncodes/protocol";
const pubkey = "...";
const prefix = "OTA";
const timestamp = Date.now();
const { code, issuedAt, expiresAt } = CodeGenerator.generateCode(pubkey, prefix, timestamp);
console.log(code); // e.g., "OTA12345678"
const isValid = CodeGenerator.validateCode(code, pubkey, timestamp, prefix);
console.log(isValid); // true
```
***
# Protocol Meta
Source: https://docs.actioncodes.org/protocol/components/meta
The protocol meta format is used for structured code verification and memo/message parsing in the Action Codes Protocol. It encodes metadata about the code, its issuer, and its context in a compact string format.
***
## ProtocolMetaV1 Interface
```ts theme={null}
interface ProtocolMetaV1 {
version: string;
prefix: string;
initiator: string;
id: string;
iss?: string; // issuer (protocol authority)
params?: string;
}
```
Describes the structure of protocol meta information for code verification.
***
## ProtocolMetaParser Class
Utility class for parsing, serializing, and validating protocol meta strings.
### parse
```ts theme={null}
static parse(metaString: string): ProtocolMetaV1 | null
```
Parses a protocol meta string and returns a `ProtocolMetaV1` object, or `null` if invalid.
***
### serialize
```ts theme={null}
static serialize(meta: ProtocolMetaV1): string
```
Serializes a `ProtocolMetaV1` object into a protocol meta string.
***
### fromInitiator
```ts theme={null}
static fromInitiator(initiator: string, iss: string, prefix?: string, params?: string, timestamp?: number): ProtocolMetaV1
```
Creates a `ProtocolMetaV1` object from the initiator, issuer, prefix, optional params, and optional timestamp.
***
### validateCode
```ts theme={null}
static validateCode(meta: ProtocolMetaV1, timestamp?: number): boolean
```
Validates if a code matches the protocol meta (by comparing the code hash).
***
### validateMetaFromString
```ts theme={null}
static validateMetaFromString(metaString: string, timestamp?: number): boolean
```
Validates if a code matches the protocol meta by parsing from a string.
***
## Example
```ts theme={null}
import { ProtocolMetaParser } from "@actioncodes/protocol";
const metaString = "actioncodes:v=1&pre=CODE&ini=...&id=...&iss=...";
const meta = ProtocolMetaParser.parse(metaString);
if (meta) {
const isValid = ProtocolMetaParser.validateCode(meta);
console.log(isValid);
}
const serialized = ProtocolMetaParser.serialize(meta!);
console.log(serialized);
```
***
# Protocol
Source: https://docs.actioncodes.org/protocol/components/protocol
The `ActionCodesProtocol` class is the main entry point for the One-Time Action Code Protocol. It provides a unified interface for generating, validating, and managing action codes.
***
## ProtocolConfig Interface
```ts theme={null}
interface ProtocolConfig {
version: string;
defaultPrefix: string;
codeTTL: number;
codeLength: number;
maxPrefixLength: number;
minPrefixLength: number;
}
```
Defines the configuration options for the protocol instance.
***
## ActionCodesProtocol
### constructor
```ts theme={null}
new ActionCodesProtocol(config?: Partial)
```
Creates a new protocol instance with optional custom configuration.
***
### registerAdapter
```ts theme={null}
registerAdapter(adapter: BaseChainAdapter): void
```
Registers a chain adapter implementation.
***
### getRegisteredChains
```ts theme={null}
getRegisteredChains(): string[]
```
Returns an array of registered chain identifiers.
***
### isChainSupported
```ts theme={null}
isChainSupported(chain: string): boolean
```
Checks if a chain is supported.
***
### getChainAdapter
```ts theme={null}
getChainAdapter(chain: string): BaseChainAdapter | undefined
```
Returns the chain adapter for a given chain, or undefined if not registered.
***
### validateActionCode
```ts theme={null}
validateActionCode(actionCode: ActionCode): boolean
```
Validates an action code, checking intent type and required fields.
***
### createActionCode
```ts theme={null}
async createActionCode(
pubkey: string,
signFn: (message: string) => Promise,
chain: SupportedChain,
prefix?: string,
timestamp?: number
): Promise
```
Creates a new action code for a given public key, signing function, and chain.
***
### attachTransaction
```ts theme={null}
attachTransaction(
actionCode: ActionCode,
transaction: string,
issuer: string,
params?: string,
txType?: string
): ActionCode
```
Attaches a transaction to an action code with protocol meta injection.
***
### attachMessage
```ts theme={null}
attachMessage(
actionCode: ActionCode,
message: string,
params?: string,
messageType?: string
): ActionCode
```
Attaches a message to an action code (sign-only mode).
***
### finalizeActionCode
```ts theme={null}
finalizeActionCode(actionCode: ActionCode, signature: string): ActionCode
```
Finalizes an action code based on its intent type.
***
### createProtocolMeta
```ts theme={null}
createProtocolMeta(
actionCode: ActionCode,
issuer?: string,
params?: string,
timestamp?: number
): ProtocolMetaV1
```
Creates protocol meta for a transaction.
***
### encodeProtocolMeta
```ts theme={null}
encodeProtocolMeta(meta: ProtocolMetaV1, chain: string): any
```
Encodes protocol meta for a specific chain.
***
### decodeProtocolMeta
```ts theme={null}
decodeProtocolMeta(transaction: any, chain: string): ProtocolMetaV1 | null
```
Decodes protocol meta from a transaction.
***
### validateTransaction
```ts theme={null}
validateTransaction(
transaction: any,
chain: string,
authorities: string[],
expectedPrefix?: string
): boolean
```
Validates a transaction with protocol meta.
***
### validateTransactionTyped
```ts theme={null}
validateTransactionTyped(
transaction: T,
chain: string,
authorities: string[],
expectedPrefix?: string
): boolean
```
Type-safe transaction validation for specific chains.
***
### detectTampering
```ts theme={null}
detectTampering(
transaction: T,
chain: string,
authorities: string[],
expectedPrefix?: string
): boolean
```
Detects tampered transactions with type safety.
***
### decodeProtocolMetaTyped
```ts theme={null}
decodeProtocolMetaTyped(transaction: T, chain: string): ProtocolMetaV1 | null
```
Type-safe protocol meta decoding.
***
### getConfig
```ts theme={null}
getConfig(): ProtocolConfig
```
Returns the current protocol configuration.
***
### updateConfig
```ts theme={null}
updateConfig(updates: Partial): void
```
Updates the protocol configuration.
***
### static create
```ts theme={null}
static create(): ActionCodesProtocol
```
Creates a new protocol instance with default configuration.
***
### static createWithConfig
```ts theme={null}
static createWithConfig(config: Partial): ActionCodesProtocol
```
Creates a new protocol instance with custom configuration.
***
## Example
```ts theme={null}
import { ActionCodesProtocol } from "@actioncodes/protocol";
const protocol = ActionCodesProtocol.create();
protocol.registerAdapter(myChainAdapter);
const actionCode = await protocol.createActionCode(pubkey, signFn, "solana");
```
***
# Protocol Overview
Source: https://docs.actioncodes.org/protocol/overview
How Action Codes work under the hood
**This section is for advanced users.** You do NOT need to read this to use Action Codes. The [SDK](/sdk/methods) handles all protocol details automatically.
## When to read this
* You're building a **custom relayer**
* You're implementing a **new chain adapter**
* You're **auditing** the protocol security
* You want to understand **how codes are derived**
If you're just integrating Action Codes into your app, see the [Quick Start](/quickstart) instead.
***
## Performance
The protocol is designed for speed:
| Operation | Time |
| ------------------ | ------------------- |
| Code generation | \~1ms |
| Code validation | \~3ms |
| Memory footprint | Negligible |
| Network dependency | None for validation |
***
## Two Strategies
Action Codes supports two code generation strategies:
### Wallet Strategy (Default)
Direct code generation from a user's wallet. This is what most apps use.
```mermaid theme={null}
sequenceDiagram
participant W as Wallet
participant P as Protocol
participant R as Relayer
W->>P: Sign canonical message
P->>P: Generate code via HMAC-SHA256
P->>R: Publish code
R->>R: Validate & store
```
**How it works:**
* User's wallet signs a canonical message
* Code is derived using HMAC-SHA256 with the signature as entropy
* Codes are cryptographically bound to the wallet's public key
* Validation is immediate — no external dependencies
**Use cases:** Direct authentication, transaction signing, user interactions
### Delegation Strategy (Advanced)
Pre-authorize a delegated keypair to generate codes on behalf of a wallet. Enables relayer services and automated workflows.
```mermaid theme={null}
sequenceDiagram
participant W as User Wallet
participant D as Delegated Key
participant R as Relayer
W->>D: Sign delegation proof (once)
Note over D: Proof includes expiration
D->>R: Generate codes using proof
R->>R: Validate both signatures
```
**How it works:**
1. User signs a delegation proof specifying: delegated keypair, chain, expiration
2. Delegated keypair generates codes bound to the proof
3. Relayers validate both the delegation proof AND the code signature
**Security guarantees:**
* Stolen proofs cannot generate codes (require delegated private key)
* Relayers cannot generate codes (only validate)
* Cross-proof attacks are prevented through cryptographic binding
**Use cases:** Relayer services, automated trading bots, complex workflows
***
## Core Concepts
### Code derivation
Action Codes are deterministically derived:
```
code = HMAC-SHA256(signature, pubkey + timestamp)[0:8]
```
* **signature** — Wallet signature over canonical message (secret entropy)
* **pubkey** — User's wallet public key
* **timestamp** — Current time, rounded to 2-minute windows
This makes codes:
* **Unpredictable** — Cannot be guessed without the signature
* **Verifiable** — Can be validated with the signature
* **Time-bound** — Expire after \~2 minutes
### Canonical messages
Every code generation involves signing a deterministic JSON message:
```json theme={null}
{
"pubkey": "7gNqUuY5...",
"code": "48291037",
"timestamp": 1704067200
}
```
Deterministic serialization (sorted keys, no whitespace) prevents ambiguity.
### Protocol Meta
Metadata attached to transactions:
| Field | Description |
| ----- | --------------------------------- |
| `ver` | Protocol version |
| `id` | Code hash identifier |
| `int` | Intent owner (wallet public key) |
| `iss` | Issuer (optional, for delegation) |
| `p` | Parameters (optional) |
Maximum size: 512 bytes. When `iss` is present, both issuer and intent owner must sign.
***
## Architecture
```mermaid theme={null}
flowchart LR
W[Wallet] -->|1. Generate code| R[Relayer]
A[Your App] -->|2. Attach action| R
R -->|3. Show request| W
W -->|4. Approve| R
R -->|5. Return signature| A
```
### Components
| Component | Role |
| ----------------- | --------------------------------------------------------- |
| **Wallet** | Generates codes, signs transactions |
| **Relayer** | Validates codes, stores encrypted state, coordinates flow |
| **App** | Attaches actions, observes status |
| **Chain Adapter** | Chain-specific transaction handling |
### The Relayer
The relayer is a trusted intermediary that:
1. **Validates codes** — Verifies signature, timestamp, format
2. **Stores state** — Encrypted transaction/message payloads
3. **Coordinates flow** — Connects apps and wallets
4. **Enforces expiry** — Rejects expired codes
The official relayer is free to use and maintained by Action Codes.
***
## Security Model
| Property | How it's achieved |
| --------------------------- | ---------------------------------------------- |
| **Codes are unpredictable** | Derived from wallet signature (secret entropy) |
| **Codes are verifiable** | Signature can be verified against pubkey |
| **Codes are time-bound** | 2-minute windows, enforced by relayer |
| **Codes are one-time** | Relayer tracks usage |
| **Payloads are encrypted** | Code itself is the decryption key |
| **No on-chain state** | Everything is off-chain until finalization |
### Threat mitigations
| Threat | Mitigation |
| ------------------ | ----------------------------------------- |
| Code guessing | 8 digits + signature binding = infeasible |
| Replay attacks | Time windows + one-time use |
| Payload tampering | Encrypted with code-derived key |
| Relayer compromise | Relayer never has raw private keys |
| Delegation abuse | Time-limited proofs + dual signatures |
For full security details, see [Security & Determinism](/security-determinism).
***
## Using the Protocol Package
For low-level protocol access:
```bash theme={null}
npm install @actioncodes/protocol
```
```typescript theme={null}
import { ActionCodesProtocol, SolanaAdapter } from '@actioncodes/protocol'
// Initialize with configuration
const protocol = new ActionCodesProtocol({
codeLength: 8, // 6-24 digits
ttlMs: 120000, // 2 minutes
clockSkewMs: 5000 // Clock tolerance
})
// Register chain adapter
protocol.registerAdapter('solana', new SolanaAdapter())
// Generate a code (wallet strategy)
const actionCode = await protocol.generate(
'wallet',
userPublicKey,
'solana',
signFn
)
// Validate a code
protocol.validate('wallet', actionCode)
```
Most applications should use `@actioncodes/sdk` instead, which wraps the protocol with a simpler API and handles relayer communication.
***
## Chain Adapters
Adapters provide chain-specific functionality:
```typescript theme={null}
interface ChainAdapter {
// Protocol meta
createProtocolMetaIx(): TransactionInstruction
parseMeta(tx: Transaction): ProtocolMeta
// Verification
verifyTransactionMatchesCode(tx, code): boolean
verifyTransactionSignedByIntentOwner(tx, pubkey): boolean
// Attachment
attachProtocolMeta(tx, meta): Transaction
}
```
Currently supported: **Solana**
See [Adapter Reference](/protocol/components/adapter) for implementation details.
***
## Best Practices
1. **Set appropriate TTL** — Balance security vs. user experience
2. **Validate server-side** — Never trust client-only validation
3. **Use delegation carefully** — Set short expiration windows
4. **Monitor relayer activity** — Watch for unusual patterns
5. **Handle expiry gracefully** — Prompt users to regenerate codes
# Quick Start
Source: https://docs.actioncodes.org/quickstart
Integrate Action Codes in 5 minutes
This guide gets you from zero to working integration in 5 minutes.
## Prerequisites
* Node.js 18+ (or Bun)
* A user with a Solana wallet
* An auth token (request one by DMing [@beharefe](https://t.me/beharefe) on Telegram or emailing [gm@actioncodes.org](mailto:gm@actioncodes.org))
Users generate codes at [actioncode.app](https://actioncode.app) using their wallet. Your app receives and processes these codes — you don't need wallet access.
## Install the SDK
```bash npm theme={null}
npm install @actioncodes/sdk
```
```bash pnpm theme={null}
pnpm add @actioncodes/sdk
```
```bash yarn theme={null}
yarn add @actioncodes/sdk
```
```bash bun theme={null}
bun add @actioncodes/sdk
```
## Initialize the client
```typescript theme={null}
import { ActionCodesClient } from '@actioncodes/sdk'
const client = new ActionCodesClient({
authToken: process.env.ACTION_CODES_TOKEN
})
```
Set your auth token as an environment variable:
```bash theme={null}
export ACTION_CODES_TOKEN="your-auth-token"
```
## Accept a code from a user
When a user wants to interact with your app:
1. They visit [actioncode.app](https://actioncode.app) in their wallet browser
2. They connect their wallet and get an 8-digit code
3. They share that code with your app
```typescript theme={null}
// User provides their code (from actioncode.app)
const code = '48291037' // example code
// Resolve it to verify and get details
const actionCode = await client.resolve(code)
console.log('Valid code from wallet:', actionCode.pubkey)
console.log('Expires at:', actionCode.expiresAt)
```
## Attach an action
Now attach what you want the user to approve. Choose based on your use case:
```typescript theme={null}
import { Transaction, SystemProgram, PublicKey, Connection } from '@solana/web3.js'
// Build your transaction
const connection = new Connection('https://api.mainnet-beta.solana.com')
const { blockhash } = await connection.getLatestBlockhash()
const transaction = new Transaction({
recentBlockhash: blockhash,
feePayer: new PublicKey(actionCode.pubkey)
}).add(
SystemProgram.transfer({
fromPubkey: new PublicKey(actionCode.pubkey),
toPubkey: new PublicKey('RecipientAddressHere'),
lamports: 1_000_000 // 0.001 SOL
})
)
// Serialize and attach to the code
const serialized = transaction.serialize({
requireAllSignatures: false
}).toString('base64')
await client.attachTransaction(code, serialized)
```
```typescript theme={null}
// Create a message for the user to sign
const message = 'Sign in to MyApp - ' + new Date().toISOString()
// Attach it to the code
await client.attachMessage(code, message)
```
## Wait for approval
The user will see the pending request in [actioncode.app](https://actioncode.app) and can approve it with their wallet.
```typescript theme={null}
// Watch for the user to approve
for await (const status of client.observeStatus(code)) {
console.log('Current status:', status.status)
// Check for transaction signature
if (status.finalizedSignature) {
console.log('Transaction signed!')
console.log('Signature:', status.finalizedSignature)
break
}
// Check for signed message
if (status.signedMessage) {
console.log('Message signed!')
console.log('Signed message:', status.signedMessage)
break
}
}
```
## Complete example
Here's everything together — a simple flow where a user signs a message:
```typescript theme={null}
import { ActionCodesClient } from '@actioncodes/sdk'
const client = new ActionCodesClient({
authToken: process.env.ACTION_CODES_TOKEN
})
async function requestSignature(userCode: string) {
// 1. Verify the code
const actionCode = await client.resolve(userCode)
console.log(`Code valid for wallet: ${actionCode.pubkey}`)
// 2. Attach a message to sign
const message = `Verify ownership of ${actionCode.pubkey} at ${Date.now()}`
await client.attachMessage(userCode, message)
console.log('Message attached — waiting for user approval...')
// 3. Wait for signature
for await (const status of client.observeStatus(userCode)) {
if (status.signedMessage) {
console.log('Success! Signed message:', status.signedMessage)
return status.signedMessage
}
// Handle expiry
if (status.status === 'expired') {
throw new Error('Code expired before approval')
}
}
}
// Usage
const userCode = '48291037' // User provides this from actioncode.app
const signedMessage = await requestSignature(userCode)
```
## User flow summary
User opens [actioncode.app](https://actioncode.app) in their wallet browser (Phantom, Solflare, etc.) and taps "Get Code"
User copies the 8-digit code and pastes it into your app
You call `attachTransaction()` or `attachMessage()`
User sees the request in actioncode.app and taps "Approve" — their wallet signs it
`observeStatus()` returns the signature or signed message
## Next steps
Understand the lifecycle in detail
Full API reference
Copy-paste examples for common use cases
Try it interactively
# Sign a Message
Source: https://docs.actioncodes.org/recipes/sign-message
Request a user to sign a message for authentication or verification
Use message signing for authentication, verification, or any scenario where you need proof of wallet ownership without a transaction.
## The flow
1. User generates code at [actioncode.app](https://actioncode.app)
2. User shares code with your app
3. Your app creates a message and attaches it
4. User signs in their wallet
5. Your app gets the signed message
## Authentication example
```typescript theme={null}
import { ActionCodesClient } from '@actioncodes/sdk'
import { verify } from '@noble/ed25519' // or your preferred signature verification
const client = new ActionCodesClient({
authToken: process.env.ACTION_CODES_TOKEN
})
async function authenticateUser(userCode: string) {
// 1. Verify the code
const actionCode = await client.resolve(userCode)
const walletAddress = actionCode.pubkey
// 2. Create a unique sign-in message
const nonce = crypto.randomUUID()
const timestamp = Date.now()
const message = [
'Sign in to MyApp',
'',
`Wallet: ${walletAddress}`,
`Nonce: ${nonce}`,
`Timestamp: ${timestamp}`
].join('\n')
// 3. Attach the message
await client.attachMessage(userCode, message, {
description: 'Sign in to MyApp'
})
console.log('Sign-in request sent — waiting for approval...')
// 4. Wait for signature
for await (const status of client.observeStatus(userCode)) {
if (status.signedMessage) {
// 5. Verify the signature
const signatureBytes = Buffer.from(status.signedMessage, 'base64')
const messageBytes = Buffer.from(message)
const pubkeyBytes = Buffer.from(walletAddress, 'base64') // Adjust encoding as needed
const isValid = await verify(signatureBytes, messageBytes, pubkeyBytes)
if (isValid) {
console.log('Authentication successful!')
return {
wallet: walletAddress,
signedAt: timestamp,
nonce
}
} else {
throw new Error('Signature verification failed')
}
}
if (status.status === 'expired') {
throw new Error('Sign-in expired')
}
}
}
// Usage
const session = await authenticateUser('48291037')
console.log('Authenticated wallet:', session.wallet)
```
## Simple verification
For simpler cases where you just need proof of ownership:
```typescript theme={null}
async function verifyWalletOwnership(userCode: string) {
const actionCode = await client.resolve(userCode)
// Simple challenge message
const challenge = `Verify ownership: ${Date.now()}`
await client.attachMessage(userCode, challenge)
for await (const status of client.observeStatus(userCode)) {
if (status.signedMessage) {
return {
verified: true,
wallet: actionCode.pubkey,
signature: status.signedMessage
}
}
}
return { verified: false }
}
```
## Best practices for auth messages
1. **Include a nonce** — Prevents replay attacks
2. **Include a timestamp** — Allow time-based expiry
3. **Include the wallet address** — Confirms which wallet is signing
4. **Use a recognizable format** — Users should understand what they're signing
```typescript theme={null}
// Good: Clear, includes security elements
const message = `
Sign in to MyApp
Wallet: ${walletAddress}
Nonce: ${crypto.randomUUID()}
Time: ${new Date().toISOString()}
This signature proves you own this wallet.
`.trim()
// Bad: Unclear, no security elements
const message = 'Login'
```
## Signature verification
The signed message format depends on the wallet. For Solana, you'll typically verify using ed25519:
```typescript theme={null}
import { verify } from '@noble/ed25519'
import { PublicKey } from '@solana/web3.js'
async function verifySignature(
signedMessage: string,
originalMessage: string,
walletAddress: string
): Promise {
const signature = Buffer.from(signedMessage, 'base64')
const message = Buffer.from(originalMessage)
const pubkey = new PublicKey(walletAddress).toBytes()
return await verify(signature, message, pubkey)
}
```
Different wallets may encode signatures differently. Test with your target wallets (Phantom, Solflare, etc.) to ensure compatibility.
# Sign a Transaction
Source: https://docs.actioncodes.org/recipes/sign-transaction
Request a user to sign a Solana transaction
A common use case: your app builds a transaction and needs the user to sign it.
## The flow
1. User generates code at [actioncode.app](https://actioncode.app)
2. User shares code with your app
3. Your app builds a transaction and attaches it
4. User approves in their wallet
5. Your app gets the signed transaction
## Full example
```typescript theme={null}
import { ActionCodesClient } from '@actioncodes/sdk'
import {
Transaction,
SystemProgram,
PublicKey,
Connection,
LAMPORTS_PER_SOL
} from '@solana/web3.js'
const client = new ActionCodesClient({
authToken: process.env.ACTION_CODES_TOKEN
})
const connection = new Connection('https://api.mainnet-beta.solana.com')
async function requestTransactionSignature(
userCode: string,
recipient: string,
amountSol: number
) {
// 1. Verify the code and get wallet address
const actionCode = await client.resolve(userCode)
const userWallet = new PublicKey(actionCode.pubkey)
console.log(`Code valid for wallet: ${userWallet.toBase58()}`)
// 2. Build the transaction
const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash()
const transaction = new Transaction({
recentBlockhash: blockhash,
feePayer: userWallet,
lastValidBlockHeight
}).add(
SystemProgram.transfer({
fromPubkey: userWallet,
toPubkey: new PublicKey(recipient),
lamports: amountSol * LAMPORTS_PER_SOL
})
)
// 3. Serialize and attach to the code
const serialized = transaction
.serialize({ requireAllSignatures: false })
.toString('base64')
await client.attachTransaction(userCode, serialized, {
description: `Transfer ${amountSol} SOL to ${recipient.slice(0, 8)}...`
})
console.log('Transaction attached — waiting for user approval...')
// 4. Wait for user to approve
for await (const status of client.observeStatus(userCode)) {
if (status.finalizedSignature) {
console.log('Transaction signed!')
return status.finalizedSignature
}
if (status.status === 'expired') {
throw new Error('Code expired before user approved')
}
}
}
// Usage
const signature = await requestTransactionSignature(
'48291037', // User's code from actioncode.app
'RecipientAddress', // Where to send
0.1 // Amount in SOL
)
console.log('Transaction signature:', signature)
```
## Submitting the transaction
After getting the signature, you can submit the transaction to the network:
```typescript theme={null}
// The signature confirms the user signed — now submit to network
const txid = await connection.sendRawTransaction(signedTransaction)
// Wait for confirmation
await connection.confirmTransaction({
signature: txid,
blockhash,
lastValidBlockHeight
})
console.log('Transaction confirmed:', txid)
```
The `finalizedSignature` returned by `observeStatus` is the signature from the user's wallet. Depending on your setup, you may need to also add your own signature (if you're a co-signer) before submitting.
## With metadata
Add helpful context for the user:
```typescript theme={null}
await client.attachTransaction(code, serialized, {
description: 'Purchase NFT: Cool Cat #1234',
label: 'NFT Purchase',
memo: 'Order ID: abc123'
})
```
The user will see this description in actioncode.app when reviewing the request.
## Error handling
```typescript theme={null}
import {
CodeNotFoundError,
ExpiredCodeError
} from '@actioncodes/sdk'
try {
const actionCode = await client.resolve(userCode)
// ... rest of flow
} catch (error) {
if (error instanceof CodeNotFoundError) {
return { error: 'Invalid code. Please get a new one from actioncode.app' }
}
if (error instanceof ExpiredCodeError) {
return { error: 'Code expired. Please get a new one from actioncode.app' }
}
throw error
}
```
# Telegram Bot
Source: https://docs.actioncodes.org/recipes/telegram-bot
Build a Telegram bot that accepts Action Codes
Telegram bots are a great use case for Action Codes — users can interact with blockchain actions directly in chat.
## The flow
1. User starts chat with your bot
2. Bot asks for an action code
3. User gets code from [actioncode.app](https://actioncode.app) and sends it
4. Bot attaches an action (transaction or message)
5. User approves in actioncode.app
6. Bot confirms completion
## Example bot
Using [grammY](https://grammy.dev/) (works similarly with other libraries):
```typescript theme={null}
import { Bot, Context } from 'grammy'
import { ActionCodesClient } from '@actioncodes/sdk'
const bot = new Bot(process.env.BOT_TOKEN!)
const client = new ActionCodesClient({
authToken: process.env.ACTION_CODES_TOKEN
})
// /start command
bot.command('start', (ctx) => {
ctx.reply(
'Welcome! I can help you sign messages with your Solana wallet.\n\n' +
'Commands:\n' +
'/sign - Sign a message\n' +
'/verify - Verify wallet ownership'
)
})
// /sign command
bot.command('sign', async (ctx) => {
await ctx.reply(
'To sign a message:\n\n' +
'1. Open actioncode.app in your Solana wallet\n' +
'2. Connect and get your code\n' +
'3. Send me the 8-digit code'
)
// Store state that we're expecting a code for signing
await setUserState(ctx.from!.id, { action: 'sign' })
})
// Handle code input
bot.on('message:text', async (ctx) => {
const text = ctx.message.text
// Check if it looks like an action code (8 digits)
if (!/^\d{8}$/.test(text)) {
return // Not a code, ignore or handle other messages
}
const userState = await getUserState(ctx.from!.id)
if (!userState?.action) {
await ctx.reply('Send /sign first to start a signing session.')
return
}
await ctx.reply('Verifying code...')
try {
// Resolve the code
const actionCode = await client.resolve(text)
await ctx.reply(
`Code valid!\n` +
`Wallet: ${actionCode.pubkey.slice(0, 8)}...\n\n` +
`Attaching message for you to sign...`
)
// Create and attach message
const message = `Signed via Telegram bot\nUser: @${ctx.from!.username}\nTime: ${new Date().toISOString()}`
await client.attachMessage(text, message, {
description: 'Telegram signature request'
})
await ctx.reply(
'Message attached!\n\n' +
'Now go to actioncode.app and approve the signing request.'
)
// Wait for signature
for await (const status of client.observeStatus(text, { timeout: 120000 })) {
if (status.signedMessage) {
await ctx.reply(
`Message signed!\n\n` +
`Signature: ${status.signedMessage.slice(0, 20)}...`
)
await clearUserState(ctx.from!.id)
return
}
}
// If we get here, it timed out
await ctx.reply('Code expired. Use /sign to try again.')
} catch (error: any) {
await ctx.reply(`Error: ${error.message}\n\nUse /sign to try again.`)
}
await clearUserState(ctx.from!.id)
})
// Start bot
bot.start()
```
## State management helpers
```typescript theme={null}
// Simple in-memory state (use Redis or DB in production)
const userStates = new Map()
async function setUserState(userId: number, state: { action: string }) {
userStates.set(userId, state)
}
async function getUserState(userId: number) {
return userStates.get(userId)
}
async function clearUserState(userId: number) {
userStates.delete(userId)
}
```
## Transaction example
For sending SOL:
```typescript theme={null}
bot.command('send', async (ctx) => {
await ctx.reply(
'To send SOL:\n\n' +
'1. Reply with: \n' +
' Example: 0.1 ABC123...\n\n' +
'2. Then send your action code from actioncode.app'
)
await setUserState(ctx.from!.id, { action: 'send' })
})
// Handle "0.1 ABC123..." format
bot.hears(/^(\d+\.?\d*)\s+(\w{32,44})$/, async (ctx) => {
const amount = parseFloat(ctx.match[1])
const recipient = ctx.match[2]
await setUserState(ctx.from!.id, {
action: 'send',
amount,
recipient
})
await ctx.reply(
`Ready to send ${amount} SOL to ${recipient.slice(0, 8)}...\n\n` +
`Now send your 8-digit code from actioncode.app`
)
})
```
## User experience tips
1. **Guide users clearly** — Not everyone knows what actioncode.app is
2. **Show progress** — Send messages at each step
3. **Handle timeouts gracefully** — Codes expire in \~2 minutes
4. **Confirm success** — Show the result clearly
```typescript theme={null}
// Good UX: Clear instructions
await ctx.reply(
'Getting ready to sign!\n\n' +
'1. Open your Solana wallet (Phantom, Solflare, etc.)\n' +
'2. In the wallet browser, go to: actioncode.app\n' +
'3. Tap "Get Code" and copy the 8 digits\n' +
'4. Paste the code here\n\n' +
'The code expires in 2 minutes, so do this quickly!'
)
```
## Live example
Try the official example bot: [@action\_codes\_bot](https://t.me/action_codes_bot)
Source code: [github.com/otaprotocol/telegram-bot-example](https://github.com/otaprotocol/telegram-bot-example)
# Action Codes Relayer
Source: https://docs.actioncodes.org/relayer/introduction
Relayer is completely free to use and officially maintained by Action Codes.
#### What is the Relayer?
The Relayer is a trusted middleware service that acts as the bridge between users, applications, and the blockchain using the Action Codes Protocol.
It enables:
* Secure registration of action codes
* Storage of encrypted code state (e.g., pending/resolved/finalized)
* Status resolution via code hashes (never raw code)
* Code usage verification, metadata injection, and expiration enforcement
* Seamless UX with APIs for attaching and finalizing transactions
This allows applications to:
* Never store private code values
* Use short, secure, and shareable codes
* Outsource protocol validation and persistence to a lightweight server
#### Why It Matters
* Security: Encrypted code state stored by hash only
* Flexibility: Works with any wallet or frontend
* Trust Layer: Prevents invalid or spoofed transactions
```mermaid theme={null}
sequenceDiagram
participant Wallet
participant CodeGenerator
participant Relayer
participant Blockchain
Wallet->>CodeGenerator: Connect wallet
CodeGenerator->>Wallet: Sign protocol message
CodeGenerator->>Relayer: POST /register { code, pubkey, sig, metadata }
Relayer->>Relayer: Validate + Encrypt + Store
Relayer-->>CodeGenerator: codeHash + expiry
CodeGenerator->>Relayer: POST /attach { code, tx/message }
Relayer->>Relayer: Validate + Inject metadata
Relayer-->>CodeGenerator: Status = resolved
CodeGenerator->>Wallet: Prompt to sign TX/message
Wallet->>CodeGenerator: TX signature or signed message
CodeGenerator->>Relayer: POST /finalize { signature }
Relayer-->>CodeGenerator: Status = finalized
CodeGenerator->>Relayer: GET /status/:code
Relayer-->>CodeGenerator: { status, signature, metadata }
```
# SDK Methods
Source: https://docs.actioncodes.org/sdk/methods
Complete reference for @actioncodes/sdk
## Installation
```bash npm theme={null}
npm install @actioncodes/sdk
```
```bash pnpm theme={null}
pnpm add @actioncodes/sdk
```
```bash yarn theme={null}
yarn add @actioncodes/sdk
```
## Initialize
```typescript theme={null}
import { ActionCodesClient } from '@actioncodes/sdk'
const client = new ActionCodesClient({
authToken: process.env.ACTION_CODES_TOKEN
})
```
**Auth token required.** Request one by DMing [@beharefe](https://t.me/beharefe) on Telegram or emailing [gm@actioncodes.org](mailto:gm@actioncodes.org).
***
## Methods
### resolve
Verify a code and get its details.
```typescript theme={null}
const actionCode = await client.resolve(code)
```
The 8-digit action code from the user
The 8-digit code
The wallet public key this code belongs to
Unix timestamp when the code expires
Current status: `pending`, `attached`, `finalized`, or `expired`
**Example:**
```typescript theme={null}
const actionCode = await client.resolve('48291037')
console.log('Wallet:', actionCode.pubkey)
console.log('Expires:', new Date(actionCode.expiresAt))
console.log('Status:', actionCode.status)
```
***
### getStatus
Get the current status of a code.
```typescript theme={null}
const status = await client.getStatus(code)
```
The action code to check
`pending` | `attached` | `finalized` | `expired`
Unix timestamp
Whether a transaction is attached
Whether a message is attached
Transaction signature (if finalized with transaction)
Signed message bytes (if finalized with message)
**Example:**
```typescript theme={null}
const status = await client.getStatus('48291037')
if (status.finalizedSignature) {
console.log('Transaction signed:', status.finalizedSignature)
}
```
***
### observeStatus
Watch for status changes over time. Returns an async iterator.
```typescript theme={null}
for await (const status of client.observeStatus(code, options)) {
// Handle status updates
}
```
The action code to observe
Polling interval in milliseconds
Maximum time to observe in milliseconds
**Example:**
```typescript theme={null}
for await (const status of client.observeStatus('48291037', { interval: 1000 })) {
console.log('Status:', status.status)
if (status.finalizedSignature) {
console.log('Done! Signature:', status.finalizedSignature)
break
}
if (status.signedMessage) {
console.log('Done! Signed message:', status.signedMessage)
break
}
if (status.status === 'expired') {
console.log('Code expired')
break
}
}
```
`observeStatus` automatically stops when the code is finalized or expires. You can also `break` early.
***
### attachTransaction
Attach a transaction for the user to sign.
```typescript theme={null}
await client.attachTransaction(code, transaction, meta?)
```
The action code
Base64-encoded serialized transaction
Optional metadata (description, label, etc.)
**Example:**
```typescript theme={null}
import { Transaction, SystemProgram, PublicKey, Connection } from '@solana/web3.js'
// Build the transaction
const connection = new Connection('https://api.mainnet-beta.solana.com')
const { blockhash } = await connection.getLatestBlockhash()
const tx = new Transaction({
recentBlockhash: blockhash,
feePayer: new PublicKey(actionCode.pubkey)
}).add(
SystemProgram.transfer({
fromPubkey: new PublicKey(actionCode.pubkey),
toPubkey: new PublicKey(recipient),
lamports: amount
})
)
// Serialize (without signatures) and attach
const serialized = tx.serialize({ requireAllSignatures: false }).toString('base64')
await client.attachTransaction(code, serialized, {
description: 'Transfer 0.1 SOL'
})
```
***
### attachMessage
Attach a message for the user to sign.
```typescript theme={null}
await client.attachMessage(code, message, meta?)
```
The action code
The message to sign
Optional metadata
**Example:**
```typescript theme={null}
await client.attachMessage(code, 'Sign in to MyApp at ' + Date.now(), {
description: 'Sign-in verification'
})
```
***
### finalizeTransaction
Manually finalize a code with a transaction signature. Usually not needed — the user's wallet does this automatically.
```typescript theme={null}
await client.finalizeTransaction(code, signature)
```
The action code
The transaction signature
***
### finalizeMessage
Manually finalize a code with a signed message. Usually not needed — the user's wallet does this automatically.
```typescript theme={null}
await client.finalizeMessage(code, signedMessage)
```
The action code
The signed message
***
### register
Create a new action code. **For advanced use** — most apps receive codes from users instead.
```typescript theme={null}
const actionCode = await client.register(pubkey, sign, metadata?)
```
The wallet public key
A function that signs a message: `(message: string) => Promise`
Optional metadata
**Example:**
```typescript theme={null}
import { PublicKey } from '@solana/web3.js'
const actionCode = await client.register(
new PublicKey(walletAddress),
async (message) => {
// Sign with your wallet
return wallet.signMessage(message)
},
{ description: 'My action code' }
)
console.log('Generated code:', actionCode.code)
```
Most applications should **receive** codes from users rather than generate them. Users generate codes at [actioncode.app](https://actioncode.app).
***
## Error Handling
The SDK throws specific error types:
```typescript theme={null}
import {
CodeNotFoundError,
ExpiredCodeError,
InvalidCodeFormatError,
UnauthorizedError
} from '@actioncodes/sdk'
try {
await client.resolve(code)
} catch (error) {
if (error instanceof CodeNotFoundError) {
console.log('Code does not exist')
} else if (error instanceof ExpiredCodeError) {
console.log('Code has expired')
} else if (error instanceof InvalidCodeFormatError) {
console.log('Invalid code format (must be 8 digits)')
}
}
```
| Error | Cause |
| ------------------------ | --------------------------------------- |
| `CodeNotFoundError` | The code doesn't exist |
| `ExpiredCodeError` | The code has expired (\~2 min lifetime) |
| `InvalidCodeFormatError` | Code is not 8 digits |
| `UnauthorizedError` | Permission denied |
***
## Types
### ActionCodeMeta
```typescript theme={null}
interface ActionCodeMeta {
description?: string // Human-readable description
label?: string // Short label
memo?: string // Additional memo
}
```
### ActionCodeStatusResponse
```typescript theme={null}
interface ActionCodeStatusResponse {
status: 'pending' | 'attached' | 'finalized' | 'expired'
expiresAt: number
hasTransaction: boolean
hasMessage: boolean
finalizedSignature?: string
signedMessage?: string
}
```
### ObserveStatusOptions
```typescript theme={null}
interface ObserveStatusOptions {
interval?: number // Polling interval in ms (default: 2000)
timeout?: number // Max observation time in ms (default: 120000)
}
```
# Security & Determinism
Source: https://docs.actioncodes.org/security-determinism
How Action Codes achieve security without on-chain state
This is advanced documentation about the protocol's security model. For integration guides, see the [Quick Start](/quickstart).
The Action Codes Protocol is designed to offer cryptographic security and strict determinism without requiring persistent on-chain state or user sessions. Every code can be verified, traced, and resolved based entirely on public inputs and signatures.
#### Deterministic Code Generation
**Each Action Code is deterministically derived using the combination of:**
* Public key (pubkey)
* Time window (timestamp, rounded to 2-minute slots)
* Namespace prefix (prefix)
* Signature over a canonical message
This ensures that:
* The same inputs will always produce the same code
* Multiple clients (relayers, wallets) can reproduce and validate the code without external coordination
* No central authority or database is required to issue or verify codes
#### Signature Binding
All codes are signed by the user’s wallet, ensuring:
* The user explicitly authorizes the creation of the code
* The code cannot be spoofed by any third party
* Verification only requires the signature and public key
Each signature is over a canonical string:
```text theme={null}
{prefix}:{code}:{timestamp}
```
This binding ensures replay protection and uniqueness within the time window.
#### Encrypted Payloads
Action Code objects are encrypted using the code itself as the symmetric key. This means:
* The code is the secret needed to decrypt and access transaction details
* Even if a relayer or database is compromised, attackers cannot view transaction intent or metadata without knowing the code
* This pattern supports stateless privacy and decentralization
Only the user who created the code (or anyone they share it with) can decrypt and act on it.
#### No On-Chain State or Sessions
* No wallet connection is needed to generate or resolve a code
* No smart contract is involved in the validation of the code itself
* All logic is executed locally or server-side via open protocol rules
* Transactions are finalized only after on-chain user signatures
#### Built-in Expiry & Grace Period
Codes include expiration timestamps and are valid only within a short window (e.g. 2–3 minutes):
* Prevents reuse or replay
* Encourages real-time interaction
* Relayers automatically reject expired codes
The protocol also allows ±1 slot tolerance for clock drift.