# 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.