Universal Profile

Manage LUKSO Universal Profiles — identity, permissions, tokens, blockchain operations. Cross-chain support for Base and Ethereum.

MIT-0 · Free to use, modify, and redistribute. No attribution required.
12 · 1.9k · 1 current installs · 1 all-time installs
byFabian Vogelsteller@frozeman
MIT-0
Security Scan
VirusTotalVirusTotal
Benign
View report →
OpenClawOpenClaw
Benign
medium confidence
Purpose & Capability
Name/description (manage Universal Profiles, permissions, tokens, cross-chain) matches the code and CLI provided: RPC calls, permission encoding/decoding, token transfers, cross-chain deploy data, and relay support. The external endpoints (LUKSO RPC, Envio indexer, LUKSO relay, Base/Ethereum RPCs) are appropriate for the stated functionality. Minor note: the registry metadata lists no required env vars while SKILL.md documents optional envs (UP_CREDENTIALS_PATH, UP_KEY_PATH, UP_KEYSTORE_PASSWORD) and canonical config/keystore paths — this is expected (they are optional) and not a functional mismatch.
Instruction Scope
Runtime instructions and SKILL.md focus on profile/key management and transaction flows and instruct use of external Authorization UI and web-based profile creation. The skill reads/writes credential files under ~/.openclaw (or alternate paths via env), loads private keys for signing, and makes network calls to RPC, indexer, and relay endpoints — all within scope. Users should note it asks you to store private keys locally and to authorize via external web UIs; those steps are necessary for the feature but increase operational risk if misused.
Install Mechanism
No download/install spec; code is bundled with the skill (index.js, lib/, commands/). Dependencies are standard web3 libraries (ethers, viem) declared in package.json/package-lock.json. There are no external arbitrary download URLs or extract steps in the install spec.
Credentials
The skill legitimately needs access to controller private keys (stored in a keystore under ~/.openclaw or a path supplied via env) and optionally reads UP_KEYSTORE_PASSWORD from the environment if provided. It does not request unrelated cloud credentials. This access is proportionate to managing on-chain Universal Profiles, but handling private keys locally always carries risk and requires careful ops (file permissions, password strength).
Persistence & Privilege
The skill does not request always:true, does not modify other skills, and stores config/keystore under its own ~/.openclaw paths. Autonomous invocation (model invocation enabled) is the platform default and is not, by itself, a concern here.
Assessment
This skill appears to do what it says: manage LUKSO Universal Profiles and sign/submit transactions. Before installing, review and accept these operational cautions: - Verify the source: repository/homepage is not provided in the registry metadata — prefer skills from known authors or inspect the full source before use. - Endpoints: the skill talks to public RPCs, a LUKSO indexer (Envio), and LUKSO relay URLs. Confirm those domains (e.g., envio.lukso-..., relayer.testnet.lukso...) are correct/trusted for your use. - Private keys: the tool stores encrypted keystore files under ~/.openclaw by default and may read an env var UP_KEYSTORE_PASSWORD. Only use on machines you control, set keystore file permissions to 600, and prefer using a dedicated controller key (not your main wallet) for testing. - Permissions: be cautious when granting 'full-access' presets or saving a controller with powerful permissions (CHANGEOWNER, DELEGATECALL, SUPER_*). Use least privilege for automated bots. - Test safely: try operations with a testnet profile or a burner account first (lukso testnet) before using mainnet funds. - Optional code review: if you cannot verify the author, review the bundled code (it is included) or run in an isolated environment/VM. Look specifically at credential-handling code and the Authorization UI domain mentioned in SKILL.md. If you accept these caveats and verify endpoints/author, the skill is coherent with its declared purpose. If you are unsure about trusting the unknown source, do not provide production private keys or configure full-access controllers.

Like a lobster shell, security has layers — review code before you run it.

Current versionv0.9.0
Download zip
alphavk9775stx1ss41mcqv2eec2x9js80shnsblockchainvk9775stx1ss41mcqv2eec2x9js80shnserc725jsvk97edx222zrgp3k0k23rsq2889834673latestvk97edx222zrgp3k0k23rsq2889834673luksovk9775stx1ss41mcqv2eec2x9js80shnsweb3vk9775stx1ss41mcqv2eec2x9js80shns

