{"templateId":"markdown","versions":[{"version":"v1.40","label":"v1.40 STS","link":"/products/custody/v1.40/how-to/govern-mpts-with-policies","default":true,"active":false,"folderId":"c15a2701"},{"version":"v1.39","label":"v1.39 STS","link":"/products/custody/v1.39/how-to/govern-mpts-with-policies","default":false,"active":false,"folderId":"c15a2701"},{"version":"v1.38","label":"v1.38 STS","link":"/products/custody/v1.38/how-to/govern-mpts-with-policies","default":false,"active":false,"folderId":"c15a2701"},{"version":"v1.34","label":"v1.34 LTS","link":"/products/custody/v1.34/how-to/govern-mpts-with-policies","default":false,"active":true,"folderId":"c15a2701"},{"version":"v1.26","label":"v1.26 LTS","link":"/products/custody/v1.26/how-to/govern-mpts-with-policies","default":false,"active":false,"folderId":"c15a2701"},{"version":"v1.19","label":"v1.19 LTS","link":"/products/custody/v1.19/how-to/govern-mpts-with-policies","default":false,"active":false,"folderId":"c15a2701"},{"version":"v1.15","label":"v1.15 LTS","link":"/products/custody/v1.15/how-to/govern-mpts-with-policies","default":false,"active":false,"folderId":"c15a2701"}],"sharedDataIds":{"sidebar":"sidebar-products/custody/@v1.15/sidebars.yaml"},"props":{"metadata":{"markdoc":{"tagList":["admonition"]},"type":"markdown"},"seo":{"title":"Govern MPTs with policies","description":"User guides, API reference, and support resources.","siteUrl":"https://docs.ripple.com","lang":"en-US","llmstxt":{"hide":false,"sections":[{"title":"Table of contents","includeFiles":["**/*"],"excludeFiles":[]}],"excludeFiles":[]}},"dynamicMarkdocComponents":[],"compilationErrors":[],"ast":{"$$mdtype":"Tag","name":"article","attributes":{},"children":[{"$$mdtype":"Tag","name":"Heading","attributes":{"level":1,"id":"govern-mpts-with-policies","__idx":0},"children":["Govern MPTs with policies"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["This guide shows how to write ","Ripple Custody"," policies that control Multi-Purpose Token (MPT) operations on the XRP Ledger, including requiring compliance approval for clawbacks, restricting who can create a new MPT issuance, and enforcing dual control on freeze actions."]},{"$$mdtype":"Tag","name":"Admonition","attributes":{"type":"info"},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Target audience"]},": Integrators and custodians rolling out MPTs who need to enforce approval workflows on issuer and holder actions."]}]},{"$$mdtype":"Tag","name":"Admonition","attributes":{"type":"warning"},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The UI policy builder does not currently expose XRPL operation-level targeting (for example, a selector for ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["MPTokenIssuanceCreate"]}," or ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Clawback"]},"). To govern MPT operations, author policies as JSON and submit them via the ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/products/custody/v1.34/api/environment/policy/create"},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["v0_CreatePolicy"]}]}," intent, or include them in the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["policies"]}," payload at ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/products/custody/v1.34/deployment/install/installation#policies"},"children":["genesis setup"]},"."]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"prerequisites","__idx":1},"children":["Prerequisites"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Ripple Custody"," v1.32 or later (MPT issuer operations are available from v1.32; holder operations from v1.31)."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["An XRPL account registered in ","Ripple Custody"," as the MPT issuer."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Familiarity with ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/products/custody/v1.34/api/environment/policy/conditions"},"children":["policy conditions"]}," and the ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/products/custody/v1.34/api/environment/policy/create"},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["v0_CreatePolicy"]}," payload structure"]},"."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["A set of user roles defined in your domain that reflects how your organisation separates duties. The examples below use illustrative names like ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["transaction-operator"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["treasury-operator"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["compliance"]},", and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["cfo"]},"; substitute the equivalents that already exist in your deployment."]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"how-mpt-operations-map-to-policy-conditions","__idx":2},"children":["How MPT operations map to policy conditions"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Two characteristics of MPTs affect how you write matching conditions:"]},{"$$mdtype":"Tag","name":"ol","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":["MPT operations do not have dedicated intent types. Every MPT action — create, mint, burn, freeze, clawback, destroy — is submitted as a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["v0_CreateTransactionOrder"]}," intent. Policies that filter only by ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["intentTypes: [\"v0_CreateTransactionOrder\"]"]}," match every XRPL and EVM transaction, not just MPT ones."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["The specific MPT operation is located at ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["context.request.payload.parameters.operation.type"]},". Policy conditions must inspect this path to target an individual operation."]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The following table maps each MPT action to the condition expression that identifies it. All expressions assume the intent is a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["v0_CreateTransactionOrder"]}," with ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["payload.parameters.type == \"XRPL\""]},"."]},{"$$mdtype":"Tag","name":"div","attributes":{"className":"md-table-wrapper"},"children":[{"$$mdtype":"Tag","name":"table","attributes":{"className":"md"},"children":[{"$$mdtype":"Tag","name":"thead","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"MPT action"},"children":["MPT action"]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"operation.type"},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["operation.type"]}]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Additional disambiguation"},"children":["Additional disambiguation"]}]}]},{"$$mdtype":"Tag","name":"tbody","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Create issuance"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["MPTokenIssuanceCreate"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["—"]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Destroy issuance"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["MPTokenIssuanceDestroy"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["—"]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Freeze (holder or global)"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["MPTokenIssuanceSet"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["operation.flags.includes('tfMPTLock')"]}]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Unfreeze (holder or global)"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["MPTokenIssuanceSet"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["operation.flags.includes('tfMPTUnlock')"]}]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Issuer authorizes a holder"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["MPTokenAuthorize"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["operation.holder != null"]}]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Holder opts in"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["MPTokenAuthorize"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["operation.holder == null"]}]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Clawback"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Clawback"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["operation.currency.type == 'MultiPurposeToken'"]}]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Mint, burn, or transfer"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Payment"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["operation.currency != null && operation.currency.type == 'MultiPurposeToken'"]}]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Escrow finish"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["EscrowFinish"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["—"]}]}]}]}]},{"$$mdtype":"Tag","name":"Admonition","attributes":{"type":"info"},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Mint, burn, and holder-to-holder transfer all use the same ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Payment"]}," operation with ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["currency.type == \"MultiPurposeToken\""]},". Direction distinguishes them:"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Mint"]},": source account is the issuer."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Burn"]},": destination is the issuer (tokens sent back to the issuer are automatically burned)."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Transfer"]},": source and destination are both holders."]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Compare ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["context.request.payload.accountId"]}," (the source) against the known issuer account UUID to disambiguate in conditions."]}]},{"$$mdtype":"Tag","name":"Admonition","attributes":{"type":"warning"},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["currency"]}," field is ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["optional"]}," on the XRPL ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Payment"]}," operation. For native XRP payments it is omitted in your request, but the policy scripting engine sees it as ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["\"currency\": null"]}," — the engine re-serializes the parsed domain object before evaluation, and absent optional fields are emitted as ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["null"]}," rather than dropped. Accessing ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["operation.currency.type"]}," then throws on ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["null"]},". Use ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["operation.currency != null"]}," to guard the access — this catches both the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["null"]}," and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["undefined"]}," cases. Throwing inside a condition causes the whole intent to fail with ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["PolicyScriptingExecutionFailed"]}," — not a quiet non-match. Guard every optional nested property access with ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["!= null"]},"."]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["For full payload examples of each operation, see ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/products/custody/v1.34/api/accounting/multi-purpose-tokens"},"children":["Multi-purpose tokens"]},"."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"example-policies","__idx":3},"children":["Example policies"]},{"$$mdtype":"Tag","name":"Admonition","attributes":{"type":"info"},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The policies below are illustrative examples, not a prescribed configuration. Your deployment will have its own role taxonomy, rank ladder, risk appetite, and account structure — adapt each example accordingly."]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Before deploying any policy from this page, review and replace the following. None of these values are platform defaults — they vary by deployment:"]},{"$$mdtype":"Tag","name":"div","attributes":{"className":"md-table-wrapper"},"children":[{"$$mdtype":"Tag","name":"table","attributes":{"className":"md"},"children":[{"$$mdtype":"Tag","name":"thead","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Illustrative in this guide"},"children":["Illustrative in this guide"]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"What to substitute"},"children":["What to substitute"]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"How to discover it"},"children":["How to discover it"]}]}]},{"$$mdtype":"Tag","name":"tbody","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Role names (",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["transaction-operator"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["compliance"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["treasury-operator"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["cfo"]},", etc.)"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["The role names your deployment already uses"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["GET /v1/domains/{domainId}/users/roles"]}]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Quorum counts and workflow shape"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["The number of approvers and the approval chain your control framework requires"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Your internal policy-design standard"]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Ranks (",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["760"]},"–",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["795"]},")"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Ranks that slot correctly into your existing ladder, below breakglass (900–999) and above standard transaction rules"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["GET /v1/domains/{domainId}/policies"]}," to see the current ladder"]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Pinned account UUID (",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["11111111-2222-3333-4444-555555555555"]},")"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["The real UUID of the account you intend to permit as the issuer"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["GET /v1/domains/{domainId}/accounts"]}]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Large-payment threshold (",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["1000000n"]},")"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["A threshold that reflects your MPT's ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["assetScale"]}," and your risk appetite"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["assetScale"]}," is set at issuance creation; pair it with a display-unit amount you care about"]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Scope (",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Self"]}," / ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["SelfAndDescendants"]},")"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["The scope that matches your domain hierarchy and who you want the policy to cover"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Your domain model"]}]}]}]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Once adapted, install each policy with ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["lock: \"Locked\""]}," first, unlock it, submit a representative intent from a test account, and confirm via ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["GET /v1/domains/{domainId}/intents/{intentId}.state.progressPerPolicy[].policyReference.id"]}," that the intended policy is matched before rolling out further."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"require-compliance-approval-for-clawback","__idx":4},"children":["Require compliance approval for clawback"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Clawback lets an issuer reclaim tokens from a holder without consent. One common pattern is to require the submitting operator plus two compliance approvers on every clawback; adjust the roles and quorum to fit your approval policy."]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"json","header":{"controls":{"copy":{}}},"source":"{\n  \"payload\": {\n    \"id\": \"{{policy_id}}\",\n    \"alias\": \"mpt-clawback-requires-compliance\",\n    \"rank\": 780,\n    \"intentTypes\": [\"v0_CreateTransactionOrder\"],\n    \"scope\": \"Self\",\n    \"scriptingEngine\": \"Javascript_v0\",\n    \"condition\": {\n      \"type\": \"Expression\",\n      \"expression\": \"context.request.payload.parameters.type == 'XRPL' && context.request.payload.parameters.operation != null && context.request.payload.parameters.operation.type == 'Clawback' && context.request.payload.parameters.operation.currency.type == 'MultiPurposeToken'\"\n    },\n    \"workflow\": [\n      {\n        \"type\": \"And\",\n        \"left\":  { \"type\": \"RoleQuorum\", \"role\": \"transaction-operator\", \"quorum\": 1 },\n        \"right\": { \"type\": \"RoleQuorum\", \"role\": \"compliance\",           \"quorum\": 2 }\n      }\n    ],\n    \"lock\": \"Unlocked\",\n    \"customProperties\": {},\n    \"type\": \"v0_CreatePolicy\"\n  }\n}\n","lang":"json"},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"restrict-mpt-issuance-creation-to-a-specific-account","__idx":5},"children":["Restrict MPT issuance creation to a specific account"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["If your policy is that only one or two operational accounts should ever create new MPT issuances, you can pin creation to a specific account UUID. The example below pins to a single UUID and requires treasury plus compliance approval; replace the placeholder UUID with the issuer account in your deployment."]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"json","header":{"controls":{"copy":{}}},"source":"{\n  \"payload\": {\n    \"id\": \"{{policy_id}}\",\n    \"alias\": \"mpt-issuance-create-pinned-account\",\n    \"rank\": 790,\n    \"intentTypes\": [\"v0_CreateTransactionOrder\"],\n    \"scope\": \"SelfAndDescendants\",\n    \"scriptingEngine\": \"Javascript_v0\",\n    \"condition\": {\n      \"type\": \"Expression\",\n      \"expression\": \"context.request.payload.parameters != null && context.request.payload.parameters.operation != null && context.request.payload.parameters.operation.type == 'MPTokenIssuanceCreate' && context.request.payload.accountId == '11111111-2222-3333-4444-555555555555'\"\n    },\n    \"workflow\": [\n      {\n        \"type\": \"And\",\n        \"left\":  { \"type\": \"RoleQuorum\", \"role\": \"treasury-operator\", \"quorum\": 1 },\n        \"right\": { \"type\": \"RoleQuorum\", \"role\": \"compliance\",        \"quorum\": 2 }\n      }\n    ],\n    \"lock\": \"Unlocked\",\n    \"customProperties\": {},\n    \"type\": \"v0_CreatePolicy\"\n  }\n}\n","lang":"json"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["To reject MPT issuance creation from any other account, pair the policy above with a lower-ranked catch-all that has an empty ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["workflow"]}," array. An empty ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["workflow"]}," auto-rejects every matching intent."]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"json","header":{"controls":{"copy":{}}},"source":"{\n  \"payload\": {\n    \"id\": \"{{policy_id}}\",\n    \"alias\": \"mpt-issuance-create-reject-other-accounts\",\n    \"rank\": 770,\n    \"intentTypes\": [\"v0_CreateTransactionOrder\"],\n    \"scope\": \"SelfAndDescendants\",\n    \"scriptingEngine\": \"Javascript_v0\",\n    \"condition\": {\n      \"type\": \"Expression\",\n      \"expression\": \"context.request.payload.parameters != null && context.request.payload.parameters.operation != null && context.request.payload.parameters.operation.type == 'MPTokenIssuanceCreate'\"\n    },\n    \"workflow\": [],\n    \"lock\": \"Unlocked\",\n    \"customProperties\": {},\n    \"type\": \"v0_CreatePolicy\"\n  }\n}\n","lang":"json"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The pinned-account policy (rank 790) is evaluated first and applies to the authorized account. The catch-all (rank 770) applies only when the first policy does not match, and rejects the intent."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"require-dual-control-on-freeze-and-unfreeze","__idx":6},"children":["Require dual control on freeze and unfreeze"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Freezing a holder or the entire issuance can halt all transfers of the token, so dual control is a common control point. The example below applies a maker-checker workflow to both freeze and unfreeze actions in one policy; you can split into two policies if the two actions merit different approval paths."]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"json","header":{"controls":{"copy":{}}},"source":"{\n  \"payload\": {\n    \"id\": \"{{policy_id}}\",\n    \"alias\": \"mpt-freeze-unfreeze-dual-control\",\n    \"rank\": 785,\n    \"intentTypes\": [\"v0_CreateTransactionOrder\"],\n    \"scope\": \"Self\",\n    \"scriptingEngine\": \"Javascript_v0\",\n    \"condition\": {\n      \"type\": \"Expression\",\n      \"expression\": \"context.request.payload.parameters.operation != null && context.request.payload.parameters.operation.type == 'MPTokenIssuanceSet' && (context.request.payload.parameters.operation.flags.includes('tfMPTLock') || context.request.payload.parameters.operation.flags.includes('tfMPTUnlock'))\"\n    },\n    \"workflow\": [\n      { \"type\": \"RoleQuorum\", \"role\": \"transaction-operator\", \"quorum\": 1 },\n      { \"type\": \"RoleQuorum\", \"role\": \"compliance\",           \"quorum\": 1 }\n    ],\n    \"lock\": \"Unlocked\",\n    \"customProperties\": {},\n    \"type\": \"v0_CreatePolicy\"\n  }\n}\n","lang":"json"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["To apply different workflows for freeze and unfreeze, create two policies with distinct conditions: one that checks ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["flags.includes('tfMPTLock')"]},", and one that checks ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["flags.includes('tfMPTUnlock')"]},"."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"require-extra-approval-for-large-mpt-payments","__idx":7},"children":["Require extra approval for large MPT payments"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Mint, burn, and holder-to-holder transfers all use the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Payment"]}," operation. The example below shows how to add an amount-based threshold: any MPT payment at or above ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["1000000n"]}," base units requires CFO approval. The threshold value, the role, and the quorum are illustrative — pick values that match your own risk appetite and role taxonomy. Guard ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["operation.currency"]}," first because it is optional on the Payment schema."]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"json","header":{"controls":{"copy":{}}},"source":"{\n  \"payload\": {\n    \"id\": \"{{policy_id}}\",\n    \"alias\": \"mpt-large-payment-cfo-approval\",\n    \"rank\": 760,\n    \"intentTypes\": [\"v0_CreateTransactionOrder\"],\n    \"scope\": \"Self\",\n    \"scriptingEngine\": \"Javascript_v0\",\n    \"condition\": {\n      \"type\": \"Expression\",\n      \"expression\": \"context.request.payload.parameters.operation != null && context.request.payload.parameters.operation.type == 'Payment' && context.request.payload.parameters.operation.currency != null && context.request.payload.parameters.operation.currency.type == 'MultiPurposeToken' && BigInt(context.request.payload.parameters.operation.amount) >= 1000000n\"\n    },\n    \"workflow\": [\n      {\n        \"type\": \"And\",\n        \"left\":  { \"type\": \"RoleQuorum\", \"role\": \"transaction-operator\", \"quorum\": 1 },\n        \"right\": { \"type\": \"RoleQuorum\", \"role\": \"cfo\",                  \"quorum\": 1 }\n      }\n    ],\n    \"lock\": \"Unlocked\",\n    \"customProperties\": {},\n    \"type\": \"v0_CreatePolicy\"\n  }\n}\n","lang":"json"},"children":[]},{"$$mdtype":"Tag","name":"Admonition","attributes":{"type":"warning"},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":["MPT ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["amount"]}," values are serialized as strings and can exceed 2^53. Wrap the value in ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["BigInt(...)"]}," and use the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["n"]}," suffix on the threshold literal (",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["1000000n"]},") so the comparison is lossless. Comparing as plain JavaScript numbers silently loses precision."]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["If the token's ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["assetScale"]}," is 6, the threshold ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["1000000n"]}," corresponds to one display unit. Adjust the threshold to match the issuance's decimal precision."]},{"$$mdtype":"Tag","name":"Admonition","attributes":{"type":"info"},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Payment"]}," operation uses the field name ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["amount"]},". The ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Clawback"]}," operation uses ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["value"]},". When combining thresholds across operations, use the correct field per operation type."]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"require-a-high-approval-bar-for-destroy-issuance","__idx":8},"children":["Require a high approval bar for destroy issuance"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["MPTokenIssuanceDestroy"]}," permanently retires an MPT once supply reaches zero, so deployments typically set a high approval bar for it. The example below requires treasury plus two compliance approvers; adjust to fit your control framework."]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"json","header":{"controls":{"copy":{}}},"source":"{\n  \"payload\": {\n    \"id\": \"{{policy_id}}\",\n    \"alias\": \"mpt-destroy-issuance-high-bar\",\n    \"rank\": 795,\n    \"intentTypes\": [\"v0_CreateTransactionOrder\"],\n    \"scope\": \"SelfAndDescendants\",\n    \"scriptingEngine\": \"Javascript_v0\",\n    \"condition\": {\n      \"type\": \"Expression\",\n      \"expression\": \"context.request.payload.parameters.operation != null && context.request.payload.parameters.operation.type == 'MPTokenIssuanceDestroy'\"\n    },\n    \"workflow\": [\n      {\n        \"type\": \"And\",\n        \"left\":  { \"type\": \"RoleQuorum\", \"role\": \"treasury-operator\", \"quorum\": 1 },\n        \"right\": { \"type\": \"RoleQuorum\", \"role\": \"compliance\",        \"quorum\": 2 }\n      }\n    ],\n    \"lock\": \"Unlocked\",\n    \"customProperties\": {},\n    \"type\": \"v0_CreatePolicy\"\n  }\n}\n","lang":"json"},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"common-pitfalls","__idx":9},"children":["Common pitfalls"]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"generic-transaction-policies-outranking-mpt-policies","__idx":10},"children":["Generic transaction policies outranking MPT policies"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Policies are evaluated in descending rank order. If a generic ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["v0_CreateTransactionOrder"]}," policy has a higher rank than the MPT-specific policies, it captures the intent first and the MPT rules never apply. Always give MPT-specific policies a higher rank than the generic transaction policy."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"unguarded-property-access-throws-and-fails-the-intent","__idx":11},"children":["Unguarded property access throws and fails the intent"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["If any part of the path in a condition resolves to ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["null"]}," at evaluation time, accessing a nested property throws a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["TypeError"]}," and the intent terminates with ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["status: \"Failed\""]}," and error code ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["PolicyScriptingExecutionFailed"]},". This is not a quiet non-match — the intent is rejected entirely and never reaches later policies or execution. Guard every optional nested access with ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["!= null"]},":"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"javascript","header":{"controls":{"copy":{}}},"source":"context.request.payload.parameters.operation != null &&\ncontext.request.payload.parameters.operation.type == 'Clawback'\n","lang":"javascript"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Do not use ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["hasOwnProperty"]}," for these guards. The policy scripting engine re-serializes the parsed domain object before evaluation, and the serializer emits every optional field as ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["\"key\": null"]}," rather than omitting it. The key is always present on the JSON the condition sees, so ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["hasOwnProperty"]}," always returns ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["true"]}," and provides no protection. Use ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["!= null"]}," consistently — it catches both the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["null"]}," and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["undefined"]}," cases."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"integer-precision-for-amounts","__idx":12},"children":["Integer precision for amounts"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["MPT ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["amount"]}," and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["value"]}," fields are serialized as strings and can exceed JavaScript's safe integer range. Use ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["BigInt(...)"]}," around the field and the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["n"]}," suffix on literals in the condition. Comparing as plain numbers silently loses precision for large values."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"validating-policies-before-rollout","__idx":13},"children":["Validating policies before rollout"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Dry-run validates the JSON schema and that the condition parses as JavaScript. It does ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["not"]}," validate that path expressions resolve against a real intent and it does ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["not"]}," simulate ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["workflow: []"]}," rejection. Runtime errors surface only when an actual ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["v0_CreateTransactionOrder"]}," intent is submitted."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["To validate behaviour safely:"]},{"$$mdtype":"Tag","name":"ol","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Deploy the policy with ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["lock: \"Locked\""]}," initially so it does not affect production traffic."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Unlock the policy and submit a representative MPT intent from a test account."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Fetch ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["GET /v1/domains/{domainId}/intents/{intentId}"]}," and inspect ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["state.status"]}," (",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Rejected"]}," / ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Executed"]}," / ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Open"]},") and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["state.progressPerPolicy[].policyReference.id"]}," to confirm the expected policy matched."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["If ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["state.status"]}," is ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Failed"]}," with ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["error.code == \"PolicyScriptingExecutionFailed\""]},", the condition threw at runtime — add or tighten a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["!= null"]}," guard on the nested access that resolved to ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["null"]},"."]}]},{"$$mdtype":"Tag","name":"hr","attributes":{},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"related-topics","__idx":14},"children":["Related topics"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/products/custody/v1.34/concepts/multi-purpose-tokens"},"children":["Multi-purpose tokens concepts"]}," — MPT lifecycle, flags, and on-chain metadata."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/products/custody/v1.34/api/accounting/multi-purpose-tokens"},"children":["Multi-purpose tokens (API)"]}," — Payload reference for each MPT operation."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/products/custody/v1.34/api/environment/policy/conditions"},"children":["Policy conditions"]}," — ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["context"]}," object and JavaScript expression syntax."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/products/custody/v1.34/api/environment/policy/create"},"children":["Create policies"]}," — ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["v0_CreatePolicy"]}," payload structure."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/products/custody/v1.34/get-started/design/policies/examples"},"children":["Policy examples"]}," — Worked policy patterns for other intent types."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"a","attributes":{"href":"https://xrpl.org/docs/concepts/tokens/fungible-tokens/multi-purpose-tokens","target":"_blank"},"children":["Multi-purpose tokens (XRPL documentation)"]}," — Protocol-level reference."]}]}]},"headings":[{"value":"Govern MPTs with policies","id":"govern-mpts-with-policies","depth":1},{"value":"Prerequisites","id":"prerequisites","depth":2},{"value":"How MPT operations map to policy conditions","id":"how-mpt-operations-map-to-policy-conditions","depth":2},{"value":"Example policies","id":"example-policies","depth":2},{"value":"Require compliance approval for clawback","id":"require-compliance-approval-for-clawback","depth":3},{"value":"Restrict MPT issuance creation to a specific account","id":"restrict-mpt-issuance-creation-to-a-specific-account","depth":3},{"value":"Require dual control on freeze and unfreeze","id":"require-dual-control-on-freeze-and-unfreeze","depth":3},{"value":"Require extra approval for large MPT payments","id":"require-extra-approval-for-large-mpt-payments","depth":3},{"value":"Require a high approval bar for destroy issuance","id":"require-a-high-approval-bar-for-destroy-issuance","depth":3},{"value":"Common pitfalls","id":"common-pitfalls","depth":2},{"value":"Generic transaction policies outranking MPT policies","id":"generic-transaction-policies-outranking-mpt-policies","depth":3},{"value":"Unguarded property access throws and fails the intent","id":"unguarded-property-access-throws-and-fails-the-intent","depth":3},{"value":"Integer precision for amounts","id":"integer-precision-for-amounts","depth":3},{"value":"Validating policies before rollout","id":"validating-policies-before-rollout","depth":3},{"value":"Related topics","id":"related-topics","depth":2}],"frontmatter":{"seo":{"title":"Govern MPTs with policies"}},"lastModified":"2026-04-30T15:25:17.000Z","pagePropGetterError":{"message":"","name":""}},"slug":"/products/custody/v1.34/how-to/govern-mpts-with-policies","userData":{"isAuthenticated":false,"teams":["anonymous"]},"isPublic":true}