This section is for advanced users. You do NOT need to read this to use Action Codes. The SDK 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 instead.
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.
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.
How it works:
- User signs a delegation proof specifying: delegated keypair, chain, expiration
- Delegated keypair generates codes bound to the proof
- 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:
{
"pubkey": "7gNqUuY5...",
"code": "48291037",
"timestamp": 1704067200
}
Deterministic serialization (sorted keys, no whitespace) prevents ambiguity.
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
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:
- Validates codes — Verifies signature, timestamp, format
- Stores state — Encrypted transaction/message payloads
- Coordinates flow — Connects apps and wallets
- 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.
Using the Protocol Package
For low-level protocol access:
npm install @actioncodes/protocol
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:
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 for implementation details.
Best Practices
- Set appropriate TTL — Balance security vs. user experience
- Validate server-side — Never trust client-only validation
- Use delegation carefully — Set short expiration windows
- Monitor relayer activity — Watch for unusual patterns
- Handle expiry gracefully — Prompt users to regenerate codes