Skip to main content
Every action that changes wallet state in Dakota — sending funds, attaching a signer group, modifying a policy — requires an endorsed request: a cryptographically signed declaration of intent. This guide explains the signing model end-to-end, documents every intent type, and walks through the full signer group and policy lifecycles.
If you haven’t set up a wallet yet, start with Non-Custodial Wallets first, then return here for the complete signing reference.

The Endorsed Request Model

An endorsed request wraps two things:
  1. An intent — a JSON object describing exactly what you want to do
  2. One or more signatures — ECDSA P-256 signatures (or WebAuthn assertions) proving that authorized signers approved the intent
Dakota’s policy engine re-canonicalizes the intent server-side, verifies each signature against the stored public keys, checks that the signer group’s approval threshold is met, and only then executes the action. This means:
  • You never send private keys — only signatures
  • The server cannot fabricate intents — every action has cryptographic proof of authorization
  • Multiple signers can co-approve — the signatures array accepts as many entries as your threshold requires

How Signing Works

Every signature follows the same three-step process, regardless of intent type.

Step 1: Build the Intent JSON

Create the intent object with the exact fields required for the operation. Field names are snake_case. Amounts are strings (e.g., "10.5", not 10.5). Omit any optional fields that don’t apply.

Step 2: Canonicalize with RFC 8785 (JCS)

RFC 8785 JSON Canonicalization Scheme produces a deterministic byte sequence from any JSON object by sorting keys alphabetically and removing insignificant whitespace. This ensures that your client and Dakota’s policy engine hash identical bytes, regardless of how your language orders JSON keys.

Step 3: Hash with SHA-256, Sign with ECDSA P-256

Hash the canonical bytes with SHA-256, then sign the hash using your ECDSA P-256 private key. The signature must be in ASN.1 DER encoding (not raw r || s), then base64-encoded.
This is the flow for ES256 signers. If your signer is a WebAuthn credential (key_type: WEBAUTHN) — a passkey or hardware authenticator — the signature is a WebAuthn assertion bundle rather than a DER signature over the hash, and the intent is carried as the assertion’s challenge. See WebAuthn & Passkey Signing.

Code Examples

Browser only: crypto.subtle.sign with ECDSA returns an IEEE P1363 raw r || s encoding, not ASN.1 DER. You must convert it before submitting — the rawEcdsaSignatureToDer helper above does this. Submitting the raw form will fail signature verification.

Intent Types Reference

Every endorsed request carries one of nine intent types. The table below lists all of them with their discriminator value, endpoint, and required fields.

Send Transaction

Send crypto from a wallet. This is the only intent type without a type discriminator field.
The operation.kind field determines which sub-fields are required:
Amounts are strings ("10.5", not 10.5). The caip2 field uses CAIP-2 chain identifiers (e.g., eip155:1 for Ethereum mainnet, eip155:11155111 for Sepolia).

Attach Signer Group to Wallet

Detach Signer Group from Wallet

Attach Policy to Wallet

Detach Policy from Wallet

Add Policy Rule

Available rule types:

Remove Policy Rule

Update Policy Rule

updated_definition is a JSON string, not a nested object. Serialize the definition object to a string before including it in the intent.The update path is stricter than create — threshold must be > 0, and amount_threshold updates require the full proto Asset shape with contract_address (not just {id, name}). The accepted shapes:
  • approval_threshold: {"threshold": int32 (> 0), "description"?: string}
  • amount_threshold: {"min_amount": int64 (>= 0), "threshold": int32 (> 0), "asset": {"id", "name", "network_id", "contract_address", "token_standard", "decimals"}}
  • address_list: {"addresses": [string] (non-empty)}

Delete Policy

Deleting a policy is irreversible. Detach the policy from all wallets first — attempting to delete a policy that is still attached to a wallet will fail.

Signer Group Lifecycle

A signer group controls who can authorize wallet actions. Here is the complete lifecycle from creation through teardown.

Step-by-step

Before detaching a signer group, ensure the wallet has at least one other signer group attached, or the wallet will become unusable — no one will be able to sign transactions for it.
Adding and removing signers (steps 3 and 6) do not require endorsed requests — they are standard API calls authenticated with your API key. Only wallet-level operations (attach, detach, transact) require cryptographic signatures.

Policy Lifecycle

Policies define automated governance rules for wallets — approval thresholds, amount limits, address allowlists, and more. Here is the complete lifecycle.

Step-by-step

Deletion order matters. You must detach a policy from all wallets before you can delete it. Attempting to delete an attached policy will return an error.

Troubleshooting

Signature Verification Failed

Threshold Not Met

Signer Not Authorized

Key Format Errors