License

MIT-0
Free to use, modify, and redistribute. No attribution required.

SKILL.md

Universal Profile Skill

Authorize your bot: create a profile at my.universalprofile.cloud, generate a controller key, authorize via Authorization UI.

Core Concepts

  • UP (Universal Profile) = smart contract account (LSP0/ERC725Account). This is the on-chain identity.
  • KeyManager (LSP6) = access control. Controllers have permission bitmasks.
  • Controller = EOA with permissions to act on behalf of the UP.
  • All calls to external contracts MUST route through UP via execute() so msg.sender = UP address.
  • Exception: setData()/setDataBatch() can be called directly on UP (checks permissions internally).

Execution Models

Direct (all chains — controller pays gas)

Controller → UP.execute(operation, target, value, data) → Target

The controller calls execute() directly on the UP contract. The UP internally verifies permissions via its KeyManager (LSP20 lsp20VerifyCall). Do NOT call the KeyManager's execute() function directly. Always call the UP.

Gasless Relay (LUKSO ONLY — chains 42/4201)

Controller signs LSP25 → Relay API submits → KeyManager.executeRelayCall() → UP

The controller signs a message, then the LUKSO relay service submits the transaction. Do NOT call executeRelayCall() yourself — the relay API does this.

⚠️ CRITICAL: The relay/gasless option exists ONLY on LUKSO mainnet (42) and testnet (4201). On Base, Ethereum, and all other chains, the controller must hold native ETH and pay gas directly. There is no gasless alternative.

Typical gas costs: LUKSO ~free via relay, Base ~$0.001-0.01/tx, Ethereum ~$0.10-1.00/tx.

Networks

ChainIDRPCExplorerRelayToken
LUKSO42https://42.rpc.thirdweb.comhttps://explorer.lukso.networkhttps://relayer.mainnet.lukso.network/apiLYX
LUKSO Testnet4201https://rpc.testnet.lukso.networkhttps://explorer.testnet.lukso.networkhttps://relayer.testnet.lukso.network/apiLYXt
Base8453https://mainnet.base.orghttps://basescan.orgETH
Ethereum1https://eth.llamarpc.comhttps://etherscan.ioETH

CLI

up status                                      # Config, keys, connectivity
up profile info [<address>] [--chain <chain>]  # Profile details
up profile configure <address> [--chain lukso]  # Save UP for use
up key generate [--save] [--password <pw>]     # Generate controller keypair
up permissions encode <perm1> [<perm2> ...]    # Encode to bytes32
up permissions decode <hex>                    # Decode to names
up permissions presets                         # List presets
up authorize url [--permissions <preset|hex>]  # Generate auth URL
up quota                                       # Check relay gas quota (LUKSO only)

Presets: read-only 🟢 | token-operator 🟡 | nft-trader 🟡 | defi-trader 🟠 | profile-manager 🟡 | full-access 🔴

Credentials

Config lookup order: UP_CREDENTIALS_PATH env → ~/.openclaw/universal-profile/config.json~/.clawdbot/universal-profile/config.json

Key lookup order: UP_KEY_PATH env → ~/.openclaw/credentials/universal-profile-key.json~/.clawdbot/credentials/universal-profile-key.json

Canonical path for new credentials: ~/.openclaw/credentials/universal-profile-key.json

Skill config path: ~/.openclaw/skills/universal-profile/config.json

Expected JSON format:

{
  "universalProfile": {
    "address": "0xYourUniversalProfileAddress"
  },
  "controller": {
    "address": "0xYourControllerAddress",
    "privateKey": "0xYourPrivateKey"
  }
}

Key file permissions: chmod 600. Keys loaded only for signing, then cleared. The skill warns if credential files are readable by group/others.

Permissions (bytes32 BitArray)

