Skip to main content

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.

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.
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:
  1. The intent — a JSON object describing the operation. Fields use snake_case, amounts are strings.
  2. The canonical digest — the intent canonicalized per RFC 8785 JCS, then hashed with SHA-256.
  3. The signature(s) — ECDSA P-256 signatures of that digest in ASN.1 DER encoding, each base64-encoded.

The Intent

{
  "wallet_id": "2LfZm5KMnRvLFtRP7nJJug4zJEP",
  "caip2": "eip155:1",
  "operation": {
    "kind": "transfer",
    "from": "0xYourWalletAddress...",
    "to": "0xDestinationAddress...",
    "amount": "10.5",
    "asset_id": "USDC"
  },
  "idempotency_key": "a6f8c8c0-6f0a-4a24-a3a3-9e8a0cf2f7c0"
}
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.
import { createSign } from 'node:crypto';
import canonicalize from 'canonicalize'; // npm install canonicalize

function signIntent(intent, privateKey) {
  const canonical = canonicalize(intent);
  const signer = createSign('SHA256');
  signer.update(canonical);
  // dsaEncoding 'der' matches Go's ecdsa.SignASN1 / the server's verify path.
  const signature = signer.sign({ key: privateKey, dsaEncoding: 'der' });
  return signature.toString('base64');
}
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.

Submit the Signed Transaction

curl -X POST https://api.platform.dakota.xyz/wallets/2LfZm5KMnRvLFtRP7nJJug4zJEP/transactions \
  -H "X-API-Key: $DAKOTA_API_KEY" \
  -H "X-Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "signatures": [
      "MEQCIEtPHo4edFaeOAWql3CHzcEJTX0MlUxjnqdlQwv+FYbrAiAhRAXEiruewidHx1JTofP1QQ+mJnRx6cXQ6vjCHp9wlQ=="
    ],
    "intent": {
      "wallet_id": "2LfZm5KMnRvLFtRP7nJJug4zJEP",
      "caip2": "eip155:1",
      "operation": {
        "kind": "transfer",
        "from": "0xYourWalletAddress...",
        "to": "0xDestinationAddress...",
        "amount": "10.5",
        "asset_id": "USDC"
      },
      "idempotency_key": "a6f8c8c0-6f0a-4a24-a3a3-9e8a0cf2f7c0"
    }
  }'
The 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:
  1. Build an intent JSON object with a type discriminator field
  2. Canonicalize it per RFC 8785 JCS and sign with ECDSA P-256 (use the same signIntent function above — it works for every intent type)
  3. POST to the mutation endpoint with { "signatures": [...], "intent": {...} }
The signing process is identical to transactions — only the intent schema and endpoint differ. Each intent carries a type field that tells the server which schema to expect.
OperationEndpointtype value
Add a rule to a policyPOST /policies/{policy_id}/rulesadd_policy_rule
Update a rule’s definitionPATCH /policies/{policy_id}/rules/{rule_id}update_policy_rule
Remove a rule from a policyDELETE /policies/{policy_id}/rules/{rule_id}remove_policy_rule
Delete a policyDELETE /policies/{policy_id}delete_policy
Attach a policy to a walletPUT /policies/{policy_id}/wallets/{wallet_id}attach_policy_to_wallet
Detach a policy from a walletDELETE /policies/{policy_id}/wallets/{wallet_id}detach_policy_from_wallet
Attach a signer group to a walletPUT /wallets/{wallet_id}/signer-groups/{signer_group_id}attach_group_to_wallet
Detach a signer group from a walletDELETE /wallets/{wallet_id}/signer-groups/{signer_group_id}detach_group_from_wallet
Add signers to a groupPOST /signer-groups/{signer_group_id}/signersadd_signers_to_group
Remove a signer from a groupDELETE /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.
The number of signatures required matches the approval threshold of the relevant policy.

Example: Add a Rule to an Existing Policy

The AddPolicyRuleIntent shape:
{
  "type": "add_policy_rule",
  "policy_id": "2LfQm5KMnRvLFtRP7nJJug4zJEP",
  "rule_type": "approval_threshold",
  "action": "allow",
  "definition": { "threshold": 2 },
  "idempotency_key": "a6f8c8c0-6f0a-4a24-a3a3-9e8a0cf2f7c0"
}
Field shape reminders:
  • type is required on every intent and must match the operation (e.g. "add_policy_rule") — it’s part of what you sign
  • action is "allow" or "deny"
  • rule_type is "approval_threshold", "amount_threshold", or "address_list"
  • definition is a flat object for the chosen rule_type — e.g. {"threshold": 2} for approval_threshold, {"addresses": [...]} for address_list
Sign the intent with the same signIntent function from above, then submit:
curl -X POST https://api.platform.dakota.xyz/policies/2LfQm5KMnRvLFtRP7nJJug4zJEP/rules \
  -H "X-API-Key: $DAKOTA_API_KEY" \
  -H "X-Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "signatures": [
      "MEYCIQCr24vqv9xdz92Kj8xMsTxd8cOalqiRCuXzjYdDSA/VtgIhAPzJqR/tvG8eUgX/b4sTL6/+bCpaliRa/r5Y1toKJkSl"
    ],
    "intent": {
      "type": "add_policy_rule",
      "policy_id": "2LfQm5KMnRvLFtRP7nJJug4zJEP",
      "rule_type": "approval_threshold",
      "action": "allow",
      "definition": { "threshold": 2 },
      "idempotency_key": "a6f8c8c0-6f0a-4a24-a3a3-9e8a0cf2f7c0"
    }
  }'
The intent schemas for the other mutations are listed in the OpenAPI reference. They follow the same conventions: snake_case fields, a type discriminator matching the operation, and string enum values.