Skip to main content

High-risk Operation Verification

Live Demo

Try High-risk Step-up

Enter the DeviceId to simulate a Step-up verification for a transfer.

1
Enter DeviceId & initiate
2
Palm scan in PalmxApp
3
Get Assertion JWT

⚡ Sandbox demo — no data persisted.

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

LevelDescriptionBehavior
1Async notification onlyImmediately approved + immediately signs Assertion JWT (also pushes notification to the real person)
2 / 3Real person must scan palmFirst 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 ChallengeId again 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
}
}
StatusMeaningBusiness Action
pendingWaiting for real person confirmationContinue polling (recommended 2s interval)
approvedReal person confirmedRetrieve AssertionJwt, execute business logic after completing verification
deniedReal person rejectedStop, reject the operation
expiredNo response within timeoutStop, 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_detail bytes 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.