High-risk Operation Verification
Try High-risk Step-up
Enter the DeviceId to simulate a Step-up verification for a transfer.
Identity verification only proves "is this the same Agent" — it does not mean the real person has approved this specific operation. High-risk actions such as transfers, withdrawals, and deletions must go through Step-up, requiring the real person to scan their palm in real time.
Step-up Level
| Level | Description | Behavior |
|---|---|---|
| 1 | Async notification only | Immediately approved + immediately signs Assertion JWT (also pushes notification to the real person) |
| 2 / 3 | Real person must scan palm | First pending, then JWT is issued only after the real person scans palm on PalmxApp to approved / denied |
Step 1 · Business Initiates Verification
POST /palm/openai/x/agent/stepup/create
Authorization: Bearer <api_key>
Content-Type: application/json
{
"DeviceId": "trader-001",
"ChallengeId": "550e8400-e29b-41d4-a716-446655440000", // Your generated UUID v4, idempotency key
"Action": "transfer_money",
"ActionDetail": "{\"amount\":\"1000 USD\",\"to\":\"alice\"}", // Passthrough JSON, ≤512B, displayed to the real person
"Level": 3,
"NonBlocking": false
}
Response (Level 2/3):
{
"code": 0,
"data": {
"ChallengeId": "550e8400-...",
"ExpiresAt": 1717488300,
"Status": "pending"
}
}
Response (Level 1, immediate approval): Status: "approved" with immediate AssertionJwt.
Idempotent: submitting the same
ChallengeIdagain does not create a new verification — it returns the existing status.
Step 2 · Real Person Scans Palm on PalmxApp
The server pushes a notification to the real person owner's PalmxApp. The real person sees the operation details (ActionDetail) and scans their palm to approve or reject. This step is handled by PalmxApp — the business does not need to implement it.
Step 3 · Business Polls for Assertion JWT
POST /palm/openai/x/agent/stepup/query
Authorization: Bearer <api_key>
Content-Type: application/json
{ "ChallengeId": "550e8400-..." }
Response:
{
"code": 0,
"data": {
"Status": "approved", // pending / approved / denied / expired
"AssertionJwt": "eyJhbGciOiJIUzI1Ni...." // One-time return when approved
}
}
| Status | Meaning | Business Action |
|---|---|---|
pending | Waiting for real person confirmation | Continue polling (recommended 2s interval) |
approved | Real person confirmed | Retrieve AssertionJwt, execute business logic after completing verification |
denied | Real person rejected | Stop, reject the operation |
expired | No response within timeout | Stop, prompt that retry is possible |
Step 4 · Verify Assertion JWT (Mandatory Before Execution)
Assertion JWT is the final credential for authorizing high-risk operations. Algorithm: HS256, secret: assertion_jwt.secret (issued by the platform, store env-only).
JWT Payload:
{
"iss": "trpc.x.oauth.OAuth",
"aud": "agent:{app_id}:{device_id}", // Audience: the Agent associated with this verification
"sub": "{owner_open_id}", // Real person owner
"iat": 1717488000,
"exp": 1717488300, // Default +5min
"jti": "{uuid v4}", // Anti-replay
"act": "transfer_money", // action
"adh": "sha256_hex(action_detail)", // action_detail hash
"lvl": 3,
"chl": "{challenge_id}"
}
Business must perform complete verification (in order):
def execute_dangerous_action(jwt_str, app_id, device_id, action, action_detail_bytes):
# 1. Verify signature + parse (auto-check exp)
try:
claims = jwt_decode(jwt_str, secret=ASSERTION_SECRET,
algorithms=["HS256"], options={"verify_aud": False})
except (InvalidSignature, ExpiredToken):
return reject("invalid assertion")
# 2. Check typ (JWT header)
if jwt_header(jwt_str).get("typ") != "palmauth-assertion+jwt":
return reject("wrong typ")
# 3. Verify aud points to the expected agent
if claims["aud"] != f"agent:{app_id}:{device_id}":
return reject("aud mismatch")
# 4. Verify action matches this operation
if claims["act"] != action:
return reject("action mismatch")
# 5. Verify action_detail has not been tampered with ←← Core check against amount/recipient tampering
# MUST use the original bytes from the Agent request; do not trust re-serialized JSON from the JWT
if claims["adh"] != sha256_hex(action_detail_bytes):
return reject("action_detail tampered")
# 6. JTI anti-replay (same JWT cannot be used twice)
if redis.set_nx(f"used:{claims['jti']}", "1", ex=300) is False:
return reject("replayed")
do_transfer(...) # Passed! Execute business logic
⚠️ Step 5 is the most critical: Must use the original
action_detailbytes from the Agent request to compute the hash, preventing amount/recipient tampering.⚠️ JTI deduplication is the business's responsibility: Once the JWT is issued, if not deduplicated within the 5-minute TTL, an attacker who captures it can reuse it.
⚠️ Fail-closed: When Palm is unavailable, high-risk operations must be rejected, never allowed through.