Every wallet transaction in Dakota is an endorsed request: a canonical JSON intent plus one or more ECDSA P-256 signatures. This page is the reference for doing that correctly. If you only want the conceptual model and the signer/policy lifecycles, read Signing & Endorsed Requests. If you’ve just stood up a wallet in Common Flows, this is where you continue.Documentation Index
Fetch the complete documentation index at: https://docs.dakota.xyz/llms.txt
Use this file to discover all available pages before exploring further.
Prerequisites. You already have a wallet, a signer group, and at least one ES256 private key. If not, start with Manage Non-Custodial Wallets.
The Three Parts of a Signed Transaction
Dakota wallets use an intent-based model: every transaction begins with a canonical JSON description of what the wallet should do, signed with the private key of at least one signer in the wallet’s signer group. Platform forwards the signed intent to the policy engine, which verifies each signature against the stored public keys and — if the approval threshold is satisfied — executes the transaction on-chain. A signed wallet transaction has three parts:- The intent — a JSON object describing the operation. Fields use
snake_case, amounts are strings. - The canonical digest — the intent canonicalized per RFC 8785 JCS, then hashed with SHA-256.
- The signature(s) — ECDSA P-256 signatures of that digest in ASN.1 DER encoding, each base64-encoded.
The Intent
Field names are
snake_case (wallet_id, asset_id, idempotency_key). Amounts are strings ("10.5", not 10.5). The operation.kind field determines which sub-fields are required: transfer uses from / to / amount / asset_id; evm_contract_call additionally accepts method / args / data. Omit any field that doesn’t apply — canonicalization drops unset fields and sorts keys alphabetically before hashing.Canonicalize, Hash, and Sign
Canonicalization is what ensures that a client and the policy engine hash the same bytes regardless of JSON key ordering or whitespace. All three reference libraries below implement RFC 8785 JCS, so their output is byte-identical for the same input.Submit the Signed Transaction
signatures array holds as many entries as the signer group’s approval threshold requires; each entry is a DER-then-base64 ECDSA signature from a distinct signer in the group. The intent object must be byte-equivalent to what was canonicalized and signed — platform re-canonicalizes the intent on the server side before signature verification, so ordering and whitespace in the wire JSON do not matter, but field values must match exactly.
Modifying Policies, Wallets, and Signer Groups
Adding rules to a policy, attaching a signer group to a wallet, and any other mutation of a wallet’s authorization graph after creation go through the same endorsed-request pattern:- Build an intent JSON object with a
typediscriminator field - Canonicalize it per RFC 8785 JCS and sign with ECDSA P-256 (use the same
signIntentfunction above — it works for every intent type) - POST to the mutation endpoint with
{ "signatures": [...], "intent": {...} }
type field that tells the server which schema to expect.
| Operation | Endpoint | type value |
|---|---|---|
| Add a rule to a policy | POST /policies/{policy_id}/rules | add_policy_rule |
| Update a rule’s definition | PATCH /policies/{policy_id}/rules/{rule_id} | update_policy_rule |
| Remove a rule from a policy | DELETE /policies/{policy_id}/rules/{rule_id} | remove_policy_rule |
| Delete a policy | DELETE /policies/{policy_id} | delete_policy |
| Attach a policy to a wallet | PUT /policies/{policy_id}/wallets/{wallet_id} | attach_policy_to_wallet |
| Detach a policy from a wallet | DELETE /policies/{policy_id}/wallets/{wallet_id} | detach_policy_from_wallet |
| Attach a signer group to a wallet | PUT /wallets/{wallet_id}/signer-groups/{signer_group_id} | attach_group_to_wallet |
| Detach a signer group from a wallet | DELETE /wallets/{wallet_id}/signer-groups/{signer_group_id} | detach_group_from_wallet |
| Add signers to a group | POST /signer-groups/{signer_group_id}/signers | add_signers_to_group |
| Remove a signer from a group | DELETE /signer-groups/{signer_group_id}/signers/{signer_id} | remove_signers_from_group |
The signers that must endorse each intent are determined by the resource being mutated:
- Policy mutations (
add_policy_rule,remove_policy_rule,update_policy_rule,delete_policy) must be signed by members of the policy’s own signer group. - Wallet mutations (
attach_policy_to_wallet,detach_policy_from_wallet,attach_group_to_wallet,detach_group_from_wallet) must be signed by members of a signer group already attached to the wallet. - Signer-group mutations (
add_signers_to_group,remove_signers_from_group) must be signed by existing members of the group.
Example: Add a Rule to an Existing Policy
TheAddPolicyRuleIntent shape:
Field shape reminders:
typeis required on every intent and must match the operation (e.g."add_policy_rule") — it’s part of what you signactionis"allow"or"deny"rule_typeis"approval_threshold","amount_threshold", or"address_list"definitionis a flat object for the chosenrule_type— e.g.{"threshold": 2}forapproval_threshold,{"addresses": [...]}foraddress_list
signIntent function from above, then submit:
type discriminator matching the operation, and string enum values.