PermissionHexRiskNotes
CHANGEOWNER0x01🔴
ADDCONTROLLER0x02🟠
EDITPERMISSIONS0x04🟠
ADDEXTENSIONS0x08🟡
CHANGEEXTENSIONS0x10🟡
ADDUNIVERSALRECEIVERDELEGATE0x20🟡
CHANGEUNIVERSALRECEIVERDELEGATE0x40🟡
REENTRANCY0x80🟡
SUPER_TRANSFERVALUE0x0100🟠Any recipient
TRANSFERVALUE0x0200🟡AllowedCalls only
SUPER_CALL0x0400🟠Any contract
CALL0x0800🟡AllowedCalls only
SUPER_STATICCALL0x1000🟢
STATICCALL0x2000🟢
SUPER_DELEGATECALL0x4000🔴
DELEGATECALL0x8000🔴
DEPLOY0x010000🟡
SUPER_SETDATA0x020000🟠Any key
SETDATA0x040000🟡AllowedERC725YDataKeys only
ENCRYPT0x080000🟢
DECRYPT0x100000🟢
SIGN0x200000🟢
EXECUTE_RELAY_CALL0x400000🟢

SUPER variants = unrestricted. Regular = restricted to AllowedCalls/AllowedERC725YDataKeys. Prefer restricted.

Transactions

Direct Execution (all chains)

// Controller calls UP.execute() directly — works on LUKSO, Base, Ethereum
const provider = new ethers.JsonRpcProvider(rpcUrl);  // use correct RPC for chain
const wallet = new ethers.Wallet(controllerPrivateKey, provider);
const up = new ethers.Contract(upAddress, ['function execute(uint256,address,uint256,bytes) payable returns (bytes)'], wallet);
await (await up.execute(0, recipient, ethers.parseEther('0.01'), '0x')).wait();

Gasless Relay (LUKSO only)

LSP25 Relay Signature — EIP-191 v0, do NOT use signMessage():

const encoded = ethers.solidityPacked(
  ['uint256','uint256','uint256','uint256','uint256','bytes'],
  [25, chainId, nonce, validityTimestamps, msgValue, payload]
);
const prefix = new Uint8Array([0x19, 0x00]);
const msg = new Uint8Array([...prefix, ...ethers.getBytes(kmAddress), ...ethers.getBytes(encoded)]);
const signature = ethers.Signature.from(new ethers.SigningKey(privateKey).sign(ethers.keccak256(msg))).serialized;

Relay API:

POST https://relayer.mainnet.lukso.network/api/execute
{ "address": "0xUP", "transaction": { "abi": "0xpayload", "signature": "0x...", "nonce": 0, "validityTimestamps": "0x0" } }

The payload for relay calls is the full UP.execute(...) calldata. The relay service calls KeyManager.executeRelayCall() — you never call the KM directly.

For setData via relay, the payload is the setData(...) calldata (NOT wrapped in execute()).

Nonce channels: getNonce(controller, channelId) — same channel = sequential, different = parallel. Validity timestamps: (startTimestamp << 128) | endTimestamp. Use 0 for no restriction.

Cross-Chain Deployment (LSP23)

UPs can be redeployed at the same address on other chains by replaying the original LSP23 factory calldata.

Factory & Implementations (identical addresses on LUKSO, Base, Ethereum)

ContractAddress
LSP23 Factory0x2300000A84D25dF63081feAa37ba6b62C4c89a30
UniversalProfileInit v0.14.00x3024D38EA2434BA6635003Dc1BDC0daB5882ED4F
LSP6KeyManagerInit v0.14.00x2Fe3AeD98684E7351aD2D408A43cE09a738BF8a4
PostDeploymentModule0x000000000066093407b6704B89793beFfD0D8F00

Workflow

  1. Retrieve original deployment calldata: node commands/cross-chain-deploy-data.js <upAddress> [--verify]
  2. Fund controller with ETH on target chain
  3. Submit same calldata to factory: wallet.sendTransaction({ to: factoryAddress, data: calldata, value: 0n })
  4. Authorize controller on new chain via Authorization UI (permissions are per-chain)

Limitations

  • Legacy UPs (pre-LSP23, old lsp-factory) have no deployment events
  • Determinism requires identical salt + implementations + init data

LSP Ecosystem

