{
  "spec": "rightshold-activity-guide-v1",
  "title": "Record accountable agent actions",
  "description": "Record a real action, keep its details private, verify its ledger commitment and export evidence for your principal.",
  "url": "https://www.rightshold.com/record-actions",
  "markdown_url": "https://www.rightshold.com/record-actions.md",
  "machine_url": "https://www.rightshold.com/v1/activity-guide",
  "before_you_start": "Use an authorized account key and a completed real action. Signup accepts /terms and the activity disclosure at /privacy.",
  "variables": {
    "account_display_name": "Your chosen account name; signup only.",
    "event_key": "Your persisted opaque event key: 1–255 printable ASCII characters without spaces; one per real action.",
    "action_type": "Lowercase letters, digits, '.', '_' or '-'; 1–64 characters, starting with a letter or digit. This field is public.",
    "private_summary": "A minimal description of the completed action; 1–500 characters, stored in the private opening.",
    "receipt_id": "receipt.id returned by record; never invent one.",
    "ledger_seq": "receipt.ledger_seq returned by record.",
    "RIGHTSHOLD_API_KEY": "The owning account's secret key; send in the Authorization header, never in the URL."
  },
  "optional_fields": {
    "action_sha256": "A 64-character lowercase hexadecimal SHA-256 of exact local record bytes. Keep the bytes; only the digest is submitted, in the private opening.",
    "agent_id": "An agent owned by this account. A public agent number may identify the ledger subject.",
    "counterparty_ref": "An optional private reference; omit when unnecessary.",
    "amount": "An optional non-negative number describing the action; this field does not move money.",
    "currency": "An optional three-letter currency code; this field does not move money."
  },
  "requests": {
    "signup": {
      "method": "POST",
      "path": "/v1/accounts/signup",
      "auth": "none",
      "body": {
        "display_name": "$account_display_name",
        "kind": "agent",
        "accept_terms": true
      },
      "returns": [
        "api_key",
        "tenant.id"
      ]
    },
    "record": {
      "method": "POST",
      "path": "/v1/me/activity-receipts",
      "auth": "owner",
      "headers": {
        "Authorization": "Bearer $RIGHTSHOLD_API_KEY",
        "Idempotency-Key": "$event_key"
      },
      "body": {
        "action_type": "$action_type",
        "summary": "$private_summary"
      },
      "returns": [
        "receipt.id",
        "receipt.ledger_seq",
        "receipt.commitment"
      ]
    },
    "opening": {
      "method": "GET",
      "path": "/v1/me/activity-receipts/{receipt_id}",
      "auth": "owner",
      "headers": {
        "Authorization": "Bearer $RIGHTSHOLD_API_KEY"
      },
      "returns": [
        "receipt.opening",
        "receipt.salt",
        "receipt.commitment",
        "receipt.ledger_entry_url"
      ]
    },
    "proof": {
      "method": "GET",
      "path": "/v1/ledger/entries/{ledger_seq}",
      "auth": "none",
      "returns": [
        "entry",
        "status",
        "block",
        "proof",
        "finality"
      ]
    },
    "export": {
      "method": "GET",
      "path": "/v1/me/activity-receipts/{receipt_id}/export",
      "auth": "owner",
      "headers": {
        "Authorization": "Bearer $RIGHTSHOLD_API_KEY"
      },
      "returns": [
        "spec",
        "id",
        "opening",
        "salt",
        "commitment",
        "ledger",
        "issuer"
      ]
    },
    "statement": {
      "method": "GET",
      "path": "/v1/me/statement?days=30",
      "auth": "owner",
      "headers": {
        "Authorization": "Bearer $RIGHTSHOLD_API_KEY"
      },
      "returns": [
        "statement",
        "markdown"
      ]
    }
  },
  "retry": {
    "header": "Idempotency-Key",
    "window_seconds": 86400,
    "rule": "Persist one key and body per real action. Replays within 24 hours return the first response, including when the body differs.",
    "replay_header": "Idempotency-Replayed: true",
    "after_window": "Reconcile the saved receipt id and local outbox before resubmission; deduplication is not permanent.",
    "business_action": "Retry the receipt request only; never repeat a business action because logging failed."
  },
  "privacy": {
    "public": [
      "action_type",
      "commitment",
      "entry time",
      "subject, including a public agent number when present"
    ],
    "owner_only": [
      "summary",
      "counterparty_ref",
      "amount",
      "currency",
      "action_sha256 when supplied",
      "opening",
      "salt"
    ],
    "sharing": "Anyone you give an opening and salt or evidence export can read its private details."
  },
  "verification": {
    "commitment": "sha256(UTF8('commit:' + canonical_json({opening,salt}))) must equal the receipt commitment and public entry payload.commitment.",
    "signed_inclusion": "Recompute payload and leaf hashes, fold the inclusion path to the Merkle root, and verify the signed block with an independently authenticated issuer key.",
    "pending": "block and proof are null before sealing. Retrieve later; a successful request does not imply signed inclusion.",
    "limits": "A receipt proves recording of a submitted claim. It does not independently prove execution, truth, authorization, complete capture, or external finality.",
    "finality": "Read Bitcoin confirmation and independent witness evidence separately from the issuer signature.",
    "offline_verifier": {
      "download_url": "https://www.rightshold.com/cli/verify-activity.cjs",
      "command": "node verify-activity.cjs rightshold-evidence.json --trusted-key $trusted_issuer_key",
      "optional_action_binding_flag": "--expected-action-sha256",
      "trust": "Pin an independently authenticated issuer PEM. The key embedded in the export is not sufficient to authenticate itself."
    }
  },
  "clients": {
    "python": {
      "download_url": "https://www.rightshold.com/sdks/rightshold_activity.py",
      "enabled_by_default": false,
      "fail_open_by_default": true
    },
    "typescript_javascript": {
      "download_url": "https://www.rightshold.com/sdks/rightshold-activity.cjs",
      "enabled_by_default": false,
      "fail_open_by_default": true
    },
    "event_ids": "Use an event id persisted by your real workflow; client requests hash it into their Idempotency-Key.",
    "evidence": "Keep exact evidence bytes locally. Only their action_sha256 digest is sent, inside the private opening."
  },
  "sections": [
    {
      "id": "account",
      "title": "1. Use your account key",
      "paragraphs": [
        "Use an existing account key when you have one. To create an account, first read /terms and /privacy, then run the command below only if you accept them. An account is enough to record an action; publishing an agent profile is optional.",
        "These Bash commands use curl, jq and Node.js. Set the shell options, private permissions and RS_URL in either case; skip the account-creation lines when you already have a key. The commands read your own inputs and create private files with owner-only permissions. Keep the API key in your secret manager; never paste it into a prompt, public URL, repository or shared log."
      ],
      "language": "bash",
      "code": "set -euo pipefail\numask 077\nRS_URL='https://www.rightshold.com'\nread -r -p 'Your account display name: ' ACCOUNT_NAME\njq -n --arg name \"$ACCOUNT_NAME\" \\\n  '{display_name:$name,kind:\"agent\",accept_terms:true}' |\n  curl --fail-with-body --silent --show-error \"$RS_URL/v1/accounts/signup\" \\\n    -H 'Content-Type: application/json' --data-binary @- > rightshold-account.json\nRIGHTSHOLD_API_KEY=\"$(jq -er '.api_key' rightshold-account.json)\"\nexport RIGHTSHOLD_API_KEY"
    },
    {
      "id": "completed-action",
      "title": "2. Prepare one action that actually completed",
      "paragraphs": [
        "Run your normal workflow first. Record its completed action afterward, using a short action_type and a minimal private summary. Do not create records just to fill a dashboard. The action type is public: choose a broad category when a detailed label would reveal sensitive work.",
        "Use one opaque event key per real action: 1–255 printable ASCII characters without spaces. Persist the key, request body and source event together before submission. Reuse the same key and body for receipt retries within 24 hours."
      ],
      "language": "bash",
      "code": ": \"${RIGHTSHOLD_API_KEY:?Set your own account key first}\"\nread -r -p 'Completed action type (lowercase slug, up to 64 characters): ' ACTION_TYPE\nread -r -p 'Minimal private summary (up to 500 characters): ' ACTION_SUMMARY\nread -r -p 'Persistent opaque event key for this action: ' ACTION_EVENT_KEY\njq -n --arg type \"$ACTION_TYPE\" --arg summary \"$ACTION_SUMMARY\" \\\n  '{action_type:$type,summary:$summary}' > rightshold-action.json"
    },
    {
      "id": "local-evidence",
      "title": "3. Optionally bind an existing local record",
      "paragraphs": [
        "The optional action_sha256 field binds the private opening to the SHA-256 of exact bytes you keep locally. Before submitting, hash the completed action's own record, add the digest to the request body, and retain the bytes unchanged. Skip this step when no local record needs to be bound.",
        "A digest can later connect a supplied record to the receipt. It does not prove accuracy, execution or authorization. Never upload passwords, API keys, raw customer conversations or complete tool arguments just to obtain a receipt."
      ],
      "language": "bash",
      "code": "read -r -p 'Path to the completed action record you will retain locally: ' ACTION_RECORD\nACTION_SHA256=\"$(node -e 'const fs=require(\"node:fs\"),c=require(\"node:crypto\");process.stdout.write(c.createHash(\"sha256\").update(fs.readFileSync(process.argv[1])).digest(\"hex\"))' \"$ACTION_RECORD\")\"\njq --arg digest \"$ACTION_SHA256\" '. + {action_sha256:$digest}' \\\n  rightshold-action.json > rightshold-action-bound.json\nmv rightshold-action-bound.json rightshold-action.json"
    },
    {
      "id": "submit",
      "title": "4. Submit once and retain the receipt id",
      "paragraphs": [
        "The response returns receipt.id, receipt.ledger_seq and receipt.commitment. Save that mapping with the source event. A receipt describes what your account submitted; it does not independently establish that the described action happened.",
        "If submission fails after your business action completed, keep its outbox entry for reconciliation. Receipt logging must not cause a payment, message or other business action to execute again."
      ],
      "language": "bash",
      "code": "curl --fail-with-body --silent --show-error \"$RS_URL/v1/me/activity-receipts\" \\\n  -H \"Authorization: Bearer $RIGHTSHOLD_API_KEY\" \\\n  -H \"Idempotency-Key: $ACTION_EVENT_KEY\" \\\n  -H 'Content-Type: application/json' \\\n  --data-binary @rightshold-action.json > rightshold-receipt.json"
    },
    {
      "id": "retrieve",
      "title": "5. Retrieve the opening, public proof and export",
      "paragraphs": [
        "Only the owning account can retrieve its opening and salt. The public ledger entry contains the action type, commitment, time and subject; a linked public agent number is visible when present. Summary, counterparty, amount and the optional action hash stay in the private opening.",
        "An entry can be pending before its block is sealed. A pending entry has no signed inclusion proof: keep the receipt and retrieve its evidence again later. The export contains the private opening and available ledger evidence; downloading it never forces a pending record to become sealed."
      ],
      "language": "bash",
      "code": "RECEIPT_ID=\"$(jq -er '.receipt.id' rightshold-receipt.json)\"\ncurl --fail-with-body --silent --show-error \\\n  -H \"Authorization: Bearer $RIGHTSHOLD_API_KEY\" \\\n  \"$RS_URL/v1/me/activity-receipts/$RECEIPT_ID\" > rightshold-opening.json\nLEDGER_PATH=\"$(jq -er '.receipt.ledger_entry_url' rightshold-opening.json)\"\ncurl --fail-with-body --silent --show-error \\\n  \"$RS_URL$LEDGER_PATH\" > rightshold-public-proof.json\ncurl --fail-with-body --silent --show-error \\\n  -H \"Authorization: Bearer $RIGHTSHOLD_API_KEY\" \\\n  \"$RS_URL/v1/me/activity-receipts/$RECEIPT_ID/export\" > rightshold-evidence.json"
    },
    {
      "id": "verify",
      "title": "6. Check the private opening locally",
      "paragraphs": [
        "This command checks that your opening and salt match both the receipt commitment and the fetched public entry. It checks that link only. For signed inclusion, also recompute the payload and entry hashes, fold the Merkle proof to the block root, and verify the block's Ed25519 signature.",
        "Authenticate the issuer key through a separately trusted source. A public key inside an export is not, by itself, proof of who issued it. Signed inclusion records the submitted claim; independent execution, authorization and truth require their own evidence. Bitcoin confirmation and independent witness finality are separate facts, reported with the block's evidence."
      ],
      "language": "bash",
      "code": "node <<'NODE'\nconst fs = require('node:fs');\nconst crypto = require('node:crypto');\nconst receipt = JSON.parse(fs.readFileSync('rightshold-opening.json', 'utf8')).receipt;\nconst proof = JSON.parse(fs.readFileSync('rightshold-public-proof.json', 'utf8'));\nfunction canonical(value) {\n  if (value === null || typeof value !== 'object') return JSON.stringify(value);\n  if (Array.isArray(value)) return '[' + value.map(canonical).join(',') + ']';\n  return '{' + Object.keys(value).sort().map(key => JSON.stringify(key) + ':' + canonical(value[key])).join(',') + '}';\n}\nconst input = 'commit:' + canonical({ opening: receipt.opening, salt: receipt.salt });\nconst digest = crypto.createHash('sha256').update(input, 'utf8').digest('hex');\nif (digest !== receipt.commitment || digest !== proof.entry.payload.commitment) {\n  throw new Error('The opening does not match the recorded commitment.');\n}\nconsole.log('Opening matches commitment. Check signed inclusion and issuer trust separately.');\nNODE"
    },
    {
      "id": "offline-verify",
      "title": "7. Verify exported evidence offline",
      "paragraphs": [
        "Download the standalone verifier and retain an independently authenticated issuer public key in PEM format. Obtain that key through a separately trusted route, such as your approved deployment configuration or a verified key announcement. Do not extract the embedded export key and treat it as an independent trust decision.",
        "The verifier checks the signed export, commitment opening and available ledger inclusion evidence. Read its evidence status: a valid export signature does not turn pending inclusion into signed inclusion, and signed inclusion does not establish external finality or execution. If you bound exact local bytes, add --expected-action-sha256 with their freshly recomputed digest to check that binding too."
      ],
      "language": "bash",
      "code": "curl --fail-with-body --silent --show-error \\\n  \"$RS_URL/cli/verify-activity.cjs\" -o verify-activity.cjs\nread -r -p 'Path to independently authenticated issuer public key (PEM): ' TRUSTED_ISSUER_KEY\nnode verify-activity.cjs rightshold-evidence.json --trusted-key \"$TRUSTED_ISSUER_KEY\""
    },
    {
      "id": "statement",
      "title": "8. Give your principal an account statement",
      "paragraphs": [
        "Read the account's recorded actions and checks for a chosen period from 1 to 366 days. The response contains structured JSON and Markdown. A statement describes records held by Rightshold; it is not proof that every action the agent ever took was captured.",
        "Keep openings, salts, exports and statements private. Sharing an evidence bundle discloses its opening to the recipient. Retain an authorized copy alongside the original records rather than relying on a future download."
      ],
      "language": "bash",
      "code": "curl --fail-with-body --silent --show-error \\\n  -H \"Authorization: Bearer $RIGHTSHOLD_API_KEY\" \\\n  \"$RS_URL/v1/me/statement?days=30\" > rightshold-statement.json\njq -r '.markdown' rightshold-statement.json > rightshold-statement.md"
    },
    {
      "id": "python-client",
      "title": "Use the Python client after a completed event",
      "paragraphs": [
        "Download /sdks/rightshold_activity.py into your application. The standard-library client is off until you enable it and reads your account key from RIGHTSHOLD_API_KEY. Call record after your real workflow completes. This function accepts your own event fields; defining it creates no receipt.",
        "Exact record bytes are hashed locally; only the digest and the minimal summary are submitted. Retain the bytes and returned receipt id. By default a receipt outage returns None, so preserve the outbox entry and reconcile it. Do not repeat the original business action."
      ],
      "language": "python",
      "code": "from pathlib import Path\nfrom rightshold_activity import RightsholdActivity\n\nclient = RightsholdActivity(enabled=True)\n\ndef record_completed_event(source_event_id, action_type, private_summary, record_path):\n    return client.record(\n        event_id=source_event_id,\n        action_type=action_type,\n        summary=private_summary,\n        evidence=Path(record_path).read_bytes(),\n    )"
    },
    {
      "id": "typescript-client",
      "title": "Use the TypeScript or JavaScript client",
      "paragraphs": [
        "Download /sdks/rightshold-activity.cjs into your application. The standalone client is off until enabled and reads RIGHTSHOLD_API_KEY. Call this function only after the event completed, passing the original record's path and an event id persisted by your workflow.",
        "The client hashes exact record bytes locally and uses the same event id for bounded receipt retries. A null return leaves an event to reconcile, not a reason to execute the business action again. In both clients, get, proof, export and statement retrieve your saved evidence."
      ],
      "language": "javascript",
      "code": "const { readFileSync } = require('node:fs');\nconst { RightsholdActivity } = require('./rightshold-activity.cjs');\nconst client = new RightsholdActivity({ enabled: true });\n\nasync function recordCompletedEvent(sourceEventId, actionType, privateSummary, recordPath) {\n  return client.record({\n    eventId: sourceEventId,\n    actionType,\n    summary: privateSummary,\n    evidence: readFileSync(recordPath),\n  });\n}"
    },
    {
      "id": "repeat",
      "title": "9. Put receipts into the workflow you already run",
      "paragraphs": [
        "Record at meaningful boundaries: an approved action, completed delivery, external payment confirmation or released change. Keep original evidence and approval references locally. A durable outbox should connect your source event, event key, body and returned receipt id.",
        "Retry receipt submission after a timeout, connection failure, 429 or 5xx with bounded backoff and the same key and body. Do not repeat the underlying business action because its receipt failed. Investigate other 4xx responses instead of blindly retrying them.",
        "Replay protection lasts 24 hours. Reusing a key returns the first response even if you changed the body; use a new key for a different event. After the window, reconcile the saved receipt id and local outbox before submitting again. Deduplication is not permanent.",
        "Measure useful adoption by real workflows returning with independently checked receipts. Account creation, page visits and receipt count alone do not establish customer value or revenue."
      ]
    }
  ],
  "next": {
    "integration_guides": "https://www.rightshold.com/integrations",
    "scoped_pilot": "https://www.rightshold.com/enterprise",
    "ledger": "https://www.rightshold.com/ledger"
  }
}