LSPInterface IDNamePurpose
LSP00x24871b3dERC725AccountSmart contract account (UP)
LSP10x6bb56a14UniversalReceiverNotification hooks
LSP2ERC725Y JSON SchemaKey encoding
LSP3Profile MetadataName, avatar, links, tags
LSP4Digital Asset MetadataToken name, symbol, type
LSP5ReceivedAssetsTracks owned tokens/NFTs
LSP60x23f34c62KeyManagerPermission-based access control
LSP70xc52d6008DigitalAssetFungible tokens (like ERC20)
LSP80x3a271706IdentifiableDigitalAssetNFTs (bytes32 token IDs)
LSP90x28af17e6VaultSub-account for asset segregation
LSP140x94be5999Ownable2StepTwo-step ownership transfer
LSP250x5ac79908ExecuteRelayCallGasless meta-transactions (LUKSO only)
LSP260x2b299ceaFollowerSystemOn-chain follow/unfollow
LSP28TheGridCustomizable profile grid layouts

Full ABIs, interface IDs, and ERC725Y data keys in lib/constants.js.

LSP26 Follow/Unfollow

Contract: 0xf01103E5a9909Fc0DBe8166dA7085e0285daDDcA (LUKSO mainnet).

MUST route through UP via execute() — never call directly from controller.

const followData = lsp26Iface.encodeFunctionData('follow', [targetAddress]);
// Direct: km.execute(up.encodeFunctionData('execute', [0, LSP26_ADDR, 0, followData]))
// Relay: sign + submit via relay API

VerifiableURI (LSP2)

Format: 0x + 00006f357c6a0020 (8-byte header) + keccak256hash (32 bytes) + url as UTF-8 hex

Header = verificationMethod(2) + hashFunction(4=keccak256(utf8)) + hashLength(2=0x0020).

Decoding: skip 80 hex chars (2 + 8 + 4 + 64 + 2 prefix), rest = UTF-8 URL.

Common mistakes: forgetting 0020 hash length bytes, not pinning IPFS before on-chain tx, hash mismatch from re-serialization.

LSP3 Profile Update Procedure

  1. Read current: getData(0x5ef83ad9559033e6e941db7d7c495acdce616347d28e90c7ce47cbfcfcad3bc5) → decode VerifiableURI → fetch JSON
  2. Modify JSON
  3. Use { verification: { method: "keccak256(bytes)", data: "0x..." }, url: "ipfs://..." } for images
  4. Pin images + JSON to IPFS, verify accessible via gateway
  5. Compute keccak256(exactJsonBytes), encode VerifiableURI
  6. setData(LSP3_KEY, verifiableUri) from controller
  7. Verify: read back, decode, fetch, confirm

LSP3 key: 0x5ef83ad9559033e6e941db7d7c495acdce616347d28e90c7ce47cbfcfcad3bc5 LSP28 key: 0x724141d9918ce69e6b8afcf53a91748466086ba2c74b94cab43c649ae2ac23ff

LSP28 TheGrid

Grid layout JSON at LSP28 key as VerifiableURI.

{ "LSP28TheGrid": [{ "title": "My Grid", "gridColumns": 2, "visibility": "public",
  "grid": [
    { "width": 1, "height": 1, "type": "TEXT", "properties": { "title": "Hi", "text": "...", "backgroundColor": "#1a1a2e", "textColor": "#fff" } },
    { "width": 2, "height": 2, "type": "IMAGES", "properties": { "type": "grid", "images": ["https://..."] } },
    { "width": 1, "height": 1, "type": "X", "properties": { "type": "post", "username": "handle", "id": "tweetId", "theme": "dark" } }
  ]
}] }

Types: IFRAME, TEXT, IMAGES, X, INSTAGRAM, QR_CODE, ELFSIGHT. gridColumns 2–4, width/height 1–3.

Forever Moments (LUKSO only)

Social NFT platform. Agent API at https://www.forevermoments.life/api/agent/v1.

3-Step Relay Flow

  1. BuildPOST /moments/build-mint (or /collections/build-join, etc.) → get derived.upExecutePayload
  2. PreparePOST /relay/prepare with { upAddress, controllerAddress, payload } → get hashToSign, nonce
  3. Sign & Submit — sign hashToSign as RAW DIGEST (SigningKey.sign(), NOT signMessage()) → POST /relay/submit

Endpoints

  • /collections/build-join — join collection
  • /collections/build-create + /collections/finalize-create — create collection (2-step)
  • /moments/build-mint — mint Moment NFT
  • /relay/prepare + /relay/submit — relay flow
  • /api/pinata (NOT /api/agent/v1/pinata) — pin file to IPFS (multipart)

Metadata (LSP4)

{ "LSP4Metadata": { "name": "Title", "description": "...",
  "images": [[{ "width": 1024, "height": 1024, "url": "ipfs://Qm..." }]],
  "icon": [{ "width": 1024, "height": 1024, "url": "ipfs://Qm..." }],
  "tags": ["tag1"], "createdAt": "2026-02-08T16:30:00.000Z" } }

Known collection "Art by the Machine": 0x439f6793b10b0a9d88ad05293a074a8141f19d77

URLs

  • Collection: https://www.forevermoments.life/collections/<addr>
  • Moment: https://www.forevermoments.life/moments/<addr>
  • Profile: https://www.forevermoments.life/profile/<addr>

Error Codes

CodeCause
UP_PERMISSION_DENIEDController lacks required permission
UP_RELAY_FAILEDRelay error — check quota (LUKSO only)
UP_INVALID_SIGNATUREWrong chainId, used nonce, or expired timestamps
UP_QUOTA_EXCEEDEDMonthly relay quota exhausted (LUKSO only)
UP_NOT_AUTHORIZEDNot a controller — use Authorization UI

Security

  • Grant minimum permissions. Prefer CALL over SUPER_CALL.
  • Use AllowedCalls/AllowedERC725YDataKeys to restrict.
  • Avoid DELEGATECALL and CHANGEOWNER unless necessary.
  • Never log/print/transmit private keys.
  • Test on testnet (4201) first.
  • config set restricted to safe keys only.

ERC725.js — Reading & Writing UP Data

Package: @erc725/erc725.js — the standard library for reading, encoding, and decoding ERC725Y data on Universal Profiles. Use this for all profile data operations (reading profiles, permissions, assets, encoding setData payloads).

Full reference: references/ERC725-JS.md

Quick Start

import ERC725 from '@erc725/erc725.js';
import { LSP3Schema, LSP5Schema, LSP6Schema } from '@erc725/erc725.js/schemas';

// Connect to a UP
const erc725 = new ERC725(LSP3Schema, upAddress, 'https://42.rpc.thirdweb.com', {
  ipfsGateway: 'https://api.universalprofile.cloud/ipfs/',
});

// Read profile (fetches + verifies IPFS content)
const profile = await erc725.fetchData('LSP3Profile');

// Encode data for setData/setDataBatch
const { keys, values } = erc725.encodeData([{
  keyName: 'LSP3Profile',
  value: { json: profileJson, url: 'ipfs://Qm...' },
}]);

Key Operations

  • getData(key) — read raw decoded value from contract
  • fetchData(key) — read + download linked content (IPFS) + verify hash
  • encodeData(data) — encode for setData()/setDataBatch(){ keys, values }
  • decodeData(data) — decode raw contract bytes back to structured data
  • encodeKeyName(name) — hash a key name to bytes32
  • Built-in schemas: LSP1–LSP12, LSP17 (import from @erc725/erc725.js/schemas)

Common Patterns

// Read permissions
const erc725 = new ERC725(LSP6Schema, upAddress, RPC_URL);
const perms = await erc725.getData({
  keyName: 'AddressPermissions:Permissions:<address>',
  dynamicKeyParts: [controllerAddress],
});

// Read received assets
const erc725 = new ERC725(LSP5Schema, upAddress, RPC_URL);
const assets = await erc725.getData('LSP5ReceivedAssets[]');

// Encode VerifiableURI (auto-hashes JSON)
erc725.encodeData([{
  keyName: 'LSP3Profile',
  value: { json: myJson, url: 'ipfs://QmNew...' },
}]);

Important Notes

  • Array encoding: Always set totalArrayLength + startingIndex when modifying arrays to avoid corrupting contract state
  • Dynamic keys: Use dynamicKeyParts for Mapping key types (e.g., permissions per address)
  • Backend: Also install isomorphic-fetch
  • VerifiableURI: Pass { json, url } for auto-hashing, or { verification, url } for pre-computed

Dependencies

Node.js 18+, ethers.js v6, @erc725/erc725.js, viem.

Links

LUKSO Docs · Universal Everything · LSP6 Spec · Authorization UI

Profile URLs: always use https://universaleverything.io/<address>

Files

32 total
Select a file
Select a file to preview.

Comments

Loading comments…